diff --git a/CHANGELOG b/CHANGELOG index d3e7c505c..2e301dc39 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,51 @@ +27-4-2018 (v2.1.0) + * update Tutorials + * shapes.Standard - add new set of high-performance elements and links + * dia.LinkView - new flexible definitions based on geometric representation + * dia.LinkView - refactor event handles + * dia.LinkView - introduce anchors, connectionPoints and connectionStrategy + * dia.LinkView - add getConnection(), getSerializedConnection(), getConnectionSubdivisions(), getPointAtRatio(), getTangentAtLength(), getTangentAtRatio() getClosestPoint() and getClosestPointLength() + * dia.LinkView - add getVertexIndex(), getLabelCoordinates() + * dia.Link - add vertex API + * dia.Link - add label API and allow define a default label + * dia.Link - add source(), target(), router(), connector() + * anchors - ready-to-use anchors (center, top, bottom, left, right, topLeft, topRight, bottomLeft, bottomRight, perpendicular, midSide) + * connectionPoints - ready-to-use connection points (anchor, bbox, rectangle, boundary), + * connectionStrategies - predefined connection strategies (defaulAnchor, pinAbsolute, pinRelative) + * dia.ElementView - allow element's rotation without rotatable group + * dia.ElementView - refactor event handlers + * dia.ElementView - apply vector-effect: non-scaling-stroke to nodes inside ths scalable group only + * dia.Element - add angle() + * dia.CellView - render markup from JSON (link, element, ports and labels) + * dia.Cell - avoid unnecessary z-index changes during toFront or toBack + * dia.ToolsView - set of tools for a link + * dia.ToolView - base class for a single link tool + * linkTools - ready-to-use tools (vertices, segments, anchor, arrowhead, boundary, remove button) + * dia.Paper - complete set of events + * dia.Paper - add allowLink option to revert/remove invalid links + * dia.Paper - add getContentArea() + * dia.Paper - findParentBy option can be defined as a function + * dia.Paper - consequitive pointerdown, pointermove and pointerup can share event data + * dia.Paper - fire pointerup event on touchcancel + * dia.Paper - improve preventing image dragging in FireFox + * dia.attributes - sourceMarker, targetMarker and vertextMarker receive default stroke, fill and opacity values from its context + * dia.attributes - add refRInscribed, refRCircumscribed, refD, refPoints, title, textVerticalAnchor attributes + * dia.attributes - add connection, atConnectionLength, atConnectionRatio + * routers.Manhattan - adaptive grid for pathfinding + * routers - supports anchors (don't necessary start and end in the center of the magnet) + * layout.DirectedGraph - prevent undesired input cells sorting + * Vectorizer - add toGeometryShape(), normalizePathData(), tagName() and id to prototype + * Vectorizer - add transformLine() and transformPolyline() + * Vectorizer - text() accepts textVerticalAnchor option + * Vectorizer - improve Kappa value + * Geometry - add Path and Curves + * Geometry - add Polyline bbox(), scale(), translate(), clone() and serialize() + * Geometry - implement intersections of line with various shapes + * Geometry - add Point lerp() for linear interpolation + * shapes.basic.TextBlock - sanitize text + * util - normalizeSides() validates input and accepts horizontal and vertical attributes + * util - add parseDOMJSON(), dataUriToBlob(), downloadBlob(), downloadDataUri() and isPercentage() + 15-11-2017 (v2.0.1) * toggleFullscreen() - fix canceling fullscreen in an iframe * dia.Link - fix default label font color (IE) @@ -40,7 +88,7 @@ * Vectorizer - add appendTo() method * Vectorizer - V(node); does not set automatically id on the node anymore * Vectorizer - text() with content doesn't set invalid display: null on node anymore - * Vectorizer - fix convertRectToPathData() for rounded rectangles + * Vectorizer - fix convertRectToPathData() for rounded rectangles * dia.attributes - add namespace for defining custom attributes, allow camelCase attribute style * dia.attributes - new attributes `sourceMarker`, `targetMarker`, `vertexMarker`, `textWrap`, `refRx`, `refRy`, `refCx`, `refCy`, `refX2`, `refY2` * dia.attributes - improve `text` attribute performance on cellView update diff --git a/dist/geometry.js b/dist/geometry.js index b12041f27..a612bd424 100644 --- a/dist/geometry.js +++ b/dist/geometry.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -27,8 +27,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. }(this, function() { - -// Geometry library. +// Geometry library. +// ----------------- var g = (function() { @@ -40,8 +40,8 @@ var g = (function() { var cos = math.cos; var sin = math.sin; var sqrt = math.sqrt; - var mmin = math.min; - var mmax = math.max; + var min = math.min; + var max = math.max; var atan2 = math.atan2; var round = math.round; var floor = math.floor; @@ -52,27 +52,25 @@ var g = (function() { 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). + // @deprecated // @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); - } + console.warn('deprecated'); - return path; + return new Path(Curve.throughPoints(points)).serialize(); }, // Get open-ended Bezier Spline Control Points. + // @deprecated // @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) { + console.warn('deprecated'); + var firstControlPoints = []; var secondControlPoints = []; var n = knots.length - 1; @@ -81,11 +79,17 @@ var g = (function() { // 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); + firstControlPoints[0] = new 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); + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); + return [firstControlPoints, secondControlPoints]; } @@ -97,8 +101,10 @@ var g = (function() { 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); @@ -106,50 +112,44 @@ var g = (function() { 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])); + firstControlPoints.push(new 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])); + secondControlPoints.push(new 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)); + secondControlPoints.push(new 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 }]; - }; + return [firstControlPoints, secondControlPoints]; }, // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @deprecated // @param rhs Right hand side vector. // @return Solution vector. getFirstControlPoints: function(rhs) { + console.warn('deprecated'); + var n = rhs.length; // `x` is a solution vector. var x = []; @@ -157,870 +157,981 @@ var g = (function() { 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; }, + // Divide a Bezier curve into two at point defined by value 't' <0,1>. + // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 + // @deprecated + // @param control points (start, control start, control end, end) + // @return a function that accepts t and returns 2 curves. + getCurveDivider: function(p0, p1, p2, p3) { + + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + + return function divideCurve(t) { + + var divided = curve.divide(t); + + return [{ + p0: divided[0].start, + p1: divided[0].controlPoint1, + p2: divided[0].controlPoint2, + p3: divided[0].end + }, { + p0: divided[1].start, + p1: divided[1].controlPoint1, + p2: divided[1].controlPoint2, + p3: divided[1].end + }]; + }; + }, + // 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. + // @deprecated // @param control points (start, control start, control end, end) - // @return a function accepts a point and returns t. + // @return a function that 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; - }; - } + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + 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); + + return curve.closestPointT(p); }; } }; - var Ellipse = g.Ellipse = function(c, a, b) { + var Curve = g.Curve = function(p1, p2, p3, p4) { - if (!(this instanceof Ellipse)) { - return new Ellipse(c, a, b); + if (!(this instanceof Curve)) { + return new Curve(p1, p2, p3, p4); } - if (c instanceof Ellipse) { - return new Ellipse(Point(c), c.a, c.b); + if (p1 instanceof Curve) { + return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end); } - c = Point(c); - this.x = c.x; - this.y = c.y; - this.a = a; - this.b = b; + this.start = new Point(p1); + this.controlPoint1 = new Point(p2); + this.controlPoint2 = new Point(p3); + this.end = new Point(p4); }; - g.Ellipse.fromRect = function(rect) { + // Curve passing 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} curves. + Curve.throughPoints = (function() { - rect = Rect(rect); - return Ellipse(rect.center(), rect.width / 2, rect.height / 2); - }; + // 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. + function getFirstControlPoints(rhs) { + + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; - g.Ellipse.prototype = { + x[0] = rhs[0] / b; - bbox: function() { + // 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; + } - return Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); - }, + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } - clone: function() { + return x; + } - return Ellipse(this); - }, + // 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. + function getCurveControlPoints(knots) { - /** - * @param {g.Point} point - * @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside - */ - normalizedDistance: function(point) { + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; - var x0 = point.x; - var y0 = point.y; - var a = this.a; - var b = this.b; - var x = this.x; - var y = this.y; + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new Point( + (2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3 + ); - return ((x0 - x) * (x0 - x)) / (a * a ) + ((y0 - y) * (y0 - y)) / (b * b); - }, + // P2 = 2P1 – P0 + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); - // 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; + return [firstControlPoints, secondControlPoints]; } - if (dy === undefined) { - dy = dx; + // 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; } - this.a += 2 * dx; - this.b += 2 * dy; + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - return this; - }, + // Get first control points X-values. + var x = 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; + } - /** - * @param {g.Point} p - * @returns {boolean} - */ - containsPoint: function(p) { + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - return this.normalizedDistance(p) <= 1; - }, + // Get first control points Y-values. + var y = getFirstControlPoints(rhs); - /** - * @returns {g.Point} - */ - center: function() { + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(new Point(x[i], y[i])); - return Point(this.x, this.y); - }, + // Second control point. + if (i < n - 1) { + secondControlPoints.push(new Point( + 2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1] + )); - /** 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) { + } else { + secondControlPoints.push(new Point( + (knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2 + )); + } + } - 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; + return [firstControlPoints, secondControlPoints]; + } - var q1 = x0 > center.x + a / 2; - var q3 = x0 < center.x - a / 2; + return function(points) { - 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; + if (!points || (Array.isArray(points) && points.length < 2)) { + throw new Error('At least 2 points are required'); } - return g.point(x, y).theta(p); + var controlPoints = getCurveControlPoints(points); - }, + var curves = []; + var n = controlPoints[0].length; + for (var i = 0; i < n; i++) { - equals: function(ellipse) { + var controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y); + var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y); - return !!ellipse && - ellipse.x === this.x && - ellipse.y === this.y && - ellipse.a === this.a && - ellipse.b === this.b; - }, + curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1])); + } - // 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) { + return curves; + }; + })(); - 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))); + Curve.prototype = { - 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; - }, + // Returns a bbox that tightly envelops the curve. + bbox: function() { - toString: function() { + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; - return Point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; - } - }; + var x0 = start.x; + var y0 = start.y; + var x1 = controlPoint1.x; + var y1 = controlPoint1.y; + var x2 = controlPoint2.x; + var y2 = controlPoint2.y; + var x3 = end.x; + var y3 = end.y; - var Line = g.Line = function(p1, p2) { + var points = new Array(); // local extremes + var tvalues = new Array(); // t values of local extremes + var bounds = [new Array(), new Array()]; - if (!(this instanceof Line)) { - return new Line(p1, p2); - } + var a, b, c, t; + var t1, t2; + var b2ac, sqrtb2ac; - if (p1 instanceof Line) { - return Line(p1.start, p1.end); - } + for (var i = 0; i < 2; ++i) { - this.start = Point(p1); - this.end = Point(p2); - }; + if (i === 0) { + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; - g.Line.prototype = { + } else { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } - // @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() { + if (abs(a) < 1e-12) { // Numerical robustness + if (abs(b) < 1e-12) { // Numerical robustness + continue; + } - 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)); + t = -c / b; + if ((0 < t) && (t < 1)) tvalues.push(t); - var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']; + continue; + } - var index = brng - 22.5; - if (index < 0) - index += 360; - index = parseInt(index / 45); + b2ac = b * b - 4 * c * a; + sqrtb2ac = sqrt(b2ac); - return bearings[index]; - }, + if (b2ac < 0) continue; - clone: function() { + t1 = (-b + sqrtb2ac) / (2 * a); + if ((0 < t1) && (t1 < 1)) tvalues.push(t1); - return Line(this.start, this.end); - }, + t2 = (-b - sqrtb2ac) / (2 * a); + if ((0 < t2) && (t2 < 1)) tvalues.push(t2); + } - equals: function(l) { + var j = tvalues.length; + var jlen = j; + var mt; + var x, y; - return !!l && - this.start.x === l.start.x && - this.start.y === l.start.y && - this.end.x === l.end.x && - this.end.y === l.end.y; - }, + while (j--) { + t = tvalues[j]; + mt = 1 - t; - // @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) - ); + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; - } else if (l instanceof Rect) { - // Passed in parameter is a rectangle. + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; - var r = l; - var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; - var points = []; - var dedupeArr = []; - var pt, i; + points[j] = { X: x, Y: y }; + } - 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()); - } - } + tvalues[jlen] = 0; + tvalues[jlen + 1] = 1; - return points.length > 0 ? points : null; - } + points[jlen] = { X: x0, Y: y0 }; + points[jlen + 1] = { X: x3, Y: y3 }; - // Passed in parameter is neither a Line nor a Rectangle. - return null; - }, + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; - // @return {double} length of the line - length: function() { - return sqrt(this.squaredLength()); - }, + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; - // @return {point} my midpoint - midpoint: function() { - return Point( - (this.start.x + this.end.x) / 2, - (this.start.y + this.end.y) / 2 - ); - }, + tvalues.length = jlen + 2; + bounds[0].length = jlen + 2; + bounds[1].length = jlen + 2; + points.length = jlen + 2; - // @return {point} my point at 't' <0,1> - pointAt: function(t) { + var left = min.apply(null, bounds[0]); + var top = min.apply(null, bounds[1]); + var right = max.apply(null, bounds[0]); + var bottom = max.apply(null, bounds[1]); - 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 new Rect(left, top, (right - left), (bottom - top)); }, - // @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) { + clone: function() { - // 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 new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); }, - // @return vector {point} of the line - vector: function() { + // Returns the point on the curve closest to point `p` + closestPoint: function(p, opt) { - return Point(this.end.x - this.start.x, this.end.y - this.start.y); + return this.pointAtT(this.closestPointT(p, opt)); }, - // @return {point} the closest point on the line to point `p` - closestPoint: function(p) { + closestPointLength: function(p, opt) { - return this.pointAt(this.closestPointNormalizedLength(p)); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + return this.lengthAtT(this.closestPointT(p, localOpt), localOpt); }, - // @return {number} the normalized length of the closest point on the line to point `p` - closestPointNormalizedLength: function(p) { + closestPointNormalizedLength: function(p, opt) { - var product = this.vector().dot(Line(this.start, p).vector()); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - return Math.min(1, Math.max(0, product / this.squaredLength())); - }, + var cpLength = this.closestPointLength(p, localOpt); + if (!cpLength) return 0; - // @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; + var length = this.length(localOpt); + if (length === 0) return 0; + + return cpLength / length; }, - toString: function() { - return this.start.toString() + ' ' + this.end.toString(); - } - }; + // Returns `t` of the point on the curve closest to point `p` + closestPointT: function(p, opt) { - // For backwards compatibility: - g.Line.prototype.intersection = g.Line.prototype.intersect; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // does not use localOpt - /* - Point is the most basic object consisting of x/y coordinate. + // identify the subdivision that contains the point: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + var distFromStart; // distance of point from start of baseline + var distFromEnd; // distance of point from end of baseline + var minSumDist; // lowest observed sum of the two distances + var n = subdivisions.length; + var subdivisionSize = (n ? (1 / n) : 0); + for (var i = 0; i < n; i++) { - Possible instantiations are: - * `Point(10, 20)` - * `new Point(10, 20)` - * `Point('10 20')` - * `Point(Point(10, 20))` - */ - var Point = g.Point = function(x, y) { + var currentSubdivision = subdivisions[i]; - if (!(this instanceof Point)) { - return new Point(x, y); - } + var startDist = currentSubdivision.start.distance(p); + var endDist = currentSubdivision.end.distance(p); + var sumDist = startDist + endDist; - 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; - } + // check that the point is closest to current subdivision and not any other + if (!minSumDist || (sumDist < minSumDist)) { + investigatedSubdivision = currentSubdivision; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - }; + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; - // Alternative constructor, from polar coordinates. - // @param {number} Distance. - // @param {number} Angle in radians. - // @param {point} [optional] Origin. - g.Point.fromPolar = function(distance, angle, origin) { + distFromStart = startDist; + distFromEnd = endDist; - origin = (origin && Point(origin)) || Point(0, 0); - var x = abs(distance * cos(angle)); - var y = abs(distance * sin(angle)); - var deg = normalizeAngle(toDeg(angle)); + minSumDist = sumDist; + } + } - if (deg < 90) { - y = -y; - } else if (deg < 180) { - x = -x; - y = -y; - } else if (deg < 270) { - x = -x; - } + var precisionRatio = pow(10, -precision); - return Point(origin.x + x, origin.y + y); - }; + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. - g.Point.random = function(x1, x2, y1, y2) { + // check if we have reached required observed precision + var startPrecisionRatio; + var endPrecisionRatio; - return Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); - }; + startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0); + endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0); + if ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio) < precisionRatio) { + return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT); + } - g.Point.prototype = { + // otherwise, set up for next iteration + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - // 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) { + var startDist1 = divided[0].start.distance(p); + var endDist1 = divided[0].end.distance(p); + var sumDist1 = startDist1 + endDist1; - if (r.containsPoint(this)) { - return this; - } + var startDist2 = divided[1].start.distance(p); + var endDist2 = divided[1].end.distance(p); + var sumDist2 = startDist2 + endDist2; - 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; - }, + if (sumDist1 <= sumDist2) { + investigatedSubdivision = divided[0]; - // Return the bearing between me and the given point. - bearing: function(point) { + investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved - return Line(this, point).bearing(); - }, + distFromStart = startDist1; + distFromEnd = endDist1; - // Returns change in angle from my previous position (-dx, -dy) to my new position - // relative to ref point. - changeInAngle: function(dx, dy, ref) { + } else { + investigatedSubdivision = divided[1]; - // Revert the translation and measure the change in angle around x-axis. - return Point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved + + distFromStart = startDist2; + distFromEnd = endDist2; + } + } }, - clone: function() { + closestPointTangent: function(p, opt) { - return Point(this); + return this.tangentAtT(this.closestPointT(p, opt)); }, - difference: function(dx, dy) { + // Divides the curve into two at point defined by `t` between 0 and 1. + // Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867). + // Additional resource: https://pomax.github.io/bezierinfo/#decasteljau + divide: function(t) { + + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return [ + new Curve(start, start, start, start), + new Curve(start, controlPoint1, controlPoint2, end) + ]; + } - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; + if (t >= 1) { + return [ + new Curve(start, controlPoint1, controlPoint2, end), + new Curve(end, end, end, end) + ]; } - return Point(this.x - (dx || 0), this.y - (dy || 0)); - }, + var dividerPoints = this.getSkeletonPoints(t); - // Returns distance between me and point `p`. - distance: function(p) { + var startControl1 = dividerPoints.startControlPoint1; + var startControl2 = dividerPoints.startControlPoint2; + var divider = dividerPoints.divider; + var dividerControl1 = dividerPoints.dividerControlPoint1; + var dividerControl2 = dividerPoints.dividerControlPoint2; - return Line(this, p).length(); + // return array with two new curves + return [ + new Curve(start, startControl1, startControl2, divider), + new Curve(divider, dividerControl1, dividerControl2, end) + ]; }, - squaredDistance: function(p) { + // Returns the distance between the curve's start and end points. + endpointDistance: function() { - return Line(this, p).squaredLength(); + return this.start.distance(this.end); }, - equals: function(p) { - - return !!p && this.x === p.x && this.y === p.y; + // Checks whether two curves are exactly the same. + equals: function(c) { + + return !!c && + this.start.x === c.start.x && + this.start.y === c.start.y && + this.controlPoint1.x === c.controlPoint1.x && + this.controlPoint1.y === c.controlPoint1.y && + this.controlPoint2.x === c.controlPoint2.x && + this.controlPoint2.y === c.controlPoint2.y && + this.end.x === c.end.x && + this.end.y === c.end.y; }, - magnitude: function() { + // Returns five helper points necessary for curve division. + getSkeletonPoints: function(t) { + + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return { + startControlPoint1: start.clone(), + startControlPoint2: start.clone(), + divider: start.clone(), + dividerControlPoint1: control1.clone(), + dividerControlPoint2: control2.clone() + }; + } - return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; - }, + if (t >= 1) { + return { + startControlPoint1: control1.clone(), + startControlPoint2: control2.clone(), + divider: end.clone(), + dividerControlPoint1: end.clone(), + dividerControlPoint2: end.clone() + }; + } - // Returns a manhattan (taxi-cab) distance between me and point `p`. - manhattanDistance: function(p) { + var midpoint1 = (new Line(start, control1)).pointAt(t); + var midpoint2 = (new Line(control1, control2)).pointAt(t); + var midpoint3 = (new Line(control2, end)).pointAt(t); - return abs(p.x - this.x) + abs(p.y - this.y); + var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t); + var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t); + + var divider = (new Line(subControl1, subControl2)).pointAt(t); + + var output = { + startControlPoint1: midpoint1, + startControlPoint2: subControl1, + divider: divider, + dividerControlPoint1: subControl2, + dividerControlPoint2: midpoint3 + }; + + return output; }, - // Move point on line starting from ref ending at me by - // distance distance. - move: function(ref, distance) { + // Returns a list of curves whose flattened length is better than `opt.precision`. + // That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1% + // (Observed difference is not real precision, but close enough as long as special cases are covered) + // (That is why skipping iteration 1 is important) + // As a rule of thumb, increasing `precision` by 1 requires two more division operations + // - Precision 0 (endpointDistance) - total of 2^0 - 1 = 0 operations (1 subdivision) + // - Precision 1 (<10% error) - total of 2^2 - 1 = 3 operations (4 subdivisions) + // - Precision 2 (<1% error) - total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions) + // - Precision 3 (<0.1% error) - total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions) + // - Precision 4 (<0.01% error) - total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions) + // (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly) + getSubdivisions: function(opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt + + var subdivisions = [new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end)]; + if (precision === 0) return subdivisions; + + var previousLength = this.endpointDistance(); + + var precisionRatio = pow(10, -precision); + + // recursively divide curve at `t = 0.5` + // until the difference between observed length at subsequent iterations is lower than precision + var iteration = 0; + while (true) { + iteration += 1; + + // divide all subdivisions + var newSubdivisions = []; + var numSubdivisions = subdivisions.length; + for (var i = 0; i < numSubdivisions; i++) { + + var currentSubdivision = subdivisions[i]; + var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!) + newSubdivisions.push(divided[0], divided[1]); + } + + // measure new length + var length = 0; + var numNewSubdivisions = newSubdivisions.length; + for (var j = 0; j < numNewSubdivisions; j++) { + + var currentNewSubdivision = newSubdivisions[j]; + length += currentNewSubdivision.endpointDistance(); + } - var theta = toRad(Point(ref).theta(this)); - return this.offset(cos(theta) * distance, -sin(theta) * distance); + // check if we have reached required observed precision + // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1 + // not a problem for further iterations because cubic curves cannot have more than two local extrema + // (i.e. cubic curves cannot intersect the baseline more than once) + // therefore two subsequent iterations cannot produce sampling with equal length + var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0); + if (iteration > 1 && observedPrecisionRatio < precisionRatio) { + return newSubdivisions; + } + + // otherwise, set up for next iteration + subdivisions = newSubdivisions; + previousLength = length; + } }, - // 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) { + isDifferentiable: function() { - var scale = (length || 1) / this.magnitude(); - return this.scale(scale, scale); + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); }, - // Offset me by the specified amount. - offset: function(dx, dy) { + // Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided. + length: function(opt) { - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt + + var length = 0; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + length += currentSubdivision.endpointDistance(); } - this.x += dx || 0; - this.y += dy || 0; - return this; + return length; }, - // Returns a point that is the reflection of me with - // the center of inversion in ref point. - reflection: function(ref) { + // Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.) + lengthAtT: function(t, opt) { + + if (t <= 0) return 0; - return Point(ref).move(this, this.distance(ref)); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt + + var subCurve = this.divide(t)[0]; + var subCurveLength = subCurve.length({ precision: precision }); + + return subCurveLength; }, - // Rotate point by angle around origin. - rotate: function(origin, angle) { + // Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + // Mirrors Line.pointAt() function. + // For a function that tracks `t`, use Curve.pointAtT(). + pointAt: function(ratio, opt) { - 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; + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + var t = this.tAt(ratio, opt); + + return this.pointAtT(t); }, - round: function(precision) { + // Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + pointAtLength: function(length, opt) { - var f = pow(10, precision || 0); - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - return this; + var t = this.tAtLength(length, opt); + + return this.pointAtT(t); }, - // Scale point with origin. - scale: function(sx, sy, origin) { + // Returns the point at provided `t` between 0 and 1. + // `t` does not track distance along curve as it does in Line objects. + // Non-linear relationship, speeds up and slows down as curve warps! + // For linear length-based solution, use Curve.pointAt(). + pointAtT: function(t) { - 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; + if (t <= 0) return this.start.clone(); + if (t >= 1) return this.end.clone(); + + return this.getSkeletonPoints(t).divider; }, - snapToGrid: function(gx, gy) { + // Default precision + PRECISION: 3, - this.x = snapToGrid(this.x, gx); - this.y = snapToGrid(this.y, gy || gx); + scale: function(sx, sy, origin) { + + this.start.scale(sx, sy, origin); + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); 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) { + // Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAt: function(ratio, opt) { - p = Point(p); - // Invert the y-axis. - var y = -(p.y - this.y); - var x = p.x - this.x; - var rad = atan2(y, x); // defined for all 0 corner cases - - // Correction for III. and IV. quadrant. - if (rad < 0) { - rad = 2 * PI + rad; - } - return 180 * rad / PI; - }, + if (!this.isDifferentiable()) return null; - // Compute the angle between vector from me to p1 and the vector from me to p2. - // ordering of points p1 and p2 is important! - // theta function's angle convention: - // returns angles between 0 and 180 when the angle is counterclockwise - // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones - // returns NaN if any of the points p1, p2 is coincident with this point - angleBetween: function(p1, p2) { - - var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - if (angleBetween < 0) { - angleBetween += 360; // correction to keep angleBetween between 0 and 360 - } - return angleBetween; - }, + if (ratio < 0) ratio = 0; + else if (ratio > 1) ratio = 1; - // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. - // Returns NaN if p is at 0,0. - vectorAngle: function(p) { - - var zero = Point(0,0); - return zero.angleBetween(this, p); + var t = this.tAt(ratio, opt); + + return this.tangentAtT(t); }, - toJSON: function() { + // Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAtLength: function(length, opt) { - return { x: this.x, y: this.y }; - }, + if (!this.isDifferentiable()) return null; - // Converts rectangular to polar coordinates. - // An origin can be specified, otherwise it's 0@0. - toPolar: function(o) { + var t = this.tAtLength(length, opt); - 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; + return this.tangentAtT(t); }, - toString: function() { + // Returns a tangent line at requested `t`. + tangentAtT: function(t) { - return this.x + '@' + this.y; - }, + if (!this.isDifferentiable()) return null; - update: function(x, y) { + if (t < 0) t = 0; + else if (t > 1) t = 1; - this.x = x || 0; - this.y = y || 0; - return this; - }, + var skeletonPoints = this.getSkeletonPoints(t); - // Returns the dot product of this point with given other point - dot: function(p) { + var p1 = skeletonPoints.startControlPoint2; + var p2 = skeletonPoints.dividerControlPoint1; - return p ? (this.x * p.x + this.y * p.y) : NaN; + var tangentStart = skeletonPoints.divider; + + var tangentLine = new Line(p1, p2); + tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested + + return tangentLine; }, - // Returns the cross product of this point relative to two other points - // this point is the common point - // point p1 lies on the first vector, point p2 lies on the second vector - // watch out for the ordering of points p1 and p2! - // positive result indicates a clockwise ("right") turn from first to second vector - // negative result indicates a counterclockwise ("left") turn from first to second vector - // note that the above directions are reversed from the usual answer on the Internet - // that is because we are in a left-handed coord system (because the y-axis points downward) - cross: function(p1, p2) { + // Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + tAt: function(ratio, opt) { - return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; - } - }; + if (ratio <= 0) return 0; + if (ratio >= 1) return 1; - var Rect = g.Rect = function(x, y, w, h) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - if (!(this instanceof Rect)) { - return new Rect(x, y, w, h); - } + var curveLength = this.length(localOpt); + var length = curveLength * ratio; - if ((Object(x) === x)) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } + return this.tAtLength(length, localOpt); + }, - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - this.width = w === undefined ? 0 : w; - this.height = h === undefined ? 0 : h; - }; + // Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + // Uses `precision` to approximate length within `precision` (always underestimates) + // Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated + // As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper + // - Precision 0 (chooses one of the two endpoints) - 0 levels + // - Precision 1 (chooses one of 5 points, <10% error) - 1 level + // - Precision 2 (<1% error) - 3 levels + // - Precision 3 (<0.1% error) - 7 levels + // - Precision 4 (<0.01% error) - 15 levels + tAtLength: function(length, opt) { + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - g.Rect.fromEllipse = function(e) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - e = Ellipse(e); - return Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); - }; + // identify the subdivision that contains the point at requested `length`: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + //var baseline; // straightened version of subdivision to investigate + //var baselinePoint; // point on the baseline that is the requested distance away from start + var baselinePointDistFromStart; // distance of baselinePoint from start of baseline + var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline + var l = 0; // length so far + var n = subdivisions.length; + var subdivisionSize = 1 / n; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - g.Rect.prototype = { + var currentSubdivision = subdivisions[i]; + var d = currentSubdivision.endpointDistance(); // length of current subdivision - // 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) { + if (length <= (l + d)) { + investigatedSubdivision = currentSubdivision; - 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); - }, + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; - bottomLeft: function() { + baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length)); + baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l)); - return Point(this.x, this.y + this.height); - }, + break; + } - bottomLine: function() { + l += d; + } - return Line(this.bottomLeft(), this.corner()); - }, + if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t + // note that precision affects what length is recorded + // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length) + // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1 - bottomMiddle: function() { + var curveLength = this.length(localOpt); - return Point(this.x + this.width / 2, this.y + this.height); - }, + var precisionRatio = pow(10, -precision); - center: function() { + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(this.x + this.width / 2, this.y + this.height / 2); - }, + // check if we have reached required observed precision + var observedPrecisionRatio; - clone: function() { + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT; + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT; - return Rect(this); - }, + // otherwise, set up for next iteration + var newBaselinePointDistFromStart; + var newBaselinePointDistFromEnd; - // @return {bool} true if point p is insight me - containsPoint: function(p) { + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - p = Point(p); - return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; - }, + var baseline1Length = divided[0].endpointDistance(); + var baseline2Length = divided[1].endpointDistance(); - // @return {bool} true if rectangle `r` is inside me. - containsRect: function(r) { + if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0] + investigatedSubdivision = divided[0]; - 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; + investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved - if (!w0 || !h0 || !w1 || !h1) { - // At least one of the dimensions is 0 - return false; - } + newBaselinePointDistFromStart = baselinePointDistFromStart; + newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart; - var x0 = r0.x; - var y0 = r0.y; - var x1 = r1.x; - var y1 = r1.y; + } else { // point at requested length is inside divided[1] + investigatedSubdivision = divided[1]; - w1 += x1; - w0 += x0; - h1 += y1; - h0 += y0; + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; + newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length; + newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart; + } + + baselinePointDistFromStart = newBaselinePointDistFromStart; + baselinePointDistFromEnd = newBaselinePointDistFromEnd; + } }, - corner: function() { + translate: function(tx, ty) { - return Point(this.x + this.width, this.y + this.height); + this.start.translate(tx, ty); + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; }, - // @return {boolean} true if rectangles are equal. - equals: function(r) { + // Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPoints: function(opt) { - 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; - }, + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt - // @return {rect} if rectangles intersect, {null} if not. - intersect: function(r) { + var points = [subdivisions[0].start.clone()]; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = r.origin(); - var rCorner = r.corner(); + var currentSubdivision = subdivisions[i]; + points.push(currentSubdivision.end.clone()); + } - // No intersection found - if (rCorner.x <= myOrigin.x || - rCorner.y <= myOrigin.y || - rOrigin.x >= myCorner.x || - rOrigin.y >= myCorner.y) return null; + return points; + }, - var x = Math.max(myOrigin.x, rOrigin.x); - var y = Math.max(myOrigin.y, rOrigin.y); + // Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPolyline: function(opt) { - return Rect(x, y, Math.min(myCorner.x, rCorner.x) - x, Math.min(myCorner.y, rCorner.y) - y); + return new Polyline(this.toPoints(opt)); }, - // 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) { + toString: function() { - p = Point(p); - var center = Point(this.x + this.width / 2, this.y + this.height / 2); - var result; - if (angle) p.rotate(center, angle); + return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; - // (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); + var Ellipse = g.Ellipse = function(c, a, b) { - 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; - }, + if (!(this instanceof Ellipse)) { + return new Ellipse(c, a, b); + } - leftLine: function() { + if (c instanceof Ellipse) { + return new Ellipse(new Point(c.x, c.y), c.a, c.b); + } - return Line(this.origin(), this.bottomLeft()); - }, + c = new Point(c); + this.x = c.x; + this.y = c.y; + this.a = a; + this.b = b; + }; - leftMiddle: function() { + Ellipse.fromRect = function(rect) { + + rect = new Rect(rect); + return new Ellipse(rect.center(), rect.width / 2, rect.height / 2); + }; + + Ellipse.prototype = { - return Point(this.x , this.y + this.height / 2); + bbox: function() { + + return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); }, - // Move and expand me. - // @param r {rectangle} representing deltas - moveAndExpand: function(r) { + clone: function() { - this.x += r.x || 0; - this.y += r.y || 0; - this.width += r.width || 0; - this.height += r.height || 0; - return this; + return new Ellipse(this); }, - // Offset me by the specified amount. - offset: function(dx, dy) { - return Point.prototype.offset.call(this, dx, dy); + /** + * @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, recompute origin [x, y] + // 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 @@ -1033,538 +1144,3734 @@ var g = (function() { dy = dx; } - this.x -= dx; - this.y -= dy; - this.width += 2 * dx; - this.height += 2 * dy; + this.a += 2 * dx; + this.b += 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; + /** + * @param {g.Point} p + * @returns {boolean} + */ + containsPoint: function(p) { + + return this.normalizedDistance(p) <= 1; }, - origin: function() { + /** + * @returns {g.Point} + */ + center: function() { - return Point(this.x, this.y); + return new 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) { + /** 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) { - 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); - }, + 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; - rightLine: function() { + var q1 = x0 > center.x + a / 2; + var q3 = x0 < center.x - a / 2; - return Line(this.topRight(), this.corner()); - }, + 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; - rightMiddle: function() { + } 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 (new Point(x, y)).theta(p); - return Point(this.x + this.width, this.y + this.height / 2); }, - round: function(precision) { + equals: function(ellipse) { - 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; + return !!ellipse && + ellipse.x === this.x && + ellipse.y === this.y && + ellipse.a === this.a && + ellipse.b === this.b; }, - // Scale rectangle with origin. - scale: function(sx, sy, origin) { + intersectionWithLine: function(line) { + + var intersections = []; + var a1 = line.start; + var a2 = line.end; + var rx = this.a; + var ry = this.b; + var dir = line.vector(); + var diff = a1.difference(new Point(this)); + var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)); + var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)); + + var a = dir.dot(mDir); + var b = dir.dot(mDiff); + var c = diff.dot(mDiff) - 1.0; + var d = b * b - a * c; + + if (d < 0) { + return null; + } else if (d > 0) { + var root = sqrt(d); + var ta = (-b - root) / a; + var tb = (-b + root) / a; + + if ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) { + // if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside + return null; + } else { + if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta)); + if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb)); + } + } else { + var t = -b / a; + if (0 <= t && t <= 1) { + intersections.push(a1.lerp(a2, t)); + } else { + // outside + return null; + } + } - origin = this.origin().scale(sx, sy, origin); - this.x = origin.x; - this.y = origin.y; - this.width *= sx; - this.height *= sy; - return this; + return intersections; }, - maxRectScaleToFit: function(rect, origin) { - - rect = g.Rect(rect); - origin || (origin = rect.center()); + // 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) { - var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; - var ox = origin.x; - var oy = origin.y; + p = new Point(p); - // 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. + if (angle) p.rotate(new Point(this.x, this.y), angle); - sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; + var dx = p.x - this.x; + var dy = p.y - this.y; + var result; - // 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); + if (dx === 0) { + result = this.bbox().pointNearestToPoint(p); + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; } - return { - sx: Math.min(sx1, sx2, sx3, sx4), - sy: Math.min(sy1, sy2, sy3, sy4) - }; - }, + var m = dy / dx; + var mSquared = m * m; + var aSquared = this.a * this.a; + var bSquared = this.b * this.b; - maxRectUniformScaleToFit: function(rect, origin) { + var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared))); + x = dx < 0 ? -x : x; - var scale = this.maxRectScaleToFit(rect, origin); - return Math.min(scale.sx, scale.sy); + var y = m * x; + result = new Point(this.x + x, this.y + y); + + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; }, - // @return {string} (left|right|top|bottom) side which is nearest to point - // @see Squeak Smalltalk, Rectangle>>sideNearestTo: - sideNearestToPoint: function(point) { + toString: function() { - 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'; + return (new Point(this.x, this.y)).toString() + ' ' + this.a + ' ' + this.b; + } + }; - if (distToRight < closest) { - closest = distToRight; - side = 'right'; - } - if (distToTop < closest) { - closest = distToTop; - side = 'top'; - } - if (distToBottom < closest) { - closest = distToBottom; - side = 'bottom'; - } - return side; + var Line = g.Line = function(p1, p2) { + + if (!(this instanceof Line)) { + return new Line(p1, p2); + } + + if (p1 instanceof Line) { + return new Line(p1.start, p1.end); + } + + this.start = new Point(p1); + this.end = new Point(p2); + }; + + Line.prototype = { + + bbox: function() { + + var left = min(this.start.x, this.end.x); + var top = min(this.start.y, this.end.y); + var right = max(this.start.x, this.end.x); + var bottom = max(this.start.y, this.end.y); + + return new Rect(left, top, (right - left), (bottom - top)); }, - snapToGrid: function(gx, gy) { + // @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 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; + 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]; }, - topLine: function() { + clone: function() { - return Line(this.origin(), this.topRight()); + return new Line(this.start, this.end); }, - topMiddle: function() { + // @return {point} the closest point on the line to point `p` + closestPoint: function(p) { - return Point(this.x + this.width / 2, this.y); + return this.pointAt(this.closestPointNormalizedLength(p)); }, - topRight: function() { + closestPointLength: function(p) { - return Point(this.x + this.width, this.y); + return this.closestPointNormalizedLength(p) * this.length(); }, - toJSON: function() { + // @return {number} the normalized length of the closest point on the line to point `p` + closestPointNormalizedLength: function(p) { - return { x: this.x, y: this.y, width: this.width, height: this.height }; + var product = this.vector().dot((new Line(this.start, p)).vector()); + var cpNormalizedLength = min(1, max(0, product / this.squaredLength())); + + // cpNormalizedLength returns `NaN` if this line has zero length + // we can work with that - if `NaN`, return 0 + if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN` + // (`NaN` is the only value that is not equal to itself) + + return cpNormalizedLength; }, - toString: function() { + closestPointTangent: function(p) { - return this.origin().toString() + ' ' + this.corner().toString(); + return this.tangentAt(this.closestPointNormalizedLength(p)); }, - // @return {rect} representing the union of both rectangles. - union: function(rect) { + equals: function(l) { - rect = Rect(rect); - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = rect.origin(); - var rCorner = rect.corner(); + return !!l && + this.start.x === l.start.x && + this.start.y === l.start.y && + this.end.x === l.end.x && + this.end.y === l.end.y; + }, - 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); + intersectionWithLine: function(line) { - return Rect(originX, originY, cornerX - originX, cornerY - originY); - } - }; + var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y); + var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y); + var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); + var deltaPt = new Point(line.start.x - this.start.x, line.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); - var Polyline = g.Polyline = function(points) { + if (det === 0 || alpha * det < 0 || beta * det < 0) { + // No intersection found. + return null; + } - if (!(this instanceof Polyline)) { - return new Polyline(points); - } + if (det > 0) { + if (alpha > det || beta > det) { + return null; + } - this.points = (Array.isArray(points)) ? points.map(Point) : []; - }; + } else { + if (alpha < det || beta < det) { + return null; + } + } - Polyline.prototype = { + return [new Point( + this.start.x + (alpha * pt1Dir.x / det), + this.start.y + (alpha * pt1Dir.y / det) + )]; + }, - pointAtLength: function(length) { - var points = this.points; - var l = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var a = points[i]; - var b = points[i+1]; - var d = a.distance(b); - l += d; - if (length <= l) { - return Line(b, a).pointAt(d ? (l - length) / d : 0); + // @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(shape, opt) { + + if (shape instanceof Line || + shape instanceof Rect || + shape instanceof Polyline || + shape instanceof Ellipse || + shape instanceof Path + ) { + var intersection = shape.intersectionWithLine(this, opt); + + // Backwards compatibility + if (intersection && (shape instanceof Line)) { + intersection = intersection[0]; } + + return intersection; } + return null; }, - length: function() { - var points = this.points; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - length += points[i].distance(points[i+1]); - } - return length; + isDifferentiable: function() { + + return !this.start.equals(this.end); }, - closestPoint: function(p) { - return this.pointAtLength(this.closestPointLength(p)); + // @return {double} length of the line + length: function() { + + return sqrt(this.squaredLength()); }, - closestPointLength: function(p) { - var points = this.points; - var pointLength; - var minSqrDistance = Infinity; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var line = Line(points[i], points[i+1]); - var lineLength = line.length(); - var cpNormalizedLength = line.closestPointNormalizedLength(p); - var cp = line.pointAt(cpNormalizedLength); - var sqrDistance = cp.squaredDistance(p); - if (sqrDistance < minSqrDistance) { - minSqrDistance = sqrDistance; - pointLength = length + cpNormalizedLength * lineLength; - } - length += lineLength; - } - return pointLength; + // @return {point} my midpoint + midpoint: function() { + + return new Point( + (this.start.x + this.end.x) / 2, + (this.start.y + this.end.y) / 2 + ); }, - toString: function() { + // @return {point} my point at 't' <0,1> + pointAt: function(t) { - return this.points + ''; + var start = this.start; + var end = this.end; + + if (t <= 0) return start.clone(); + if (t >= 1) return end.clone(); + + return start.lerp(end, t); }, - // Returns a convex-hull polyline from this polyline. - // this function implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan) - // output polyline starts at the first element of the original polyline that is on the hull - // output polyline then continues clockwise from that point - convexHull: function() { + pointAtLength: function(length) { - var i; - var n; + var start = this.start; + var end = this.end; - var points = this.points; + fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // step 1: find the starting point - point with the lowest y (if equality, highest x) - var startPoint; - n = points.length; - for (i = 0; i < n; i++) { - if (startPoint === undefined) { - // if this is the first point we see, set it as start point - startPoint = points[i]; - } else if (points[i].y < startPoint.y) { - // start point should have lowest y from all points - startPoint = points[i]; - } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { - // if two points have the lowest y, choose the one that has highest x - // there are no points to the right of startPoint - no ambiguity about theta 0 - // if there are several coincident start point candidates, first one is reported - startPoint = points[i]; - } - } + var lineLength = this.length(); + if (length >= lineLength) return (fromStart ? end.clone() : start.clone()); - // step 2: sort the list of points - // sorting by angle between line from startPoint to point and the x-axis (theta) - - // step 2a: create the point records = [point, originalIndex, angle] - var sortedPointRecords = []; - n = points.length; - for (i = 0; i < n; i++) { - var angle = startPoint.theta(points[i]); - if (angle === 0) { - angle = 360; // give highest angle to start point - // the start point will end up at end of sorted list - // the start point will end up at beginning of hull points list - } - - var entry = [points[i], i, angle]; - sortedPointRecords.push(entry); - } + return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength); + }, - // step 2b: sort the list in place - sortedPointRecords.sort(function(record1, record2) { - // returning a negative number here sorts record1 before record2 - // if first angle is smaller than second, first angle should come before second - var sortOutput = record1[2] - record2[2]; // negative if first angle smaller - if (sortOutput === 0) { - // if the two angles are equal, sort by originalIndex - sortOutput = record2[1] - record1[1]; // negative if first index larger - // coincident points will be sorted in reverse-numerical order - // so the coincident points with lower original index will be considered first - } - return sortOutput; - }); + // @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) { - // step 2c: duplicate start record from the top of the stack to the bottom of the stack - if (sortedPointRecords.length > 2) { - var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; - sortedPointRecords.unshift(startPointRecord); - } + // Find the sign of the determinant of vectors (start,end), where p is the query point. + p = new g.Point(p); + var start = this.start; + var end = this.end; + var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x)); - // step 3a: go through sorted points in order and find those with right turns - // we want to get our results in clockwise order - var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull - var hullPointRecords = []; // stack of records with right turns - hull point candidates + return determinant / this.length(); + }, - var currentPointRecord; - var currentPoint; - var lastHullPointRecord; - var lastHullPoint; - var secondLastHullPointRecord; - var secondLastHullPoint; - while (sortedPointRecords.length !== 0) { - currentPointRecord = sortedPointRecords.pop(); - currentPoint = currentPointRecord[0]; + rotate: function(origin, angle) { - // check if point has already been discarded - // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' - if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { - // this point had an incorrect turn at some previous iteration of this loop - // this disqualifies it from possibly being on the hull - continue; + this.start.rotate(origin, angle); + this.end.rotate(origin, angle); + return this; + }, + + round: function(precision) { + + var f = pow(10, precision || 0); + this.start.x = round(this.start.x * f) / f; + this.start.y = round(this.start.y * f) / f; + this.end.x = round(this.end.x * f) / f; + this.end.y = round(this.end.y * f) / f; + return this; + }, + + scale: function(sx, sy, origin) { + + this.start.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, + + // @return {number} scale the line so that it has the requested length + setLength: function(length) { + + var currentLength = this.length(); + if (!currentLength) return this; + + var scaleFactor = length / currentLength; + return this.scale(scaleFactor, scaleFactor, this.start); + }, + + // @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; + }, + + tangentAt: function(t) { + + if (!this.isDifferentiable()) return null; + + var start = this.start; + var end = this.end; + + var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1 + + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested + + return tangentLine; + }, + + tangentAtLength: function(length) { + + if (!this.isDifferentiable()) return null; + + var start = this.start; + var end = this.end; + + var tangentStart = this.pointAtLength(length); + + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested + + return tangentLine; + }, + + translate: function(tx, ty) { + + this.start.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, + + // @return vector {point} of the line + vector: function() { + + return new Point(this.end.x - this.start.x, this.end.y - this.start.y); + }, + + toString: function() { + + return this.start.toString() + ' ' + this.end.toString(); + } + }; + + // For backwards compatibility: + Line.prototype.intersection = Line.prototype.intersect; + + // Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. + // Path created is not guaranteed to be a valid (serializable) path (might not start with an M). + var Path = g.Path = function(arg) { + + if (!(this instanceof Path)) { + return new Path(arg); + } + + if (typeof arg === 'string') { // create from a path data string + return new Path.parse(arg); + } + + this.segments = []; + + var i; + var n; + + if (!arg) { + // don't do anything + + } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array + n = arg.length; + if (arg[0].isSegment) { // create from an array of segments + for (i = 0; i < n; i++) { + + var segment = arg[i]; + + this.appendSegment(segment); } - var correctTurnFound = false; - while (!correctTurnFound) { - if (hullPointRecords.length < 2) { - // not enough points for comparison, just add current point - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; - - } else { - lastHullPointRecord = hullPointRecords.pop(); - lastHullPoint = lastHullPointRecord[0]; - secondLastHullPointRecord = hullPointRecords.pop(); - secondLastHullPoint = secondLastHullPointRecord[0]; + } else { // create from an array of Curves and/or Lines + var previousObj = null; + for (i = 0; i < n; i++) { - var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); + var obj = arg[i]; - if (crossProduct < 0) { - // found a right turn - hullPointRecords.push(secondLastHullPointRecord); - hullPointRecords.push(lastHullPointRecord); - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; + if (!((obj instanceof Line) || (obj instanceof Curve))) { + throw new Error('Cannot construct a path segment from the provided object.'); + } - } else if (crossProduct === 0) { - // the three points are collinear - // three options: - // there may be a 180 or 0 degree angle at lastHullPoint - // or two of the three points are coincident - var THRESHOLD = 1e-10; // we have to take rounding errors into account - var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); - if (Math.abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 - // if the cross product is 0 because the angle is 180 degrees - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { - // if the cross product is 0 because two points are the same - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (Math.abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 - // if the cross product is 0 because the angle is 0 degrees - // remove last hull point from hull BUT do not discard it - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // put last hull point back into the sorted point records list - sortedPointRecords.push(lastHullPointRecord); - // we are switching the order of the 0deg and 180deg points - // correct turn not found - } + if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); - } else { - // found a left turn - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter of loop) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - } + // if objects do not link up, moveto segments are inserted to cover the gaps + if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); + + if (obj instanceof Line) { + this.appendSegment(Path.createSegment('L', obj.end)); + + } else if (obj instanceof Curve) { + this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); } + + previousObj = obj; } } - // at this point, hullPointRecords contains the output points in clockwise order - // the points start with lowest-y,highest-x startPoint, and end at the same point - // step 3b: remove duplicated startPointRecord from the end of the array - if (hullPointRecords.length > 2) { - hullPointRecords.pop(); - } + } else if (arg.isSegment) { // create from a single segment + this.appendSegment(arg); - // step 4: find the lowest originalIndex record and put it at the beginning of hull - var lowestHullIndex; // the lowest originalIndex on the hull - var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex - n = hullPointRecords.length; + } else if (arg instanceof Line) { // create from a single Line + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('L', arg.end)); + + } else if (arg instanceof Curve) { // create from a single Curve + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); + + } else if (arg instanceof Polyline && arg.points && arg.points.length !== 0) { // create from a Polyline + n = arg.points.length; for (i = 0; i < n; i++) { - var currentHullIndex = hullPointRecords[i][1]; - if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { - lowestHullIndex = currentHullIndex; - indexOfLowestHullIndexRecord = i; - } - } + var point = arg.points[i]; - var hullPointRecordsReordered = []; - if (indexOfLowestHullIndexRecord > 0) { - var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); - var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); - hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - } else { - hullPointRecordsReordered = hullPointRecords; + if (i === 0) this.appendSegment(Path.createSegment('M', point)); + else this.appendSegment(Path.createSegment('L', point)); } + } + }; - var hullPoints = []; - n = hullPointRecordsReordered.length; - for (i = 0; i < n; i++) { - hullPoints.push(hullPointRecordsReordered[i][0]); - } + // More permissive than V.normalizePathData and Path.prototype.serialize. + // Allows path data strings that do not start with a Moveto command (unlike SVG specification). + // Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). + // Allows for command argument chaining. + // Throws an error if wrong number of arguments is provided with a command. + // Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). + Path.parse = function(pathData) { + + if (!pathData) return new Path(); + + var path = new Path(); + + var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; + var commands = pathData.match(commandRe); - return Polyline(hullPoints); + var numCommands = commands.length; + for (var i = 0; i < numCommands; i++) { + + var command = commands[i]; + var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?))|(?:(?:-?\.\d+))/g; + var args = command.match(argRe); + + var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] + path.appendSegment(segment); } + + return path; }; + // Create a segment or an array of segments. + // Accepts unlimited points/coords arguments after `type`. + Path.createSegment = function(type) { - g.scale = { + if (!type) throw new Error('Type must be provided.'); - // Return the `value` from the `domain` interval scaled to the `range` interval. - linear: function(domain, range, value) { + var segmentConstructor = Path.segmentTypes[type]; + if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); - var domainSpan = domain[1] - domain[0]; - var rangeSpan = range[1] - range[0]; - return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; + var args = []; + var n = arguments.length; + for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array + args.push(arguments[i]); } - }; - var normalizeAngle = g.normalizeAngle = function(angle) { + return applyToNew(segmentConstructor, args); + }, - return (angle % 360) + (angle < 0 ? 360 : 0); - }; + Path.prototype = { - var snapToGrid = g.snapToGrid = function(value, gridSize) { + // Accepts one segment or an array of segments as argument. + // Throws an error if argument is not a segment or an array of segments. + appendSegment: function(arg) { - return gridSize * Math.round(value / gridSize); - }; + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - var toDeg = g.toDeg = function(rad) { + var currentSegment; - return (180 * rad / PI) % 360; - }; + var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null + var nextSegment = null; - var toRad = g.toRad = function(deg, over360) { + if (!Array.isArray(arg)) { // arg is a segment + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - over360 = over360 || false; + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.push(currentSegment); + + } else { // arg is an array of segments + if (!arg[0].isSegment) throw new Error('Segments required.'); + + var n = arg.length; + for (var i = 0; i < n; i++) { + + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.push(currentSegment); + previousSegment = currentSegment; + } + } + }, + + // Returns the bbox of the path. + // If path has no segments, returns null. + // If path has only invisible segments, returns bbox of the end point of last segment. + bbox: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var bbox; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + if (segment.isVisible) { + var segmentBBox = segment.bbox(); + bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; + } + } + + if (bbox) return bbox; + + // if the path has only invisible elements, return end point of last segment + var lastSegment = segments[numSegments - 1]; + return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); + }, + + // Returns a new path that is a clone of this path. + clone: function() { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + var path = new Path(); + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i].clone(); + path.appendSegment(segment); + } + + return path; + }, + + closestPoint: function(p, opt) { + + var t = this.closestPointT(p, opt); + if (!t) return null; + + return this.pointAtT(t); + }, + + closestPointLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var t = this.closestPointT(p, localOpt); + if (!t) return 0; + + return this.lengthAtT(t, localOpt); + }, + + closestPointNormalizedLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var cpLength = this.closestPointLength(p, localOpt); + if (cpLength === 0) return 0; // shortcut + + var length = this.length(localOpt); + if (length === 0) return 0; // prevents division by zero + + return cpLength / length; + }, + + // Private function. + closestPointT: function(p, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var closestPointT; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isVisible) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointT = { segmentIndex: i, value: segmentClosestPointT }; + minSquaredDistance = squaredDistance; + } + } + } + + if (closestPointT) return closestPointT; + + // if no visible segment, return end of last segment + return { segmentIndex: numSegments - 1, value: 1 }; + }, + + closestPointTangent: function(p, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var closestPointTangent; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isDifferentiable()) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointTangent = segment.tangentAtT(segmentClosestPointT); + minSquaredDistance = squaredDistance; + } + } + } + + if (closestPointTangent) return closestPointTangent; + + // if no valid segment, return null + return null; + }, + + // Checks whether two paths are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { + + if (!p) return false; + + var segments = this.segments; + var otherSegments = p.segments; + + var numSegments = segments.length; + if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var otherSegment = otherSegments[i]; + + // as soon as an inequality is found in segments, return false + if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; + } + + // if no inequality found in segments, return true + return true; + }, + + // Accepts negative indices. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + getSegment: function(index) { + + var segments = this.segments; + var numSegments = segments.length; + if (!numSegments === 0) throw new Error('Path has no segments.'); + + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); + + return segments[index]; + }, + + // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. + getSegmentSubdivisions: function(opt) { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.segmentSubdivisions + // not using localOpt + + var segmentSubdivisions = []; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segment.getSubdivisions({ precision: precision }); + segmentSubdivisions.push(subdivisions); + } + + return segmentSubdivisions; + }, + + // Insert `arg` at given `index`. + // `index = 0` means insert at the beginning. + // `index = segments.length` means insert at the end. + // Accepts negative indices, from `-1` to `-(segments.length + 1)`. + // Accepts one segment or an array of segments as argument. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + insertSegment: function(index, arg) { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + // note that these are incremented comapared to getSegments() + // we can insert after last element (note that this changes the meaning of index -1) + if (index < 0) index = numSegments + index + 1; // convert negative indices to positive + if (index > numSegments || index < 0) throw new Error('Index out of range.'); + + var currentSegment; + + var previousSegment = null; + var nextSegment = null; + + if (numSegments !== 0) { + if (index >= 1) { + previousSegment = segments[index - 1]; + nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null + + } else { // if index === 0 + // previousSegment is null + nextSegment = segments[0]; + } + } + + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); + + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 0, currentSegment); + + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); + + var n = arg.length; + for (var i = 0; i < n; i++) { + + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; + } + } + }, + + isDifferentiable: function() { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + // as soon as a differentiable segment is found in segments, return true + if (segment.isDifferentiable()) return true; + } + + // if no differentiable segment is found in segments, return false + return false; + }, + + // Checks whether current path segments are valid. + // Note that d is allowed to be empty - should disable rendering of the path. + isValid: function() { + + var segments = this.segments; + var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto + return isValid; + }, + + // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. + // If path has no segments, returns 0. + length: function(opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var length = 0; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + length += segment.length({ subdivisions: subdivisions }); + } + + return length; + }, + + // Private function. + lengthAtT: function(t, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array + + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return 0; // regardless of t.value + + var tValue = t.value; + if (segmentIndex >= numSegments) { + segmentIndex = numSegments - 1; + tValue = 1; + } + else if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var subdivisions; + var length = 0; + for (var i = 0; i < segmentIndex; i++) { + + var segment = segments[i]; + subdivisions = segmentSubdivisions[i]; + length += segment.length({ precisison: precision, subdivisions: subdivisions }); + } + + segment = segments[segmentIndex]; + subdivisions = segmentSubdivisions[segmentIndex]; + length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); + + return length; + }, + + // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + pointAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.pointAtLength(length, localOpt); + }, + + // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + pointAtLength: function(length, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (length === 0) return this.start.clone(); + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastVisibleSegment; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isVisible) { + if (length <= (l + d)) { + return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } + + lastVisibleSegment = segment; + } + + l += d; + } + + // if length requested is higher than the length of the path, return last visible segment endpoint + if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); + + // if no visible segment, return last segment end point (no matter if fromStart or no) + var lastSegment = segments[numSegments - 1]; + return lastSegment.end.clone(); + }, + + // Private function. + pointAtT: function(t) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].pointAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); + + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; + + return segments[segmentIndex].pointAtT(tValue); + }, + + // Helper method for adding segments. + prepareSegment: function(segment, previousSegment, nextSegment) { + + // insert after previous segment and before previous segment's next segment + segment.previousSegment = previousSegment; + segment.nextSegment = nextSegment; + if (previousSegment) previousSegment.nextSegment = segment; + if (nextSegment) nextSegment.previousSegment = segment; + + var updateSubpathStart = segment; + if (segment.isSubpathStart) { + segment.subpathStartSegment = segment; // assign self as subpath start segment + updateSubpathStart = nextSegment; // start updating from next segment + } + + // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments + if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); + + return segment; + }, + + // Default precision + PRECISION: 3, + + // Remove the segment at `index`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + removeSegment: function(index) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); + + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); + + var removedSegment = segments.splice(index, 1)[0]; + var previousSegment = removedSegment.previousSegment; + var nextSegment = removedSegment.nextSegment; + + // link the previous and next segments together (if present) + if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null + if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null + + // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached + if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, + + // Replace the segment at `index` with `arg`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Accepts one segment or an array of segments as argument. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + replaceSegment: function(index, arg) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); + + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); + + var currentSegment; + + var replacedSegment = segments[index]; + var previousSegment = replacedSegment.previousSegment; + var nextSegment = replacedSegment.nextSegment; + + var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? + + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); + + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 1, currentSegment); // directly replace + + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); + + segments.splice(index, 1); + + var n = arg.length; + for (var i = 0; i < n; i++) { + + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; + + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + } + } + + // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached + if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, + + scale: function(sx, sy, origin) { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + segment.scale(sx, sy, origin); + } + + return this; + }, + + segmentAt: function(ratio, opt) { + + var index = this.segmentIndexAt(ratio, opt); + if (!index) return null; + + return this.getSegment(index); + }, + + // Accepts negative length. + segmentAtLength: function(length, opt) { + + var index = this.segmentIndexAtLength(length, opt); + if (!index) return null; + + return this.getSegment(index); + }, + + segmentIndexAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.segmentIndexAtLength(length, localOpt); + }, + + toPoints: function(opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + + var points = []; + var partialPoints = []; + for (var i = 0; i < numSegments; i++) { + var segment = segments[i]; + if (segment.isVisible) { + var currentSegmentSubdivisions = segmentSubdivisions[i]; + if (currentSegmentSubdivisions.length > 0) { + var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { + return curve.start; + }); + Array.prototype.push.apply(partialPoints, subdivisionPoints); + } else { + partialPoints.push(segment.start); + } + } else if (partialPoints.length > 0) { + partialPoints.push(segments[i - 1].end); + points.push(partialPoints); + partialPoints = []; + } + } + + if (partialPoints.length > 0) { + partialPoints.push(this.end); + points.push(partialPoints); + } + return points; + }, + + toPolylines: function(opt) { + + var polylines = []; + var points = this.toPoints(opt); + if (!points) return null; + for (var i = 0, n = points.length; i < n; i++) { + polylines.push(new Polyline(points[i])); + } + + return polylines; + }, + + intersectionWithLine: function(line, opt) { + + var intersection = null; + var polylines = this.toPolylines(opt); + if (!polylines) return null; + for (var i = 0, n = polylines.length; i < n; i++) { + var polyline = polylines[i]; + var polylineIntersection = line.intersect(polyline); + if (polylineIntersection) { + intersection || (intersection = []); + if (Array.isArray(polylineIntersection)) { + Array.prototype.push.apply(intersection, polylineIntersection); + } else { + intersection.push(polylineIntersection); + } + } + } + + return intersection; + }, + + // Accepts negative length. + segmentIndexAtLength: function(length, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastVisibleSegmentIndex = null; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isVisible) { + if (length <= (l + d)) return i; + lastVisibleSegmentIndex = i; + } + + l += d; + } + + // if length requested is higher than the length of the path, return last visible segment index + // if no visible segment, return null + return lastVisibleSegmentIndex; + }, + + // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + tangentAt: function(ratio, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; + + var pathLength = this.length(localOpt); + var length = pathLength * ratio; + + return this.tangentAtLength(length, localOpt); + }, + + // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + tangentAtLength: function(length, opt) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastValidSegment; // visible AND differentiable (with a tangent) + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isDifferentiable()) { + if (length <= (l + d)) { + return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } + + lastValidSegment = segment; + } + + l += d; + } + + // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment + if (lastValidSegment) { + var t = (fromStart ? 1 : 0); + return lastValidSegment.tangentAtT(t); + } + + // if no valid segment, return null + return null; + }, + + // Private function. + tangentAtT: function(t) { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].tangentAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); + + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; + + return segments[segmentIndex].tangentAtT(tValue); + }, + + translate: function(tx, ty) { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + segment.translate(tx, ty); + } + + return this; + }, + + // Helper method for updating subpath start of segments, starting with the one provided. + updateSubpathStartSegment: function(segment) { + + var previousSegment = segment.previousSegment; // may be null + while (segment && !segment.isSubpathStart) { + + // assign previous segment's subpath start segment to this segment + if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null + else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! + + previousSegment = segment; + segment = segment.nextSegment; // move on to the segment after etc. + } + }, + + // Returns a string that can be used to reconstruct the path. + // Additional error checking compared to toString (must start with M segment). + serialize: function() { + + if (!this.isValid()) throw new Error('Invalid path segments.'); + + return this.toString(); + }, + + toString: function() { + + var segments = this.segments; + var numSegments = segments.length; + + var pathData = ''; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + pathData += segment.serialize() + ' '; + } + + return pathData.trim(); + } + }; + + Object.defineProperty(Path.prototype, 'start', { + // Getter for the first visible endpoint of the path. + + configurable: true, + + enumerable: true, + + get: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + if (segment.isVisible) return segment.start; + } + + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); + + Object.defineProperty(Path.prototype, 'end', { + // Getter for the last visible endpoint of the path. + + configurable: true, + + enumerable: true, + + get: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; + + for (var i = numSegments - 1; i >= 0; i--) { + + var segment = segments[i]; + if (segment.isVisible) return segment.end; + } + + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); + + /* + 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 = parseFloat(xy[0]); + y = parseFloat(xy[1]); + + } 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. + Point.fromPolar = function(distance, angle, origin) { + + origin = (origin && new Point(origin)) || new 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 new Point(origin.x + x, origin.y + y); + }; + + // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. + Point.random = function(x1, x2, y1, y2) { + + return new Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); + }; + + 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 = min(max(this.x, r.x), r.x + r.width); + this.y = min(max(this.y, r.y), r.y + r.height); + return this; + }, + + // Return the bearing between me and the given point. + bearing: function(point) { + + return (new 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 this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref); + }, + + clone: function() { + + return new Point(this); + }, + + difference: function(dx, dy) { + + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } + + return new Point(this.x - (dx || 0), this.y - (dy || 0)); + }, + + // Returns distance between me and point `p`. + distance: function(p) { + + return (new Line(this, p)).length(); + }, + + squaredDistance: function(p) { + + return (new Line(this, p)).squaredLength(); + }, + + equals: function(p) { + + return !!p && + 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((new Point(ref)).theta(this)); + var offset = this.offset(cos(theta) * distance, -sin(theta) * distance); + return offset; + }, + + // 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 (new Point(ref)).move(this, this.distance(ref)); + }, + + // Rotate point by angle around origin. + // Angle is flipped because this is a left-handed coord system (y-axis points downward). + rotate: function(origin, angle) { + + origin = origin || new g.Point(0, 0); + + angle = toRad(normalizeAngle(-angle)); + var cosAngle = cos(angle); + var sinAngle = sin(angle); + + var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x; + var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y; + + this.x = x; + this.y = 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 && new Point(origin)) || new 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 = new Point(p); + + // Invert the y-axis. + var y = -(p.y - this.y); + var x = p.x - this.x; + var rad = atan2(y, x); // defined for all 0 corner cases + + // Correction for III. and IV. quadrant. + if (rad < 0) { + rad = 2 * PI + rad; + } + + return 180 * rad / PI; + }, + + // Compute the angle between vector from me to p1 and the vector from me to p2. + // ordering of points p1 and p2 is important! + // theta function's angle convention: + // returns angles between 0 and 180 when the angle is counterclockwise + // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones + // returns NaN if any of the points p1, p2 is coincident with this point + angleBetween: function(p1, p2) { + + var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); + + if (angleBetween < 0) { + angleBetween += 360; // correction to keep angleBetween between 0 and 360 + } + + return angleBetween; + }, + + // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. + // Returns NaN if p is at 0,0. + vectorAngle: function(p) { + + var zero = new Point(0,0); + return zero.angleBetween(this, p); + }, + + 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 && new Point(o)) || new 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(new 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; + }, + + // Returns the dot product of this point with given other point + dot: function(p) { + + return p ? (this.x * p.x + this.y * p.y) : NaN; + }, + + // Returns the cross product of this point relative to two other points + // this point is the common point + // point p1 lies on the first vector, point p2 lies on the second vector + // watch out for the ordering of points p1 and p2! + // positive result indicates a clockwise ("right") turn from first to second vector + // negative result indicates a counterclockwise ("left") turn from first to second vector + // note that the above directions are reversed from the usual answer on the Internet + // that is because we are in a left-handed coord system (because the y-axis points downward) + cross: function(p1, p2) { + + return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; + }, + + + // Linear interpolation + lerp: function(p, t) { + + var x = this.x; + var y = this.y; + return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y); + } + }; + + Point.prototype.translate = Point.prototype.offset; + + 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; + }; + + Rect.fromEllipse = function(e) { + + e = new Ellipse(e); + return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); + }; + + 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) { + + if (!angle) return this.clone(); + + 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 new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); + }, + + bottomLeft: function() { + + return new Point(this.x, this.y + this.height); + }, + + bottomLine: function() { + + return new Line(this.bottomLeft(), this.bottomRight()); + }, + + bottomMiddle: function() { + + return new Point(this.x + this.width / 2, this.y + this.height); + }, + + center: function() { + + return new Point(this.x + this.width / 2, this.y + this.height / 2); + }, + + clone: function() { + + return new Rect(this); + }, + + // @return {bool} true if point p is insight me + containsPoint: function(p) { + + p = new 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 = new Rect(this).normalize(); + var r1 = new 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 new Point(this.x + this.width, this.y + this.height); + }, + + // @return {boolean} true if rectangles are equal. + equals: function(r) { + + var mr = (new Rect(this)).normalize(); + var nr = (new 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 = max(myOrigin.x, rOrigin.x); + var y = max(myOrigin.y, rOrigin.y); + + return new Rect(x, y, min(myCorner.x, rCorner.x) - x, min(myCorner.y, rCorner.y) - y); + }, + + intersectionWithLine: function(line) { + + var r = this; + var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; + var points = []; + var dedupeArr = []; + var pt, i; + + var n = rectLines.length; + for (i = 0; i < n; i ++) { + + pt = line.intersect(rectLines[i]); + if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) { + points.push(pt); + dedupeArr.push(pt.toString()); + } + } + + return points.length > 0 ? points : null; + }, + + // 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 = new Point(p); + var center = new 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 = [ + this.topLine(), + this.rightLine(), + this.bottomLine(), + this.leftLine() + ]; + var connector = new 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 new Line(this.topLeft(), this.bottomLeft()); + }, + + leftMiddle: function() { + + return new 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; + }, + + // Offset me by the specified amount. + offset: function(dx, dy) { + + // pretend that this is a point and call offset() + // rewrites x and y according to dx and dy + return Point.prototype.offset.call(this, dx, dy); + }, + + // 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 new 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 = new Point(point); + if (this.containsPoint(point)) { + var side = this.sideNearestToPoint(point); + switch (side){ + case 'right': return new Point(this.x + this.width, point.y); + case 'left': return new Point(this.x, point.y); + case 'bottom': return new Point(point.x, this.y + this.height); + case 'top': return new Point(point.x, this.y); + } + } + return point.adhereToRect(this); + }, + + rightLine: function() { + + return new Line(this.topRight(), this.bottomRight()); + }, + + rightMiddle: function() { + + return new 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 = new 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.topLeft(); + 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.bottomRight(); + 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: min(sx1, sx2, sx3, sx4), + sy: min(sy1, sy2, sy3, sy4) + }; + }, + + maxRectUniformScaleToFit: function(rect, origin) { + + var scale = this.maxRectScaleToFit(rect, origin); + return 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 = new 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 new Line(this.topLeft(), this.topRight()); + }, + + topMiddle: function() { + + return new Point(this.x + this.width / 2, this.y); + }, + + topRight: function() { + + return new 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) { + + rect = new Rect(rect); + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = rect.origin(); + var rCorner = rect.corner(); + + var originX = min(myOrigin.x, rOrigin.x); + var originY = min(myOrigin.y, rOrigin.y); + var cornerX = max(myCorner.x, rCorner.x); + var cornerY = max(myCorner.y, rCorner.y); + + return new Rect(originX, originY, cornerX - originX, cornerY - originY); + } + }; + + Rect.prototype.bottomRight = Rect.prototype.corner; + + Rect.prototype.topLeft = Rect.prototype.origin; + + Rect.prototype.translate = Rect.prototype.offset; + + var Polyline = g.Polyline = function(points) { + + if (!(this instanceof Polyline)) { + return new Polyline(points); + } + + if (typeof points === 'string') { + return new Polyline.parse(points); + } + + this.points = (Array.isArray(points) ? points.map(Point) : []); + }; + + Polyline.parse = function(svgString) { + + if (svgString === '') return new Polyline(); + + var points = []; + + var coords = svgString.split(/\s|,/); + var n = coords.length; + for (var i = 0; i < n; i += 2) { + points.push({ x: +coords[i], y: +coords[i + 1] }); + } + + return new Polyline(points); + }; + + Polyline.prototype = { + + bbox: function() { + + var x1 = Infinity; + var x2 = -Infinity; + var y1 = Infinity; + var y2 = -Infinity; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + var x = point.x; + var y = point.y; + + if (x < x1) x1 = x; + if (x > x2) x2 = x; + if (y < y1) y1 = y; + if (y > y2) y2 = y; + } + + return new Rect(x1, y1, x2 - x1, y2 - y1); + }, + + clone: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + var newPoints = []; + for (var i = 0; i < numPoints; i++) { + + var point = points[i].clone(); + newPoints.push(point); + } + + return new Polyline(newPoints); + }, + + closestPoint: function(p) { + + var cpLength = this.closestPointLength(p); + + return this.pointAtLength(cpLength); + }, + + closestPointLength: function(p) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + if (numPoints === 1) return 0; // if there is only one point + + var cpLength; + var minSqrDistance = Infinity; + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + + var line = new Line(points[i], points[i + 1]); + var lineLength = line.length(); + + var cpNormalizedLength = line.closestPointNormalizedLength(p); + var cp = line.pointAt(cpNormalizedLength); + + var sqrDistance = cp.squaredDistance(p); + if (sqrDistance < minSqrDistance) { + minSqrDistance = sqrDistance; + cpLength = length + (cpNormalizedLength * lineLength); + } + + length += lineLength; + } + + return cpLength; + }, + + closestPointNormalizedLength: function(p) { + + var cpLength = this.closestPointLength(p); + if (cpLength === 0) return 0; // shortcut + + var length = this.length(); + if (length === 0) return 0; // prevents division by zero + + return cpLength / length; + }, + + closestPointTangent: function(p) { + + var cpLength = this.closestPointLength(p); + + return this.tangentAtLength(cpLength); + }, + + // Returns a convex-hull polyline from this polyline. + // Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan). + // Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise. + // Minimal polyline is found (only vertices of the hull are reported, no collinear points). + convexHull: function() { + + var i; + var n; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + // step 1: find the starting point - point with the lowest y (if equality, highest x) + var startPoint; + for (i = 0; i < numPoints; i++) { + if (startPoint === undefined) { + // if this is the first point we see, set it as start point + startPoint = points[i]; + + } else if (points[i].y < startPoint.y) { + // start point should have lowest y from all points + startPoint = points[i]; + + } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { + // if two points have the lowest y, choose the one that has highest x + // there are no points to the right of startPoint - no ambiguity about theta 0 + // if there are several coincident start point candidates, first one is reported + startPoint = points[i]; + } + } + + // step 2: sort the list of points + // sorting by angle between line from startPoint to point and the x-axis (theta) + + // step 2a: create the point records = [point, originalIndex, angle] + var sortedPointRecords = []; + for (i = 0; i < numPoints; i++) { + + var angle = startPoint.theta(points[i]); + if (angle === 0) { + angle = 360; // give highest angle to start point + // the start point will end up at end of sorted list + // the start point will end up at beginning of hull points list + } + + var entry = [points[i], i, angle]; + sortedPointRecords.push(entry); + } + + // step 2b: sort the list in place + sortedPointRecords.sort(function(record1, record2) { + // returning a negative number here sorts record1 before record2 + // if first angle is smaller than second, first angle should come before second + + var sortOutput = record1[2] - record2[2]; // negative if first angle smaller + if (sortOutput === 0) { + // if the two angles are equal, sort by originalIndex + sortOutput = record2[1] - record1[1]; // negative if first index larger + // coincident points will be sorted in reverse-numerical order + // so the coincident points with lower original index will be considered first + } + + return sortOutput; + }); + + // step 2c: duplicate start record from the top of the stack to the bottom of the stack + if (sortedPointRecords.length > 2) { + var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; + sortedPointRecords.unshift(startPointRecord); + } + + // step 3a: go through sorted points in order and find those with right turns + // we want to get our results in clockwise order + var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull + var hullPointRecords = []; // stack of records with right turns - hull point candidates + + var currentPointRecord; + var currentPoint; + var lastHullPointRecord; + var lastHullPoint; + var secondLastHullPointRecord; + var secondLastHullPoint; + while (sortedPointRecords.length !== 0) { + + currentPointRecord = sortedPointRecords.pop(); + currentPoint = currentPointRecord[0]; + + // check if point has already been discarded + // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' + if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { + // this point had an incorrect turn at some previous iteration of this loop + // this disqualifies it from possibly being on the hull + continue; + } + + var correctTurnFound = false; + while (!correctTurnFound) { + + if (hullPointRecords.length < 2) { + // not enough points for comparison, just add current point + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; + + } else { + lastHullPointRecord = hullPointRecords.pop(); + lastHullPoint = lastHullPointRecord[0]; + secondLastHullPointRecord = hullPointRecords.pop(); + secondLastHullPoint = secondLastHullPointRecord[0]; + + var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); + + if (crossProduct < 0) { + // found a right turn + hullPointRecords.push(secondLastHullPointRecord); + hullPointRecords.push(lastHullPointRecord); + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; + + } else if (crossProduct === 0) { + // the three points are collinear + // three options: + // there may be a 180 or 0 degree angle at lastHullPoint + // or two of the three points are coincident + var THRESHOLD = 1e-10; // we have to take rounding errors into account + var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); + if (abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 + // if the cross product is 0 because the angle is 180 degrees + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + + } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { + // if the cross product is 0 because two points are the same + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + + } else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 + // if the cross product is 0 because the angle is 0 degrees + // remove last hull point from hull BUT do not discard it + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // put last hull point back into the sorted point records list + sortedPointRecords.push(lastHullPointRecord); + // we are switching the order of the 0deg and 180deg points + // correct turn not found + } + + } else { + // found a left turn + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter of loop) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + } + } + } + } + // at this point, hullPointRecords contains the output points in clockwise order + // the points start with lowest-y,highest-x startPoint, and end at the same point + + // step 3b: remove duplicated startPointRecord from the end of the array + if (hullPointRecords.length > 2) { + hullPointRecords.pop(); + } + + // step 4: find the lowest originalIndex record and put it at the beginning of hull + var lowestHullIndex; // the lowest originalIndex on the hull + var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex + n = hullPointRecords.length; + for (i = 0; i < n; i++) { + + var currentHullIndex = hullPointRecords[i][1]; + + if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { + lowestHullIndex = currentHullIndex; + indexOfLowestHullIndexRecord = i; + } + } + + var hullPointRecordsReordered = []; + if (indexOfLowestHullIndexRecord > 0) { + var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); + var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); + hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); + + } else { + hullPointRecordsReordered = hullPointRecords; + } + + var hullPoints = []; + n = hullPointRecordsReordered.length; + for (i = 0; i < n; i++) { + hullPoints.push(hullPointRecordsReordered[i][0]); + } + + return new Polyline(hullPoints); + }, + + // Checks whether two polylines are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { + + if (!p) return false; + + var points = this.points; + var otherPoints = p.points; + + var numPoints = points.length; + if (otherPoints.length !== numPoints) return false; // if the two polylines have different number of points, they cannot be equal + + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + var otherPoint = p.points[i]; + + // as soon as an inequality is found in points, return false + if (!point.equals(otherPoint)) return false; + } + + // if no inequality found in points, return true + return true; + }, + + isDifferentiable: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return false; + + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + + // as soon as a differentiable line is found between two points, return true + if (line.isDifferentiable()) return true; + } + + // if no differentiable line is found between pairs of points, return false + return false; + }, + + length: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + length += points[i].distance(points[i + 1]); + } + + return length; + }, + + pointAt: function(ratio) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point + + if (ratio <= 0) return points[0].clone(); + if (ratio >= 1) return points[numPoints - 1].clone(); + + var polylineLength = this.length(); + var length = polylineLength * ratio; + + return this.pointAtLength(length); + }, + + pointAtLength: function(length) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + var l = 0; + var n = numPoints - 1; + for (var i = (fromStart ? 0 : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (length <= (l + d)) { + return line.pointAtLength((fromStart ? 1 : -1) * (length - l)); + } + + l += d; + } + + // if length requested is higher than the length of the polyline, return last endpoint + var lastPoint = (fromStart ? points[numPoints - 1] : points[0]); + return lastPoint.clone(); + }, + + scale: function(sx, sy, origin) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + points[i].scale(sx, sy, origin); + } + + return this; + }, + + tangentAt: function(ratio) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point + + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + + var polylineLength = this.length(); + var length = polylineLength * ratio; + + return this.tangentAtLength(length); + }, + + tangentAtLength: function(length) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } + + var lastValidLine; // differentiable (with a tangent) + var l = 0; // length so far + var n = numPoints - 1; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (line.isDifferentiable()) { // has a tangent line (line length is not 0) + if (length <= (l + d)) { + return line.tangentAtLength((fromStart ? 1 : -1) * (length - l)); + } + + lastValidLine = line; + } + + l += d; + } + + // if length requested is higher than the length of the polyline, return last valid endpoint + if (lastValidLine) { + var ratio = (fromStart ? 1 : 0); + return lastValidLine.tangentAt(ratio); + } + + // if no valid line, return null + return null; + }, + + intersectionWithLine: function(l) { + var line = new Line(l); + var intersections = []; + var points = this.points; + for (var i = 0, n = points.length - 1; i < n; i++) { + var a = points[i]; + var b = points[i+1]; + var l2 = new Line(a, b); + var int = line.intersectionWithLine(l2); + if (int) intersections.push(int[0]); + } + return (intersections.length > 0) ? intersections : null; + }, + + translate: function(tx, ty) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + points[i].translate(tx, ty); + } + + return this; + }, + + // Return svgString that can be used to recreate this line. + serialize: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return ''; // if points array is empty + + var output = ''; + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + output += point.x + ',' + point.y + ' '; + } + + return output.trim(); + }, + + toString: function() { + + return this.points + ''; + } + }; + + Object.defineProperty(Polyline.prototype, 'start', { + // Getter for the first point of the polyline. + + configurable: true, + + enumerable: true, + + get: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + return this.points[0]; + }, + }); + + Object.defineProperty(Polyline.prototype, 'end', { + // Getter for the last point of the polyline. + + configurable: true, + + enumerable: true, + + get: function() { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + return this.points[numPoints - 1]; + }, + }); + + 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 normalizeAngle = g.normalizeAngle = function(angle) { + + return (angle % 360) + (angle < 0 ? 360 : 0); + }; + + var snapToGrid = g.snapToGrid = function(value, gridSize) { + + return gridSize * 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; + // For backwards compatibility: + g.ellipse = g.Ellipse; + g.line = g.Line; + g.point = g.Point; + g.rect = g.Rect; + + // Local helper function. + // Use an array of arguments to call a constructor (function called with `new`). + // Adapted from https://stackoverflow.com/a/8843181/2263595 + // It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). + // - If that is the case, use `new constructor(arg1, arg2)`, for example. + // It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. + // - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. + function applyToNew(constructor, argsArray) { + // The `new` keyword can only be applied to functions that take a limited number of arguments. + // - We can fake that with .bind(). + // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. + // - So `new (constructor.bind(thisArg, arg1, arg2...))` + // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. + // We need to pass in a variable number of arguments to the bind() call. + // - We can use .apply(). + // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, arg2...]))` + // - `thisArg` can still be anything because `new` overwrites it. + // Finally, to make sure that constructor.bind overwriting is not a problem, we switch to `Function.prototype.bind`. + // - So, the final version is `new (Function.prototype.bind.apply(constructor, [thisArg, arg1, arg2...]))` + + // The function expects `argsArray[0]` to be `thisArg`. + // - This means that whatever is sent as the first element will be ignored. + // - The constructor will only see arguments starting from argsArray[1]. + // - So, a new dummy element is inserted at the start of the array. + argsArray.unshift(null); + + return new (Function.prototype.bind.apply(constructor, argsArray)); + } + + // Local helper function. + // Add properties from arguments on top of properties from `obj`. + // This allows for rudimentary inheritance. + // - The `obj` argument acts as parent. + // - This function creates a new object that inherits all `obj` properties and adds/replaces those that are present in arguments. + // - A high-level example: calling `extend(Vehicle, Car)` would be akin to declaring `class Car extends Vehicle`. + function extend(obj) { + // In JavaScript, the combination of a constructor function (e.g. `g.Line = function(...) {...}`) and prototype (e.g. `g.Line.prototype = {...}) is akin to a C++ class. + // - When inheritance is not necessary, we can leave it at that. (This would be akin to calling extend with only `obj`.) + // - But, what if we wanted the `g.Line` quasiclass to inherit from another quasiclass (let's call it `g.GeometryObject`) in JavaScript? + // - First, realize that both of those quasiclasses would still have their own separate constructor function. + // - So what we are actually saying is that we want the `g.Line` prototype to inherit from `g.GeometryObject` prototype. + // - This method provides a way to do exactly that. + // - It copies parent prototype's properties, then adds extra ones from child prototype/overrides parent prototype properties with child prototype properties. + // - Therefore, to continue with the example above: + // - `g.Line.prototype = extend(g.GeometryObject.prototype, linePrototype)` + // - Where `linePrototype` is a properties object that looks just like `g.Line.prototype` does right now. + // - Then, `g.Line` would allow the programmer to access to all methods currently in `g.Line.Prototype`, plus any non-overriden methods from `g.GeometryObject.prototype`. + // - In that aspect, `g.GeometryObject` would then act like the parent of `g.Line`. + // - Multiple inheritance is also possible, if multiple arguments are provided. + // - What if we wanted to add another level of abstraction between `g.GeometryObject` and `g.Line` (let's call it `g.LinearObject`)? + // - `g.Line.prototype = extend(g.GeometryObject.prototype, g.LinearObject.prototype, linePrototype)` + // - The ancestors are applied in order of appearance. + // - That means that `g.Line` would have inherited from `g.LinearObject` that would have inherited from `g.GeometryObject`. + // - Any number of ancestors may be provided. + // - Note that neither `obj` nor any of the arguments need to actually be prototypes of any JavaScript quasiclass, that was just a simplified explanation. + // - We can create a new object composed from the properties of any number of other objects (since they do not have a constructor, we can think of those as interfaces). + // - `extend({ a: 1, b: 2 }, { b: 10, c: 20 }, { c: 100, d: 200 })` gives `{ a: 1, b: 10, c: 100, d: 200 }`. + // - Basically, with this function, we can emulate the `extends` keyword as well as the `implements` keyword. + // - Therefore, both of the following are valid: + // - `Lineto.prototype = extend(Line.prototype, segmentPrototype, linetoPrototype)` + // - `Moveto.prototype = extend(segmentPrototype, movetoPrototype)` + + var i; + var n; + + var args = []; + n = arguments.length; + for (i = 1; i < n; i++) { // skip over obj + args.push(arguments[i]); + } + + if (!obj) throw new Error('Missing a parent object.'); + var child = Object.create(obj); + + n = args.length; + for (i = 0; i < n; i++) { + + var src = args[i]; + + var inheritedProperty; + var key; + for (key in src) { + + if (src.hasOwnProperty(key)) { + delete child[key]; // delete property inherited from parent + inheritedProperty = Object.getOwnPropertyDescriptor(src, key); // get new definition of property from src + Object.defineProperty(child, key, inheritedProperty); // re-add property with new definition (includes getter/setter methods) + } + } + } + + return child; + } + + // Path segment interface: + var segmentPrototype = { + + // Redirect calls to closestPointNormalizedLength() function if closestPointT() is not defined for segment. + closestPointT: function(p) { + + if (this.closestPointNormalizedLength) return this.closestPointNormalizedLength(p); + + throw new Error('Neither closestPointT() nor closestPointNormalizedLength() function is implemented.'); + }, + + isSegment: true, + + isSubpathStart: false, // true for Moveto segments + + isVisible: true, // false for Moveto segments + + nextSegment: null, // needed for subpath start segment updating + + // Return a fraction of result of length() function if lengthAtT() is not defined for segment. + lengthAtT: function(t) { + + if (t <= 0) return 0; + + var length = this.length(); + + if (t >= 1) return length; + + return length * t; + }, + + // Redirect calls to pointAt() function if pointAtT() is not defined for segment. + pointAtT: function(t) { + + if (this.pointAt) return this.pointAt(t); + + throw new Error('Neither pointAtT() nor pointAt() function is implemented.'); + }, + + previousSegment: null, // needed to get segment start property + + subpathStartSegment: null, // needed to get closepath segment end property + + // Redirect calls to tangentAt() function if tangentAtT() is not defined for segment. + tangentAtT: function(t) { + + if (this.tangentAt) return this.tangentAt(t); + + throw new Error('Neither tangentAtT() nor tangentAt() function is implemented.'); + }, + + // VIRTUAL PROPERTIES (must be overriden by actual Segment implementations): + + // type + + // start // getter, always throws error for Moveto + + // end // usually directly assigned, getter for Closepath + + bbox: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + clone: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + closestPoint: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + closestPointLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + closestPointNormalizedLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + closestPointTangent: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + equals: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + getSubdivisions: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + isDifferentiable: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + length: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + pointAt: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + pointAtLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + scale: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + tangentAt: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + tangentAtLength: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + translate: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + serialize: function() { + + throw new Error('Declaration missing for virtual function.'); + }, + + toString: function() { + + throw new Error('Declaration missing for virtual function.'); + } + }; + + // Path segment implementations: + var Lineto = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Lineto)) { // switching context of `this` to Lineto when called without `new` + return applyToNew(Lineto, args); + } + + if (n === 0) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (none provided).'); + } + + var outputArray; + + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; + + } else if (n < 2) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; + } + + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; + + } else { // this is a poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { + + segmentPoint = args[i]; + outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } + }; + + var linetoPrototype = { + + clone: function() { + + return new Lineto(this.end); + }, + + getSubdivisions: function() { + + return []; + }, + + isDifferentiable: function() { + + if (!this.previousSegment) return false; + + return !this.start.equals(this.end); + }, + + scale: function(sx, sy, origin) { + + this.end.scale(sx, sy, origin); + return this; + }, + + translate: function(tx, ty) { + + this.end.translate(tx, ty); + return this; + }, + + type: 'L', + + serialize: function() { + + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, + + toString: function() { + + return this.type + ' ' + this.start + ' ' + this.end; + } + }; + + Object.defineProperty(linetoPrototype, 'start', { + // get a reference to the end point of previous segment + + configurable: true, + + enumerable: true, + + get: function() { + + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); + + return this.previousSegment.end; + } + }); + + Lineto.prototype = extend(segmentPrototype, Line.prototype, linetoPrototype); + + var Curveto = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Curveto)) { // switching context of `this` to Curveto when called without `new` + return applyToNew(Curveto, args); + } + + if (n === 0) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (none provided).'); + } + + var outputArray; + + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 6) { + this.controlPoint1 = new Point(+args[0], +args[1]); + this.controlPoint2 = new Point(+args[2], +args[3]); + this.end = new Point(+args[4], +args[5]); + return this; + + } else if (n < 6) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a poly-bezier segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 6) { // coords come in groups of six + + segmentCoords = args.slice(i, i + 6); // will send fewer than six coords if args.length not divisible by 6 + outputArray.push(applyToNew(Curveto, segmentCoords)); + } + return outputArray; + } + + } else { // points provided + if (n === 3) { + this.controlPoint1 = new Point(args[0]); + this.controlPoint2 = new Point(args[1]); + this.end = new Point(args[2]); + return this; + + } else if (n < 3) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' points provided).'); + + } else { // this is a poly-bezier segment + var segmentPoints; + outputArray = []; + for (i = 0; i < n; i += 3) { // points come in groups of three + + segmentPoints = args.slice(i, i + 3); // will send fewer than three points if args.length is not divisible by 3 + outputArray.push(applyToNew(Curveto, segmentPoints)); + } + return outputArray; + } + } + }; + + var curvetoPrototype = { + + clone: function() { + + return new Curveto(this.controlPoint1, this.controlPoint2, this.end); + }, + + isDifferentiable: function() { + + if (!this.previousSegment) return false; + + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); + }, + + scale: function(sx, sy, origin) { + + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, + + translate: function(tx, ty) { + + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, + + type: 'C', + + serialize: function() { + + var c1 = this.controlPoint1; + var c2 = this.controlPoint2; + var end = this.end; + return this.type + ' ' + c1.x + ' ' + c1.y + ' ' + c2.x + ' ' + c2.y + ' ' + end.x + ' ' + end.y; + }, + + toString: function() { + + return this.type + ' ' + this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; + + Object.defineProperty(curvetoPrototype, 'start', { + // get a reference to the end point of previous segment + + configurable: true, + + enumerable: true, + + get: function() { + + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); + + return this.previousSegment.end; + } + }); + + Curveto.prototype = extend(segmentPrototype, Curve.prototype, curvetoPrototype); + + var Moveto = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Moveto)) { // switching context of `this` to Moveto when called without `new` + return applyToNew(Moveto, args); + } + + if (n === 0) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (none provided).'); + } + + var outputArray; + + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; + + } else if (n < 2) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + if (i === 0) outputArray.push(applyToNew(Moveto, segmentCoords)); + else outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; + } + + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; + + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { // points come one by one + + segmentPoint = args[i]; + if (i === 0) outputArray.push(new Moveto(segmentPoint)); + else outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } + }; + + var movetoPrototype = { + + bbox: function() { + + return null; + }, + + clone: function() { + + return new Moveto(this.end); + }, + + closestPoint: function() { + + return this.end.clone(); + }, + + closestPointNormalizedLength: function() { + + return 0; + }, + + closestPointLength: function() { + + return 0; + }, + + closestPointT: function() { + + return 1; + }, + + closestPointTangent: function() { + + return null; + }, + + equals: function(m) { + + return this.end.equals(m.end); + }, + + getSubdivisions: function() { + + return []; + }, + + isDifferentiable: function() { + + return false; + }, + + isSubpathStart: true, + + isVisible: false, + + length: function() { + + return 0; + }, + + lengthAtT: function() { + + return 0; + }, + + pointAt: function() { + + return this.end.clone(); + }, + + pointAtLength: function() { + + return this.end.clone(); + }, + + pointAtT: function() { + + return this.end.clone(); + }, + + scale: function(sx, sy, origin) { + + this.end.scale(sx, sy, origin); + return this; + }, + + tangentAt: function() { + + return null; + }, + + tangentAtLength: function() { + + return null; + }, + + tangentAtT: function() { + + return null; + }, + + translate: function(tx, ty) { + + this.end.translate(tx, ty); + return this; + }, + + type: 'M', + + serialize: function() { + + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, + + toString: function() { + + return this.type + ' ' + this.end; + } + }; + + Object.defineProperty(movetoPrototype, 'start', { + + configurable: true, + + enumerable: true, + + get: function() { + + throw new Error('Illegal access. Moveto segments should not need a start property.'); + } + }) + + Moveto.prototype = extend(segmentPrototype, movetoPrototype); // does not inherit from any other geometry object + + var Closepath = function() { + + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } + + if (!(this instanceof Closepath)) { // switching context of `this` to Closepath when called without `new` + return applyToNew(Closepath, args); + } + + if (n > 0) { + throw new Error('Closepath constructor expects no arguments.'); + } + + return this; + }; + + var closepathPrototype = { + + clone: function() { + + return new Closepath(); + }, + + getSubdivisions: function() { + + return []; + }, + + isDifferentiable: function() { + + if (!this.previousSegment || !this.subpathStartSegment) return false; + + return !this.start.equals(this.end); + }, + + scale: function() { + + return this; + }, + + translate: function() { + + return this; + }, + + type: 'Z', + + serialize: function() { + + return this.type; + }, + + toString: function() { + + return this.type + ' ' + this.start + ' ' + this.end; + } + }; + + Object.defineProperty(closepathPrototype, 'start', { + // get a reference to the end point of previous segment + + configurable: true, + + enumerable: true, + + get: function() { + + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); + + return this.previousSegment.end; + } + }); + + Object.defineProperty(closepathPrototype, 'end', { + // get a reference to the end point of subpath start segment + + configurable: true, + + enumerable: true, + + get: function() { + + if (!this.subpathStartSegment) throw new Error('Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)'); + + return this.subpathStartSegment.end; + } + }) + + Closepath.prototype = extend(segmentPrototype, Line.prototype, closepathPrototype); + + var segmentTypes = Path.segmentTypes = { + L: Lineto, + C: Curveto, + M: Moveto, + Z: Closepath, + z: Closepath + }; + + Path.regexSupportedData = new RegExp('^[\\s\\d' + Object.keys(segmentTypes).join('') + ',.]*$'); + + Path.isDataSupported = function(d) { + if (typeof d !== 'string') return false; + return this.regexSupportedData.test(d); + } return g; diff --git a/dist/geometry.min.js b/dist/geometry.min.js index 485968b67..f2c3a1166 100644 --- a/dist/geometry.min.js +++ b/dist/geometry.min.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -27,7 +27,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. }(this, function() { -var g=function(){var a={},b=Math,c=b.abs,d=b.cos,e=b.sin,f=b.sqrt,g=b.min,h=b.max,i=b.atan2,j=b.round,k=b.floor,l=b.PI,m=b.random,n=b.pow;a.bezier={curveThroughPoints:function(a){for(var b=this.getCurveControlPoints(a),c=["M",a[0].x,a[0].y],d=0;dj.x+h/2,n=fj.x?g-e:g+e,d=h*h/(f-k)-h*h*(g-l)*(c-l)/(i*i*(f-k))+k):(d=g>j.y?f+e:f-e,c=i*i/(g-l)-i*i*(f-k)*(d-k)/(h*h*(g-l))+l),a.point(d,c).theta(b)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a),b&&a.rotate(q(this.x,this.y),b);var c,d=a.x-this.x,e=a.y-this.y;if(0===d)return c=this.bbox().pointNearestToPoint(a),b?c.rotate(q(this.x,this.y),-b):c;var g=e/d,h=g*g,i=this.a*this.a,j=this.b*this.b,k=f(1/(1/i+h/j));k=d<0?-k:k;var l=g*k;return c=q(this.x+k,this.y+l),b?c.rotate(q(this.x,this.y),-b):c},toString:function(){return q(this.x,this.y).toString()+" "+this.a+" "+this.b}};var p=a.Line=function(a,b){return this instanceof p?a instanceof p?p(a.start,a.end):(this.start=q(a),void(this.end=q(b))):new p(a,b)};a.Line.prototype={bearing:function(){var a=w(this.start.y),b=w(this.end.y),c=this.start.x,f=this.end.x,g=w(f-c),h=e(g)*d(b),j=d(a)*e(b)-e(a)*d(b)*d(g),k=v(i(h,j)),l=["NE","E","SE","S","SW","W","NW","N"],m=k-22.5;return m<0&&(m+=360),m=parseInt(m/45),l[m]},clone:function(){return p(this.start,this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.end.x===a.end.x&&this.end.y===a.end.y},intersect:function(a){if(a instanceof p){var b=q(this.end.x-this.start.x,this.end.y-this.start.y),c=q(a.end.x-a.start.x,a.end.y-a.start.y),d=b.x*c.y-b.y*c.x,e=q(a.start.x-this.start.x,a.start.y-this.start.y),f=e.x*c.y-e.y*c.x,g=e.x*b.y-e.y*b.x;if(0===d||f*d<0||g*d<0)return null;if(d>0){if(f>d||g>d)return null}else if(f0?l:null}return null},length:function(){return f(this.squaredLength())},midpoint:function(){return q((this.start.x+this.end.x)/2,(this.start.y+this.end.y)/2)},pointAt:function(a){var b=(1-a)*this.start.x+a*this.end.x,c=(1-a)*this.start.y+a*this.end.y;return q(b,c)},pointOffset:function(a){return((this.end.x-this.start.x)*(a.y-this.start.y)-(this.end.y-this.start.y)*(a.x-this.start.x))/2},vector:function(){return q(this.end.x-this.start.x,this.end.y-this.start.y)},closestPoint:function(a){return this.pointAt(this.closestPointNormalizedLength(a))},closestPointNormalizedLength:function(a){var b=this.vector().dot(p(this.start,a).vector());return Math.min(1,Math.max(0,b/this.squaredLength()))},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},toString:function(){return this.start.toString()+" "+this.end.toString()}},a.Line.prototype.intersection=a.Line.prototype.intersect;var q=a.Point=function(a,b){if(!(this instanceof q))return new q(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseInt(c[0],10),b=parseInt(c[1],10)}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};a.Point.fromPolar=function(a,b,f){f=f&&q(f)||q(0,0);var g=c(a*d(b)),h=c(a*e(b)),i=t(v(b));return i<90?h=-h:i<180?(g=-g,h=-h):i<270&&(g=-g),q(f.x+g,f.y+h)},a.Point.random=function(a,b,c,d){return q(k(m()*(b-a+1)+a),k(m()*(d-c+1)+c))},a.Point.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=g(h(this.x,a.x),a.x+a.width),this.y=g(h(this.y,a.y),a.y+a.height),this)},bearing:function(a){return p(this,a).bearing()},changeInAngle:function(a,b,c){return q(this).offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return q(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),q(this.x-(a||0),this.y-(b||0))},distance:function(a){return p(this,a).length()},squaredDistance:function(a){return p(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return f(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return c(a.x-this.x)+c(a.y-this.y)},move:function(a,b){var c=w(q(a).theta(this));return this.offset(d(c)*b,-e(c)*b)},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return q(a).move(this,this.distance(a))},rotate:function(a,b){b=(b+360)%360,this.toPolar(a),this.y+=w(b);var c=q.fromPolar(this.x,this.y,a);return this.x=c.x,this.y=c.y,this},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this},scale:function(a,b,c){return c=c&&q(c)||q(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=u(this.x,a),this.y=u(this.y,b||a),this},theta:function(a){a=q(a);var b=-(a.y-this.y),c=a.x-this.x,d=i(b,c);return d<0&&(d=2*l+d),180*d/l},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=q(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&q(a)||q(0,0);var b=this.x,c=this.y;return this.x=f((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=w(a.theta(q(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN}};var r=a.Rect=function(a,b,c,d){return this instanceof r?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new r(a,b,c,d)};a.Rect.fromEllipse=function(a){return a=o(a),r(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},a.Rect.prototype={bbox:function(a){var b=w(a||0),f=c(e(b)),g=c(d(b)),h=this.width*g+this.height*f,i=this.width*f+this.height*g;return r(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return q(this.x,this.y+this.height)},bottomLine:function(){return p(this.bottomLeft(),this.corner())},bottomMiddle:function(){return q(this.x+this.width/2,this.y+this.height)},center:function(){return q(this.x+this.width/2,this.y+this.height/2)},clone:function(){return r(this)},containsPoint:function(a){return a=q(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=r(this).normalize(),c=r(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return q(this.x+this.width,this.y+this.height)},equals:function(a){var b=r(this).normalize(),c=r(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=Math.max(b.x,d.x),g=Math.max(b.y,d.y);return r(f,g,Math.min(c.x,e.x)-f,Math.min(c.y,e.y)-g)},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a);var c,d=q(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[p(this.origin(),this.topRight()),p(this.topRight(),this.corner()),p(this.corner(),this.bottomLeft()),p(this.bottomLeft(),this.origin())],f=p(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return p(this.origin(),this.bottomLeft())},leftMiddle:function(){return q(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return q.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return q(this.x,this.y)},pointNearestToPoint:function(a){if(a=q(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return q(this.x+this.width,a.y);case"left":return q(this.x,a.y);case"bottom":return q(a.x,this.y+this.height);case"top":return q(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return p(this.topRight(),this.corner())},rightMiddle:function(){return q(this.x+this.width,this.y+this.height/2)},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this.width=j(this.width*b)/b,this.height=j(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(b,c){b=a.Rect(b),c||(c=b.center());var d,e,f,g,h,i,j,k,l=c.x,m=c.y;d=e=f=g=h=i=j=k=1/0;var n=b.origin();n.xl&&(e=(this.x+this.width-l)/(o.x-l)),o.y>m&&(i=(this.y+this.height-m)/(o.y-m));var p=b.topRight();p.x>l&&(f=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:Math.min(d,e,f,g),sy:Math.min(h,i,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return Math.min(c.sx,c.sy)},sideNearestToPoint:function(a){a=q(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cc.x&&(c=d[a]);var e=[];for(b=d.length,a=0;a2){var h=e[e.length-1];e.unshift(h)}for(var i,j,k,l,m,n,o={},p=[];0!==e.length;)if(i=e.pop(),j=i[0],!o.hasOwnProperty(i[0]+"@@"+i[1]))for(var q=!1;!q;)if(p.length<2)p.push(i),q=!0;else{k=p.pop(),l=k[0],m=p.pop(),n=m[0];var r=n.cross(l,j);if(r<0)p.push(m),p.push(k),p.push(i),q=!0;else if(0===r){var t=1e-10,u=l.angleBetween(n,j);Math.abs(u-180)2&&p.pop();var v,w=-1;for(b=p.length,a=0;a0){var z=p.slice(w),A=p.slice(0,w);y=z.concat(A)}else y=p;var B=[];for(b=y.length,a=0;a=1)return[new q(b,c,d,e),new q(e,e,e,e)];var f=this.getSkeletonPoints(a),g=f.startControlPoint1,h=f.startControlPoint2,i=f.divider,j=f.dividerControlPoint1,k=f.dividerControlPoint2;return[new q(b,g,h,i),new q(i,j,k,e)]},endpointDistance:function(){return this.start.distance(this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.controlPoint1.x===a.controlPoint1.x&&this.controlPoint1.y===a.controlPoint1.y&&this.controlPoint2.x===a.controlPoint2.x&&this.controlPoint2.y===a.controlPoint2.y&&this.end.x===a.end.x&&this.end.y===a.end.y},getSkeletonPoints:function(a){var b=this.start,c=this.controlPoint1,d=this.controlPoint2,e=this.end;if(a<=0)return{startControlPoint1:b.clone(),startControlPoint2:b.clone(),divider:b.clone(),dividerControlPoint1:c.clone(),dividerControlPoint2:d.clone()};if(a>=1)return{startControlPoint1:c.clone(),startControlPoint2:d.clone(),divider:e.clone(),dividerControlPoint1:e.clone(),dividerControlPoint2:e.clone()};var f=new s(b,c).pointAt(a),g=new s(c,d).pointAt(a),h=new s(d,e).pointAt(a),i=new s(f,g).pointAt(a),j=new s(g,h).pointAt(a),k=new s(i,j).pointAt(a),l={startControlPoint1:f,startControlPoint2:i,divider:k,dividerControlPoint1:j,dividerControlPoint2:h};return l},getSubdivisions:function(a){a=a||{};var b=void 0===a.precision?this.PRECISION:a.precision,c=[new q(this.start,this.controlPoint1,this.controlPoint2,this.end)];if(0===b)return c;for(var d=this.endpointDistance(),e=p(10,-b),f=0;;){f+=1;for(var g=[],h=c.length,i=0;i1&&r=1)return this.end.clone();var c=this.tAt(a,b);return this.pointAtT(c)},pointAtLength:function(a,b){var c=this.tAtLength(a,b);return this.pointAtT(c)},pointAtT:function(a){return a<=0?this.start.clone():a>=1?this.end.clone():this.getSkeletonPoints(a).divider},PRECISION:3,scale:function(a,b,c){return this.start.scale(a,b,c),this.controlPoint1.scale(a,b,c),this.controlPoint2.scale(a,b,c),this.end.scale(a,b,c),this},tangentAt:function(a,b){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var c=this.tAt(a,b);return this.tangentAtT(c)},tangentAtLength:function(a,b){if(!this.isDifferentiable())return null;var c=this.tAtLength(a,b);return this.tangentAtT(c)},tangentAtT:function(a){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var b=this.getSkeletonPoints(a),c=b.startControlPoint2,d=b.dividerControlPoint1,e=b.divider,f=new s(c,d);return f.translate(e.x-c.x,e.y-c.y),f},tAt:function(a,b){if(a<=0)return 0;if(a>=1)return 1;b=b||{};var c=void 0===b.precision?this.PRECISION:b.precision,d=void 0===b.subdivisions?this.getSubdivisions({precision:c}):b.subdivisions,e={precision:c,subdivisions:d},f=this.length(e),g=f*a;return this.tAtLength(g,e)},tAtLength:function(a,b){var c=!0;a<0&&(c=!1,a=-a),b=b||{};for(var d,e,f,g,h,i=void 0===b.precision?this.PRECISION:b.precision,j=void 0===b.subdivisions?this.getSubdivisions({precision:i}):b.subdivisions,k={precision:i,subdivisions:j},l=0,m=j.length,n=1/m,o=c?0:m-1;c?o=0;c?o++:o--){var q=j[o],r=q.endpointDistance();if(a<=l+r){d=q,e=o*n,f=(o+1)*n,g=c?a-l:r+l-a,h=c?r+l-a:a-l;break}l+=r}if(!d)return c?1:0;for(var s=this.length(k),t=p(10,-i);;){var u;if(u=0!==s?g/s:0,ui.x+g/2,m=ei.x?f-d:f+d,c=g*g/(e-j)-g*g*(f-k)*(b-k)/(h*h*(e-j))+j):(c=f>i.y?e+d:e-d,b=h*h/(f-k)-h*h*(e-j)*(c-j)/(g*g*(f-k))+k),new u(c,b).theta(a)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLine:function(a){var b=[],c=a.start,d=a.end,e=this.a,f=this.b,g=a.vector(),i=c.difference(new u(this)),j=new u(g.x/(e*e),g.y/(f*f)),k=new u(i.x/(e*e),i.y/(f*f)),l=g.dot(j),m=g.dot(k),n=i.dot(k)-1,o=m*m-l*n;if(o<0)return null;if(o>0){var p=h(o),q=(-m-p)/l,r=(-m+p)/l;if((q<0||10){if(f>d||g>d)return null}else if(f=1?c.clone():b.lerp(c,a)},pointAtLength:function(a){var b=this.start,c=this.end;fromStart=!0,a<0&&(fromStart=!1,a=-a);var d=this.length();return a>=d?fromStart?c.clone():b.clone():this.pointAt((fromStart?a:d-a)/d)},pointOffset:function(a){a=new c.Point(a);var b=this.start,d=this.end,e=(d.x-b.x)*(a.y-b.y)-(d.y-b.y)*(a.x-b.x);return e/this.length()},rotate:function(a,b){return this.start.rotate(a,b),this.end.rotate(a,b),this},round:function(a){var b=p(10,a||0);return this.start.x=l(this.start.x*b)/b,this.start.y=l(this.start.y*b)/b,this.end.x=l(this.end.x*b)/b,this.end.y=l(this.end.y*b)/b,this},scale:function(a,b,c){return this.start.scale(a,b,c),this.end.scale(a,b,c),this},setLength:function(a){var b=this.length();if(!b)return this;var c=a/b;return this.scale(c,c,this.start)},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},tangentAt:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAt(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},tangentAtLength:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAtLength(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},translate:function(a,b){return this.start.translate(a,b),this.end.translate(a,b),this},vector:function(){return new u(this.end.x-this.start.x,this.end.y-this.start.y)},toString:function(){return this.start.toString()+" "+this.end.toString()}},s.prototype.intersection=s.prototype.intersect;var t=c.Path=function(a){if(!(this instanceof t))return new t(a);if("string"==typeof a)return new t.parse(a);this.segments=[];var b,c;if(a){if(Array.isArray(a)&&0!==a.length)if(c=a.length,a[0].isSegment)for(b=0;b=c||a<0)throw new Error("Index out of range.");return b[a]},getSegmentSubdivisions:function(a){var b=this.segments,c=b.length;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=[],f=0;fd||a<0)throw new Error("Index out of range.");var e,f=null,g=null;if(0!==d&&(a>=1?(f=c[a-1],g=f.nextSegment):g=c[0]),Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");for(var h=b.length,i=0;i=d?(e=d-1,f=1):f<0?f=0:f>1&&(f=1),b=b||{};for(var g,h=void 0===b.precision?this.PRECISION:b.precision,i=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:h}):b.segmentSubdivisions,j=0,k=0;k=1)return this.end.clone();b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.pointAtLength(i,g)},pointAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;if(0===a)return this.start.clone();var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isVisible){if(a<=i+m)return k.pointAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f)return e?f.end:f.start;var n=c[d-1];return n.end.clone()},pointAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].pointAtT(0);if(d>=c)return b[c-1].pointAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].pointAtT(e)},prepareSegment:function(a,b,c){a.previousSegment=b,a.nextSegment=c,b&&(b.nextSegment=a),c&&(c.previousSegment=a);var d=a;return a.isSubpathStart&&(a.subpathStartSegment=a,d=c),d&&this.updateSubpathStartSegment(d),a},PRECISION:3,removeSegment:function(a){var b=this.segments,c=b.length;if(0===c)throw new Error("Path has no segments.");if(a<0&&(a=c+a),a>=c||a<0)throw new Error("Index out of range.");var d=b.splice(a,1)[0],e=d.previousSegment,f=d.nextSegment;e&&(e.nextSegment=f),f&&(f.previousSegment=e),d.isSubpathStart&&f&&this.updateSubpathStartSegment(f)},replaceSegment:function(a,b){var c=this.segments,d=c.length;if(0===d)throw new Error("Path has no segments.");if(a<0&&(a=d+a),a>=d||a<0)throw new Error("Index out of range.");var e,f=c[a],g=f.previousSegment,h=f.nextSegment,i=f.isSubpathStart;if(Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");c.splice(a,1);for(var j=b.length,k=0;k1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.segmentIndexAtLength(i,g)},toPoints:function(a){var b=this.segments,c=b.length;if(0===c)return null;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=void 0===a.segmentSubdivisions?this.getSegmentSubdivisions({precision:d}):a.segmentSubdivisions,f=[],g=[],h=0;h0){var k=j.map(function(a){return a.start});Array.prototype.push.apply(g,k)}else g.push(i.start)}else g.length>0&&(g.push(b[h-1].end),f.push(g),g=[])}return g.length>0&&(g.push(this.end),f.push(g)),f},toPolylines:function(a){var b=[],c=this.toPoints(a);if(!c)return null;for(var d=0,e=c.length;d=0;e?j++:j--){var k=c[j],l=g[j],m=k.length({precision:f,subdivisions:l});if(k.isVisible){if(a<=i+m)return j;h=j}i+=m}return h},tangentAt:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;a<0&&(a=0),a>1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.tangentAtLength(i,g)},tangentAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isDifferentiable()){if(a<=i+m)return k.tangentAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f){var n=e?1:0;return f.tangentAtT(n)}return null},tangentAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].tangentAtT(0);if(d>=c)return b[c-1].tangentAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].tangentAtT(e)},translate:function(a,b){for(var c=this.segments,d=c.length,e=0;e=0;c--){var d=a[c];if(d.isVisible)return d.end}return a[b-1].end}});var u=c.Point=function(a,b){if(!(this instanceof u))return new u(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseFloat(c[0]),b=parseFloat(c[1])}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};u.fromPolar=function(a,b,c){c=c&&new u(c)||new u(0,0);var d=e(a*f(b)),h=e(a*g(b)),i=x(z(b));return i<90?h=-h:i<180?(d=-d,h=-h):i<270&&(d=-d),new u(c.x+d,c.y+h)},u.random=function(a,b,c,d){return new u(m(o()*(b-a+1)+a),m(o()*(d-c+1)+c))},u.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=i(j(this.x,a.x),a.x+a.width),this.y=i(j(this.y,a.y),a.y+a.height),this)},bearing:function(a){return new s(this,a).bearing()},changeInAngle:function(a,b,c){return this.clone().offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return new u(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),new u(this.x-(a||0),this.y-(b||0))},distance:function(a){return new s(this,a).length()},squaredDistance:function(a){return new s(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return h(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return e(a.x-this.x)+e(a.y-this.y)},move:function(a,b){var c=A(new u(a).theta(this)),d=this.offset(f(c)*b,-g(c)*b);return d},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return new u(a).move(this,this.distance(a))},rotate:function(a,b){a=a||new c.Point(0,0),b=A(x(-b));var d=f(b),e=g(b),h=d*(this.x-a.x)-e*(this.y-a.y)+a.x,i=e*(this.x-a.x)+d*(this.y-a.y)+a.y;return this.x=h,this.y=i,this},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this},scale:function(a,b,c){return c=c&&new u(c)||new u(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=y(this.x,a),this.y=y(this.y,b||a),this},theta:function(a){a=new u(a);var b=-(a.y-this.y),c=a.x-this.x,d=k(b,c);return d<0&&(d=2*n+d),180*d/n},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=new u(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&new u(a)||new u(0,0);var b=this.x,c=this.y;return this.x=h((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=A(a.theta(new u(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN},lerp:function(a,b){var c=this.x,d=this.y;return new u((1-b)*c+b*a.x,(1-b)*d+b*a.y)}},u.prototype.translate=u.prototype.offset;var v=c.Rect=function(a,b,c,d){return this instanceof v?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new v(a,b,c,d)};v.fromEllipse=function(a){return a=new r(a),new v(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},v.prototype={bbox:function(a){if(!a)return this.clone();var b=A(a||0),c=e(g(b)),d=e(f(b)),h=this.width*d+this.height*c,i=this.width*c+this.height*d;return new v(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return new u(this.x,this.y+this.height)},bottomLine:function(){return new s(this.bottomLeft(),this.bottomRight())},bottomMiddle:function(){ +return new u(this.x+this.width/2,this.y+this.height)},center:function(){return new u(this.x+this.width/2,this.y+this.height/2)},clone:function(){return new v(this)},containsPoint:function(a){return a=new u(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=new v(this).normalize(),c=new v(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return new u(this.x+this.width,this.y+this.height)},equals:function(a){var b=new v(this).normalize(),c=new v(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=j(b.x,d.x),g=j(b.y,d.y);return new v(f,g,i(c.x,e.x)-f,i(c.y,e.y)-g)},intersectionWithLine:function(a){var b,c,d=this,e=[d.topLine(),d.rightLine(),d.bottomLine(),d.leftLine()],f=[],g=[],h=e.length;for(c=0;c0?f:null},intersectionWithLineFromCenterToPoint:function(a,b){a=new u(a);var c,d=new u(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[this.topLine(),this.rightLine(),this.bottomLine(),this.leftLine()],f=new s(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return new s(this.topLeft(),this.bottomLeft())},leftMiddle:function(){return new u(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return u.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return new u(this.x,this.y)},pointNearestToPoint:function(a){if(a=new u(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return new u(this.x+this.width,a.y);case"left":return new u(this.x,a.y);case"bottom":return new u(a.x,this.y+this.height);case"top":return new u(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return new s(this.topRight(),this.bottomRight())},rightMiddle:function(){return new u(this.x+this.width,this.y+this.height/2)},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this.width=l(this.width*b)/b,this.height=l(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(a,b){a=new v(a),b||(b=a.center());var c,d,e,f,g,h,j,k,l=b.x,m=b.y;c=d=e=f=g=h=j=k=1/0;var n=a.topLeft();n.xl&&(d=(this.x+this.width-l)/(o.x-l)),o.y>m&&(h=(this.y+this.height-m)/(o.y-m));var p=a.topRight();p.x>l&&(e=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:i(c,d,e,f),sy:i(g,h,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return i(c.sx,c.sy)},sideNearestToPoint:function(a){a=new u(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cb&&(b=i),jd&&(d=j)}return new v(a,c,b-a,d-c)},clone:function(){var a=this.points,b=a.length;if(0===b)return new w;for(var c=[],d=0;df.x&&(f=c[a]);var g=[];for(a=0;a2){var j=g[g.length-1];g.unshift(j)}for(var k,l,m,n,o,p,q={},r=[];0!==g.length;)if(k=g.pop(),l=k[0],!q.hasOwnProperty(k[0]+"@@"+k[1]))for(var s=!1;!s;)if(r.length<2)r.push(k),s=!0;else{m=r.pop(),n=m[0],o=r.pop(),p=o[0];var t=p.cross(n,l);if(t<0)r.push(o),r.push(m),r.push(k),s=!0;else if(0===t){var u=1e-10,v=n.angleBetween(p,l);e(v-180)2&&r.pop();var x,y=-1;for(b=r.length,a=0;a0){var B=r.slice(y),C=r.slice(0,y);A=B.concat(C)}else A=r;var D=[];for(b=A.length,a=0;a=1)return b[c-1].clone();var d=this.length(),e=d*a;return this.pointAtLength(e)},pointAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return b[0].clone();var d=!0;a<0&&(d=!1,a=-a);for(var e=0,f=c-1,g=d?0:f-1;d?g=0;d?g++:g--){var h=b[g],i=b[g+1],j=new s(h,i),k=h.distance(i);if(a<=e+k)return j.pointAtLength((d?1:-1)*(a-e));e+=k}var l=d?b[c-1]:b[0];return l.clone()},scale:function(a,b,c){var d=this.points,e=d.length;if(0===e)return this;for(var f=0;f1&&(a=1);var d=this.length(),e=d*a;return this.tangentAtLength(e)},tangentAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return null;var d=!0;a<0&&(d=!1,a=-a);for(var e,f=0,g=c-1,h=d?0:g-1;d?h=0;d?h++:h--){var i=b[h],j=b[h+1],k=new s(i,j),l=i.distance(j);if(k.isDifferentiable()){if(a<=f+l)return k.tangentAtLength((d?1:-1)*(a-f));e=k}f+=l}if(e){var m=d?1:0;return e.tangentAt(m)}return null},intersectionWithLine:function(a){for(var b=new s(a),c=[],d=this.points,e=0,f=d.length-1;e0?c:null},translate:function(a,b){var c=this.points,d=c.length;if(0===d)return this;for(var e=0;e=1?b:b*a},pointAtT:function(a){if(this.pointAt)return this.pointAt(a);throw new Error("Neither pointAtT() nor pointAt() function is implemented.")},previousSegment:null,subpathStartSegment:null,tangentAtT:function(a){if(this.tangentAt)return this.tangentAt(a);throw new Error("Neither tangentAtT() nor tangentAt() function is implemented.")},bbox:function(){throw new Error("Declaration missing for virtual function.")},clone:function(){throw new Error("Declaration missing for virtual function.")},closestPoint:function(){throw new Error("Declaration missing for virtual function.")},closestPointLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointNormalizedLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointTangent:function(){throw new Error("Declaration missing for virtual function.")},equals:function(){throw new Error("Declaration missing for virtual function.")},getSubdivisions:function(){throw new Error("Declaration missing for virtual function.")},isDifferentiable:function(){throw new Error("Declaration missing for virtual function.")},length:function(){throw new Error("Declaration missing for virtual function.")},pointAt:function(){throw new Error("Declaration missing for virtual function.")},pointAtLength:function(){throw new Error("Declaration missing for virtual function.")},scale:function(){throw new Error("Declaration missing for virtual function.")},tangentAt:function(){throw new Error("Declaration missing for virtual function.")},tangentAtLength:function(){throw new Error("Declaration missing for virtual function.")},translate:function(){throw new Error("Declaration missing for virtual function.")},serialize:function(){throw new Error("Declaration missing for virtual function.")},toString:function(){throw new Error("Declaration missing for virtual function.")}},C=function(){for(var b=[],c=arguments.length,d=0;d0)throw new Error("Closepath constructor expects no arguments.");return this},J={clone:function(){return new I},getSubdivisions:function(){return[]},isDifferentiable:function(){return!(!this.previousSegment||!this.subpathStartSegment)&&!this.start.equals(this.end)},scale:function(){return this},translate:function(){return this},type:"Z",serialize:function(){return this.type},toString:function(){return this.type+" "+this.start+" "+this.end}};Object.defineProperty(J,"start",{configurable:!0,enumerable:!0,get:function(){if(!this.previousSegment)throw new Error("Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)");return this.previousSegment.end}}),Object.defineProperty(J,"end",{configurable:!0,enumerable:!0,get:function(){if(!this.subpathStartSegment)throw new Error("Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)");return this.subpathStartSegment.end}}),I.prototype=b(B,s.prototype,J);var K=t.segmentTypes={L:C,C:E,M:G,Z:I,z:I};return t.regexSupportedData=new RegExp("^[\\s\\d"+Object.keys(K).join("")+",.]*$"),t.isDataSupported=function(a){return"string"==typeof a&&this.regexSupportedData.test(a)},c}(); return g; diff --git a/dist/joint.core.css b/dist/joint.core.css index c48c4165e..68e71b8aa 100644 --- a/dist/joint.core.css +++ b/dist/joint.core.css @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -64,12 +64,13 @@ the whole group of elements. Each plugin can provide its own stylesheet. } .joint-element * { - /* The default behavior when scaling an element is not to scale the stroke in order to prevent the ugly effect of stroke with different proportions. */ - vector-effect: non-scaling-stroke; user-drag: none; } - +.joint-element .scalable * { + /* The default behavior when scaling an element is not to scale the stroke in order to prevent the ugly effect of stroke with different proportions. */ + vector-effect: non-scaling-stroke; +} /* connection-wrap is a element of the joint.dia.Link that follows the .connection of that link. diff --git a/dist/joint.core.js b/dist/joint.core.js index c3fe0fbe2..2e0a70991 100644 --- a/dist/joint.core.js +++ b/dist/joint.core.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -435,8 +435,8 @@ Number.isNaN = Number.isNaN || function(value) { } - -// Geometry library. +// Geometry library. +// ----------------- var g = (function() { @@ -448,8 +448,8 @@ var g = (function() { var cos = math.cos; var sin = math.sin; var sqrt = math.sqrt; - var mmin = math.min; - var mmax = math.max; + var min = math.min; + var max = math.max; var atan2 = math.atan2; var round = math.round; var floor = math.floor; @@ -460,27 +460,25 @@ var g = (function() { 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). + // @deprecated // @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); - } + console.warn('deprecated'); - return path; + return new Path(Curve.throughPoints(points)).serialize(); }, // Get open-ended Bezier Spline Control Points. + // @deprecated // @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) { + console.warn('deprecated'); + var firstControlPoints = []; var secondControlPoints = []; var n = knots.length - 1; @@ -489,11 +487,17 @@ var g = (function() { // 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); + firstControlPoints[0] = new 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); + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); + return [firstControlPoints, secondControlPoints]; } @@ -505,8 +509,10 @@ var g = (function() { 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); @@ -514,50 +520,44 @@ var g = (function() { 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])); + firstControlPoints.push(new 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])); + secondControlPoints.push(new 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)); + secondControlPoints.push(new 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 }]; - }; + return [firstControlPoints, secondControlPoints]; }, // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @deprecated // @param rhs Right hand side vector. // @return Solution vector. getFirstControlPoints: function(rhs) { + console.warn('deprecated'); + var n = rhs.length; // `x` is a solution vector. var x = []; @@ -565,870 +565,981 @@ var g = (function() { 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; }, + // Divide a Bezier curve into two at point defined by value 't' <0,1>. + // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 + // @deprecated + // @param control points (start, control start, control end, end) + // @return a function that accepts t and returns 2 curves. + getCurveDivider: function(p0, p1, p2, p3) { + + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + + return function divideCurve(t) { + + var divided = curve.divide(t); + + return [{ + p0: divided[0].start, + p1: divided[0].controlPoint1, + p2: divided[0].controlPoint2, + p3: divided[0].end + }, { + p0: divided[1].start, + p1: divided[1].controlPoint1, + p2: divided[1].controlPoint2, + p3: divided[1].end + }]; + }; + }, + // 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. + // @deprecated // @param control points (start, control start, control end, end) - // @return a function accepts a point and returns t. + // @return a function that 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; - }; - } + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + 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); + + return curve.closestPointT(p); }; } }; - var Ellipse = g.Ellipse = function(c, a, b) { + var Curve = g.Curve = function(p1, p2, p3, p4) { - if (!(this instanceof Ellipse)) { - return new Ellipse(c, a, b); + if (!(this instanceof Curve)) { + return new Curve(p1, p2, p3, p4); } - if (c instanceof Ellipse) { - return new Ellipse(Point(c), c.a, c.b); + if (p1 instanceof Curve) { + return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end); } - c = Point(c); - this.x = c.x; - this.y = c.y; - this.a = a; - this.b = b; + this.start = new Point(p1); + this.controlPoint1 = new Point(p2); + this.controlPoint2 = new Point(p3); + this.end = new Point(p4); }; - g.Ellipse.fromRect = function(rect) { + // Curve passing 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} curves. + Curve.throughPoints = (function() { - rect = Rect(rect); - return Ellipse(rect.center(), rect.width / 2, rect.height / 2); - }; + // 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. + function getFirstControlPoints(rhs) { - g.Ellipse.prototype = { + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; - bbox: function() { + x[0] = rhs[0] / b; - return Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.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; + } - clone: function() { + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } - return Ellipse(this); - }, + return x; + } - /** - * @param {g.Point} point - * @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside - */ - normalizedDistance: function(point) { + // 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. + function getCurveControlPoints(knots) { - var x0 = point.x; - var y0 = point.y; - var a = this.a; - var b = this.b; - var x = this.x; - var y = this.y; + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; - return ((x0 - x) * (x0 - x)) / (a * a ) + ((y0 - y) * (y0 - y)) / (b * b); - }, + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new Point( + (2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3 + ); - // 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; - } + // P2 = 2P1 – P0 + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); - if (dy === undefined) { - dy = dx; + return [firstControlPoints, secondControlPoints]; } - this.a += 2 * dx; - this.b += 2 * dy; - - return this; - }, + // 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; + } - /** - * @param {g.Point} p - * @returns {boolean} - */ - containsPoint: function(p) { + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - return this.normalizedDistance(p) <= 1; - }, + // Get first control points X-values. + var x = getFirstControlPoints(rhs); - /** - * @returns {g.Point} - */ - center: function() { + // Set right hand side Y values. + for (i = 1; i < n - 1; ++i) { + rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; + } - return Point(this.x, this.y); - }, + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - /** 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) { + // Get first control points Y-values. + var y = getFirstControlPoints(rhs); - 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; + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(new Point(x[i], y[i])); - var q1 = x0 > center.x + a / 2; - var q3 = x0 < center.x - a / 2; + // Second control point. + if (i < n - 1) { + secondControlPoints.push(new Point( + 2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1] + )); - 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; + } else { + secondControlPoints.push(new Point( + (knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2 + )); + } } - return g.point(x, y).theta(p); + return [firstControlPoints, secondControlPoints]; + } - }, + return function(points) { - equals: function(ellipse) { + if (!points || (Array.isArray(points) && points.length < 2)) { + throw new Error('At least 2 points are required'); + } - return !!ellipse && - ellipse.x === this.x && - ellipse.y === this.y && - ellipse.a === this.a && - ellipse.b === this.b; - }, + var controlPoints = getCurveControlPoints(points); - // 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) { + var curves = []; + var n = controlPoints[0].length; + for (var i = 0; i < n; i++) { - 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 controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y); + var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y); + + curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1])); } - 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; - }, + return curves; + }; + })(); - toString: function() { + Curve.prototype = { - return Point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; - } - }; + // Returns a bbox that tightly envelops the curve. + bbox: function() { - var Line = g.Line = function(p1, p2) { + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; - if (!(this instanceof Line)) { - return new Line(p1, p2); - } + var x0 = start.x; + var y0 = start.y; + var x1 = controlPoint1.x; + var y1 = controlPoint1.y; + var x2 = controlPoint2.x; + var y2 = controlPoint2.y; + var x3 = end.x; + var y3 = end.y; - if (p1 instanceof Line) { - return Line(p1.start, p1.end); - } + var points = new Array(); // local extremes + var tvalues = new Array(); // t values of local extremes + var bounds = [new Array(), new Array()]; - this.start = Point(p1); - this.end = Point(p2); - }; + var a, b, c, t; + var t1, t2; + var b2ac, sqrtb2ac; - g.Line.prototype = { + for (var i = 0; i < 2; ++i) { - // @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() { + if (i === 0) { + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; - 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)); + } else { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } - var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']; + if (abs(a) < 1e-12) { // Numerical robustness + if (abs(b) < 1e-12) { // Numerical robustness + continue; + } - var index = brng - 22.5; - if (index < 0) - index += 360; - index = parseInt(index / 45); + t = -c / b; + if ((0 < t) && (t < 1)) tvalues.push(t); - return bearings[index]; - }, + continue; + } - clone: function() { + b2ac = b * b - 4 * c * a; + sqrtb2ac = sqrt(b2ac); - return Line(this.start, this.end); - }, + if (b2ac < 0) continue; - equals: function(l) { + t1 = (-b + sqrtb2ac) / (2 * a); + if ((0 < t1) && (t1 < 1)) tvalues.push(t1); - return !!l && - this.start.x === l.start.x && - this.start.y === l.start.y && - this.end.x === l.end.x && - this.end.y === l.end.y; - }, + t2 = (-b - sqrtb2ac) / (2 * a); + if ((0 < t2) && (t2 < 1)) tvalues.push(t2); + } - // @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) - ); + var j = tvalues.length; + var jlen = j; + var mt; + var x, y; - } else if (l instanceof Rect) { - // Passed in parameter is a rectangle. + while (j--) { + t = tvalues[j]; + mt = 1 - t; - var r = l; - var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; - var points = []; - var dedupeArr = []; - var pt, i; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; - 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()); - } - } + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; - return points.length > 0 ? points : null; + points[j] = { X: x, Y: y }; } - // Passed in parameter is neither a Line nor a Rectangle. - return null; - }, + tvalues[jlen] = 0; + tvalues[jlen + 1] = 1; - // @return {double} length of the line - length: function() { - return sqrt(this.squaredLength()); - }, + points[jlen] = { X: x0, Y: y0 }; + points[jlen + 1] = { X: x3, Y: y3 }; - // @return {point} my midpoint - midpoint: function() { - return Point( - (this.start.x + this.end.x) / 2, - (this.start.y + this.end.y) / 2 - ); - }, + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; - // @return {point} my point at 't' <0,1> - pointAt: function(t) { + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; - 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); - }, + tvalues.length = jlen + 2; + bounds[0].length = jlen + 2; + bounds[1].length = jlen + 2; + points.length = jlen + 2; - // @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) { + var left = min.apply(null, bounds[0]); + var top = min.apply(null, bounds[1]); + var right = max.apply(null, bounds[0]); + var bottom = max.apply(null, bounds[1]); - // 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 new Rect(left, top, (right - left), (bottom - top)); }, - // @return vector {point} of the line - vector: function() { + clone: function() { - return Point(this.end.x - this.start.x, this.end.y - this.start.y); + return new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); }, - // @return {point} the closest point on the line to point `p` - closestPoint: function(p) { + // Returns the point on the curve closest to point `p` + closestPoint: function(p, opt) { - return this.pointAt(this.closestPointNormalizedLength(p)); + return this.pointAtT(this.closestPointT(p, opt)); }, - // @return {number} the normalized length of the closest point on the line to point `p` - closestPointNormalizedLength: function(p) { + closestPointLength: function(p, opt) { - var product = this.vector().dot(Line(this.start, p).vector()); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - return Math.min(1, Math.max(0, product / this.squaredLength())); + return this.lengthAtT(this.closestPointT(p, localOpt), localOpt); }, - // @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; + closestPointNormalizedLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + var cpLength = this.closestPointLength(p, localOpt); + if (!cpLength) return 0; + + var length = this.length(localOpt); + if (length === 0) return 0; + + return cpLength / length; }, - toString: function() { - return this.start.toString() + ' ' + this.end.toString(); - } - }; + // Returns `t` of the point on the curve closest to point `p` + closestPointT: function(p, opt) { - // For backwards compatibility: - g.Line.prototype.intersection = g.Line.prototype.intersect; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // does not use localOpt + + // identify the subdivision that contains the point: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + var distFromStart; // distance of point from start of baseline + var distFromEnd; // distance of point from end of baseline + var minSumDist; // lowest observed sum of the two distances + var n = subdivisions.length; + var subdivisionSize = (n ? (1 / n) : 0); + for (var i = 0; i < n; i++) { - /* - Point is the most basic object consisting of x/y coordinate. + var currentSubdivision = subdivisions[i]; - Possible instantiations are: - * `Point(10, 20)` - * `new Point(10, 20)` - * `Point('10 20')` - * `Point(Point(10, 20))` - */ - var Point = g.Point = function(x, y) { + var startDist = currentSubdivision.start.distance(p); + var endDist = currentSubdivision.end.distance(p); + var sumDist = startDist + endDist; - if (!(this instanceof Point)) { - return new Point(x, y); - } + // check that the point is closest to current subdivision and not any other + if (!minSumDist || (sumDist < minSumDist)) { + investigatedSubdivision = currentSubdivision; - 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; - } + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - }; + distFromStart = startDist; + distFromEnd = endDist; - // Alternative constructor, from polar coordinates. - // @param {number} Distance. - // @param {number} Angle in radians. - // @param {point} [optional] Origin. - g.Point.fromPolar = function(distance, angle, origin) { + minSumDist = sumDist; + } + } - origin = (origin && Point(origin)) || Point(0, 0); - var x = abs(distance * cos(angle)); - var y = abs(distance * sin(angle)); - var deg = normalizeAngle(toDeg(angle)); + var precisionRatio = pow(10, -precision); - if (deg < 90) { - y = -y; - } else if (deg < 180) { - x = -x; - y = -y; - } else if (deg < 270) { - x = -x; - } + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(origin.x + x, origin.y + y); - }; + // check if we have reached required observed precision + var startPrecisionRatio; + var endPrecisionRatio; - // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. - g.Point.random = function(x1, x2, y1, y2) { + startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0); + endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0); + if ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio) < precisionRatio) { + return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT); + } - return Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); - }; + // otherwise, set up for next iteration + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - g.Point.prototype = { + var startDist1 = divided[0].start.distance(p); + var endDist1 = divided[0].end.distance(p); + var sumDist1 = startDist1 + endDist1; - // 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) { + var startDist2 = divided[1].start.distance(p); + var endDist2 = divided[1].end.distance(p); + var sumDist2 = startDist2 + endDist2; - if (r.containsPoint(this)) { - return this; - } + if (sumDist1 <= sumDist2) { + investigatedSubdivision = divided[0]; - 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; - }, + investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved - // Return the bearing between me and the given point. - bearing: function(point) { + distFromStart = startDist1; + distFromEnd = endDist1; - return Line(this, point).bearing(); - }, + } else { + investigatedSubdivision = divided[1]; - // Returns change in angle from my previous position (-dx, -dy) to my new position - // relative to ref point. - changeInAngle: function(dx, dy, ref) { + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - // Revert the translation and measure the change in angle around x-axis. - return Point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); + distFromStart = startDist2; + distFromEnd = endDist2; + } + } }, - clone: function() { + closestPointTangent: function(p, opt) { - return Point(this); + return this.tangentAtT(this.closestPointT(p, opt)); }, - difference: function(dx, dy) { + // Divides the curve into two at point defined by `t` between 0 and 1. + // Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867). + // Additional resource: https://pomax.github.io/bezierinfo/#decasteljau + divide: function(t) { - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return [ + new Curve(start, start, start, start), + new Curve(start, controlPoint1, controlPoint2, end) + ]; } - return Point(this.x - (dx || 0), this.y - (dy || 0)); - }, + if (t >= 1) { + return [ + new Curve(start, controlPoint1, controlPoint2, end), + new Curve(end, end, end, end) + ]; + } - // Returns distance between me and point `p`. - distance: function(p) { + var dividerPoints = this.getSkeletonPoints(t); + + var startControl1 = dividerPoints.startControlPoint1; + var startControl2 = dividerPoints.startControlPoint2; + var divider = dividerPoints.divider; + var dividerControl1 = dividerPoints.dividerControlPoint1; + var dividerControl2 = dividerPoints.dividerControlPoint2; - return Line(this, p).length(); + // return array with two new curves + return [ + new Curve(start, startControl1, startControl2, divider), + new Curve(divider, dividerControl1, dividerControl2, end) + ]; }, - squaredDistance: function(p) { + // Returns the distance between the curve's start and end points. + endpointDistance: function() { - return Line(this, p).squaredLength(); + return this.start.distance(this.end); }, - equals: function(p) { - - return !!p && this.x === p.x && this.y === p.y; + // Checks whether two curves are exactly the same. + equals: function(c) { + + return !!c && + this.start.x === c.start.x && + this.start.y === c.start.y && + this.controlPoint1.x === c.controlPoint1.x && + this.controlPoint1.y === c.controlPoint1.y && + this.controlPoint2.x === c.controlPoint2.x && + this.controlPoint2.y === c.controlPoint2.y && + this.end.x === c.end.x && + this.end.y === c.end.y; }, - magnitude: function() { + // Returns five helper points necessary for curve division. + getSkeletonPoints: function(t) { + + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return { + startControlPoint1: start.clone(), + startControlPoint2: start.clone(), + divider: start.clone(), + dividerControlPoint1: control1.clone(), + dividerControlPoint2: control2.clone() + }; + } - return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; - }, + if (t >= 1) { + return { + startControlPoint1: control1.clone(), + startControlPoint2: control2.clone(), + divider: end.clone(), + dividerControlPoint1: end.clone(), + dividerControlPoint2: end.clone() + }; + } - // Returns a manhattan (taxi-cab) distance between me and point `p`. - manhattanDistance: function(p) { + var midpoint1 = (new Line(start, control1)).pointAt(t); + var midpoint2 = (new Line(control1, control2)).pointAt(t); + var midpoint3 = (new Line(control2, end)).pointAt(t); - return abs(p.x - this.x) + abs(p.y - this.y); - }, + var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t); + var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t); - // Move point on line starting from ref ending at me by - // distance distance. - move: function(ref, distance) { + var divider = (new Line(subControl1, subControl2)).pointAt(t); + + var output = { + startControlPoint1: midpoint1, + startControlPoint2: subControl1, + divider: divider, + dividerControlPoint1: subControl2, + dividerControlPoint2: midpoint3 + }; - var theta = toRad(Point(ref).theta(this)); - return this.offset(cos(theta) * distance, -sin(theta) * distance); + return output; }, - // 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) { + // Returns a list of curves whose flattened length is better than `opt.precision`. + // That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1% + // (Observed difference is not real precision, but close enough as long as special cases are covered) + // (That is why skipping iteration 1 is important) + // As a rule of thumb, increasing `precision` by 1 requires two more division operations + // - Precision 0 (endpointDistance) - total of 2^0 - 1 = 0 operations (1 subdivision) + // - Precision 1 (<10% error) - total of 2^2 - 1 = 3 operations (4 subdivisions) + // - Precision 2 (<1% error) - total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions) + // - Precision 3 (<0.1% error) - total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions) + // - Precision 4 (<0.01% error) - total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions) + // (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly) + getSubdivisions: function(opt) { - var scale = (length || 1) / this.magnitude(); - return this.scale(scale, scale); - }, + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt - // Offset me by the specified amount. - offset: function(dx, dy) { + var subdivisions = [new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end)]; + if (precision === 0) return subdivisions; - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; - } + var previousLength = this.endpointDistance(); - this.x += dx || 0; - this.y += dy || 0; - return this; - }, + var precisionRatio = pow(10, -precision); - // Returns a point that is the reflection of me with - // the center of inversion in ref point. - reflection: function(ref) { + // recursively divide curve at `t = 0.5` + // until the difference between observed length at subsequent iterations is lower than precision + var iteration = 0; + while (true) { + iteration += 1; - return Point(ref).move(this, this.distance(ref)); - }, + // divide all subdivisions + var newSubdivisions = []; + var numSubdivisions = subdivisions.length; + for (var i = 0; i < numSubdivisions; i++) { - // Rotate point by angle around origin. - rotate: function(origin, angle) { + var currentSubdivision = subdivisions[i]; + var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!) + newSubdivisions.push(divided[0], divided[1]); + } - 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; - }, + // measure new length + var length = 0; + var numNewSubdivisions = newSubdivisions.length; + for (var j = 0; j < numNewSubdivisions; j++) { - round: function(precision) { + var currentNewSubdivision = newSubdivisions[j]; + length += currentNewSubdivision.endpointDistance(); + } - var f = pow(10, precision || 0); - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - return this; + // check if we have reached required observed precision + // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1 + // not a problem for further iterations because cubic curves cannot have more than two local extrema + // (i.e. cubic curves cannot intersect the baseline more than once) + // therefore two subsequent iterations cannot produce sampling with equal length + var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0); + if (iteration > 1 && observedPrecisionRatio < precisionRatio) { + return newSubdivisions; + } + + // otherwise, set up for next iteration + subdivisions = newSubdivisions; + previousLength = length; + } }, - // Scale point with origin. - scale: function(sx, sy, origin) { + isDifferentiable: function() { - 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; + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); }, - snapToGrid: function(gx, gy) { + // Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided. + length: function(opt) { - this.x = snapToGrid(this.x, gx); - this.y = snapToGrid(this.y, gy || gx); - return this; - }, + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt - // Compute the angle between me and `p` and the x axis. - // (cartesian-to-polar coordinates conversion) - // Return theta angle in degrees. - theta: function(p) { + var length = 0; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { - p = Point(p); - // Invert the y-axis. - var y = -(p.y - this.y); - var x = p.x - this.x; - var rad = atan2(y, x); // defined for all 0 corner cases - - // Correction for III. and IV. quadrant. - if (rad < 0) { - rad = 2 * PI + rad; + var currentSubdivision = subdivisions[i]; + length += currentSubdivision.endpointDistance(); } - return 180 * rad / PI; - }, - // Compute the angle between vector from me to p1 and the vector from me to p2. - // ordering of points p1 and p2 is important! - // theta function's angle convention: - // returns angles between 0 and 180 when the angle is counterclockwise - // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones - // returns NaN if any of the points p1, p2 is coincident with this point - angleBetween: function(p1, p2) { - - var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - if (angleBetween < 0) { - angleBetween += 360; // correction to keep angleBetween between 0 and 360 - } - return angleBetween; + return length; }, - // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. - // Returns NaN if p is at 0,0. - vectorAngle: function(p) { - - var zero = Point(0,0); - return zero.angleBetween(this, p); - }, + // Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.) + lengthAtT: function(t, opt) { - toJSON: function() { + if (t <= 0) return 0; - return { x: this.x, y: this.y }; - }, + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt - // Converts rectangular to polar coordinates. - // An origin can be specified, otherwise it's 0@0. - toPolar: function(o) { + var subCurve = this.divide(t)[0]; + var subCurveLength = subCurve.length({ precision: precision }); - 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; + return subCurveLength; }, - toString: function() { + // Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + // Mirrors Line.pointAt() function. + // For a function that tracks `t`, use Curve.pointAtT(). + pointAt: function(ratio, opt) { - return this.x + '@' + this.y; + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + var t = this.tAt(ratio, opt); + + return this.pointAtT(t); }, - update: function(x, y) { + // Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + pointAtLength: function(length, opt) { - this.x = x || 0; - this.y = y || 0; - return this; + var t = this.tAtLength(length, opt); + + return this.pointAtT(t); }, - // Returns the dot product of this point with given other point - dot: function(p) { + // Returns the point at provided `t` between 0 and 1. + // `t` does not track distance along curve as it does in Line objects. + // Non-linear relationship, speeds up and slows down as curve warps! + // For linear length-based solution, use Curve.pointAt(). + pointAtT: function(t) { - return p ? (this.x * p.x + this.y * p.y) : NaN; + if (t <= 0) return this.start.clone(); + if (t >= 1) return this.end.clone(); + + return this.getSkeletonPoints(t).divider; }, - // Returns the cross product of this point relative to two other points - // this point is the common point - // point p1 lies on the first vector, point p2 lies on the second vector - // watch out for the ordering of points p1 and p2! - // positive result indicates a clockwise ("right") turn from first to second vector - // negative result indicates a counterclockwise ("left") turn from first to second vector - // note that the above directions are reversed from the usual answer on the Internet - // that is because we are in a left-handed coord system (because the y-axis points downward) - cross: function(p1, p2) { + // Default precision + PRECISION: 3, - return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; - } - }; + scale: function(sx, sy, origin) { - var Rect = g.Rect = function(x, y, w, h) { + this.start.scale(sx, sy, origin); + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - if (!(this instanceof Rect)) { - return new Rect(x, y, w, h); - } + // Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAt: function(ratio, opt) { - if ((Object(x) === x)) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } + if (!this.isDifferentiable()) return null; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - this.width = w === undefined ? 0 : w; - this.height = h === undefined ? 0 : h; - }; + if (ratio < 0) ratio = 0; + else if (ratio > 1) ratio = 1; - g.Rect.fromEllipse = function(e) { + var t = this.tAt(ratio, opt); - e = Ellipse(e); - return Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); - }; + return this.tangentAtT(t); + }, - g.Rect.prototype = { + // Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAtLength: function(length, opt) { - // 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) { + if (!this.isDifferentiable()) return null; - 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); + var t = this.tAtLength(length, opt); + + return this.tangentAtT(t); }, - bottomLeft: function() { + // Returns a tangent line at requested `t`. + tangentAtT: function(t) { - return Point(this.x, this.y + this.height); - }, + if (!this.isDifferentiable()) return null; - bottomLine: function() { + if (t < 0) t = 0; + else if (t > 1) t = 1; - return Line(this.bottomLeft(), this.corner()); - }, + var skeletonPoints = this.getSkeletonPoints(t); - bottomMiddle: function() { + var p1 = skeletonPoints.startControlPoint2; + var p2 = skeletonPoints.dividerControlPoint1; - return Point(this.x + this.width / 2, this.y + this.height); - }, + var tangentStart = skeletonPoints.divider; - center: function() { + var tangentLine = new Line(p1, p2); + tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested - return Point(this.x + this.width / 2, this.y + this.height / 2); + return tangentLine; }, - clone: function() { + // Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + tAt: function(ratio, opt) { - return Rect(this); - }, + if (ratio <= 0) return 0; + if (ratio >= 1) return 1; - // @return {bool} true if point p is insight me - containsPoint: function(p) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - p = Point(p); - return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + var curveLength = this.length(localOpt); + var length = curveLength * ratio; + + return this.tAtLength(length, localOpt); }, - // @return {bool} true if rectangle `r` is inside me. - containsRect: function(r) { + // Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + // Uses `precision` to approximate length within `precision` (always underestimates) + // Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated + // As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper + // - Precision 0 (chooses one of the two endpoints) - 0 levels + // - Precision 1 (chooses one of 5 points, <10% error) - 1 level + // - Precision 2 (<1% error) - 3 levels + // - Precision 3 (<0.1% error) - 7 levels + // - Precision 4 (<0.01% error) - 15 levels + tAtLength: function(length, opt) { - 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; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - if (!w0 || !h0 || !w1 || !h1) { - // At least one of the dimensions is 0 - return false; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + // identify the subdivision that contains the point at requested `length`: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + //var baseline; // straightened version of subdivision to investigate + //var baselinePoint; // point on the baseline that is the requested distance away from start + var baselinePointDistFromStart; // distance of baselinePoint from start of baseline + var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline + var l = 0; // length so far + var n = subdivisions.length; + var subdivisionSize = 1 / n; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var currentSubdivision = subdivisions[i]; + var d = currentSubdivision.endpointDistance(); // length of current subdivision + + if (length <= (l + d)) { + investigatedSubdivision = currentSubdivision; + + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; + + baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length)); + baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l)); + + break; + } + + l += d; } - var x0 = r0.x; - var y0 = r0.y; - var x1 = r1.x; - var y1 = r1.y; + if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t + // note that precision affects what length is recorded + // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length) + // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1 - w1 += x1; - w0 += x0; - h1 += y1; - h0 += y0; + var curveLength = this.length(localOpt); - return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; - }, + var precisionRatio = pow(10, -precision); - corner: function() { + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(this.x + this.width, this.y + this.height); - }, + // check if we have reached required observed precision + var observedPrecisionRatio; - // @return {boolean} true if rectangles are equal. - equals: function(r) { + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT; + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT; - 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; - }, + // otherwise, set up for next iteration + var newBaselinePointDistFromStart; + var newBaselinePointDistFromEnd; - // @return {rect} if rectangles intersect, {null} if not. - intersect: function(r) { + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = r.origin(); - var rCorner = r.corner(); + var baseline1Length = divided[0].endpointDistance(); + var baseline2Length = divided[1].endpointDistance(); - // No intersection found - if (rCorner.x <= myOrigin.x || - rCorner.y <= myOrigin.y || - rOrigin.x >= myCorner.x || - rOrigin.y >= myCorner.y) return null; + if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0] + investigatedSubdivision = divided[0]; + + investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart; + newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart; + + } else { // point at requested length is inside divided[1] + investigatedSubdivision = divided[1]; - var x = Math.max(myOrigin.x, rOrigin.x); - var y = Math.max(myOrigin.y, rOrigin.y); + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - return Rect(x, y, Math.min(myCorner.x, rCorner.x) - x, Math.min(myCorner.y, rCorner.y) - y); + newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length; + newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart; + } + + baselinePointDistFromStart = newBaselinePointDistFromStart; + baselinePointDistFromEnd = newBaselinePointDistFromEnd; + } }, - // 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) { + translate: function(tx, ty) { - p = Point(p); - var center = Point(this.x + this.width / 2, this.y + this.height / 2); - var result; - if (angle) p.rotate(center, angle); + this.start.translate(tx, ty); + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, - // (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); + // Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPoints: function(opt) { - for (var i = sides.length - 1; i >= 0; --i) { - var intersection = sides[i].intersection(connector); - if (intersection !== null) { - result = intersection; - break; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt + + var points = [subdivisions[0].start.clone()]; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + points.push(currentSubdivision.end.clone()); } - if (result && angle) result.rotate(center, -angle); - return result; + + return points; }, - leftLine: function() { + // Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPolyline: function(opt) { - return Line(this.origin(), this.bottomLeft()); + return new Polyline(this.toPoints(opt)); }, - leftMiddle: function() { + toString: function() { + + return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; + + 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(new Point(c.x, c.y), c.a, c.b); + } + + c = new Point(c); + this.x = c.x; + this.y = c.y; + this.a = a; + this.b = b; + }; + + Ellipse.fromRect = function(rect) { + + rect = new Rect(rect); + return new Ellipse(rect.center(), rect.width / 2, rect.height / 2); + }; + + Ellipse.prototype = { - return Point(this.x , this.y + this.height / 2); + bbox: function() { + + return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); }, - // Move and expand me. - // @param r {rectangle} representing deltas - moveAndExpand: function(r) { + clone: function() { - this.x += r.x || 0; - this.y += r.y || 0; - this.width += r.width || 0; - this.height += r.height || 0; - return this; + return new Ellipse(this); }, - // Offset me by the specified amount. - offset: function(dx, dy) { - return Point.prototype.offset.call(this, dx, dy); + /** + * @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, recompute origin [x, y] + // 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 @@ -1441,14691 +1552,23047 @@ var g = (function() { dy = dx; } - this.x -= dx; - this.y -= dy; - this.width += 2 * dx; - this.height += 2 * dy; + this.a += 2 * dx; + this.b += 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() { + /** + * @param {g.Point} p + * @returns {boolean} + */ + containsPoint: function(p) { - return Point(this.x, this.y); + return this.normalizedDistance(p) <= 1; }, - // @return {point} a point on my boundary nearest to the given point. - // @see Squeak Smalltalk, Rectangle>>pointNearestTo: - pointNearestToPoint: function(point) { + /** + * @returns {g.Point} + */ + center: function() { - 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); + return new Point(this.x, this.y); }, - rightLine: function() { + /** 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) { - return Line(this.topRight(), this.corner()); - }, + 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; - rightMiddle: function() { + var q1 = x0 > center.x + a / 2; + var q3 = x0 < center.x - a / 2; - return Point(this.x + this.width, this.y + this.height / 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; - round: function(precision) { + } 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 (new Point(x, y)).theta(p); - 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) { + equals: function(ellipse) { - origin = this.origin().scale(sx, sy, origin); - this.x = origin.x; - this.y = origin.y; - this.width *= sx; - this.height *= sy; - return this; + return !!ellipse && + ellipse.x === this.x && + ellipse.y === this.y && + ellipse.a === this.a && + ellipse.b === this.b; }, - maxRectScaleToFit: function(rect, origin) { - - rect = g.Rect(rect); - origin || (origin = rect.center()); + intersectionWithLine: function(line) { - var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; - var ox = origin.x; - var oy = origin.y; + var intersections = []; + var a1 = line.start; + var a2 = line.end; + var rx = this.a; + var ry = this.b; + var dir = line.vector(); + var diff = a1.difference(new Point(this)); + var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)); + var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)); - // 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. + var a = dir.dot(mDir); + var b = dir.dot(mDiff); + var c = diff.dot(mDiff) - 1.0; + var d = b * b - a * c; - sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; + if (d < 0) { + return null; + } else if (d > 0) { + var root = sqrt(d); + var ta = (-b - root) / a; + var tb = (-b + root) / a; - // 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 ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) { + // if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside + return null; + } else { + if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta)); + if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb)); + } + } else { + var t = -b / a; + if (0 <= t && t <= 1) { + intersections.push(a1.lerp(a2, t)); + } else { + // outside + return null; + } } - if (p4.y > oy) { - sy4 = (this.y + this.height - oy) / (p4.y - oy); + + return intersections; + }, + + // 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 = new Point(p); + + if (angle) p.rotate(new 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(new Point(this.x, this.y), -angle); + return result; } - return { - sx: Math.min(sx1, sx2, sx3, sx4), - sy: Math.min(sy1, sy2, sy3, sy4) - }; + 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 = new Point(this.x + x, this.y + y); + + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; }, - maxRectUniformScaleToFit: function(rect, origin) { + toString: function() { - var scale = this.maxRectScaleToFit(rect, origin); - return Math.min(scale.sx, scale.sy); + return (new 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); + } + + if (p1 instanceof Line) { + return new Line(p1.start, p1.end); + } + + this.start = new Point(p1); + this.end = new Point(p2); + }; + + Line.prototype = { + + bbox: function() { + + var left = min(this.start.x, this.end.x); + var top = min(this.start.y, this.end.y); + var right = max(this.start.x, this.end.x); + var bottom = max(this.start.y, this.end.y); + + return new Rect(left, top, (right - left), (bottom - top)); }, - // @return {string} (left|right|top|bottom) side which is nearest to point - // @see Squeak Smalltalk, Rectangle>>sideNearestTo: - sideNearestToPoint: function(point) { + // @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() { - 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'; + 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)); - if (distToRight < closest) { - closest = distToRight; - side = 'right'; - } - if (distToTop < closest) { - closest = distToTop; - side = 'top'; - } - if (distToBottom < closest) { - closest = distToBottom; - side = 'bottom'; - } - return side; + 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]; }, - snapToGrid: function(gx, gy) { + clone: function() { - 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; + return new Line(this.start, this.end); }, - topLine: function() { + // @return {point} the closest point on the line to point `p` + closestPoint: function(p) { - return Line(this.origin(), this.topRight()); + return this.pointAt(this.closestPointNormalizedLength(p)); }, - topMiddle: function() { + closestPointLength: function(p) { - return Point(this.x + this.width / 2, this.y); + return this.closestPointNormalizedLength(p) * this.length(); }, - topRight: function() { + // @return {number} the normalized length of the closest point on the line to point `p` + closestPointNormalizedLength: function(p) { - return Point(this.x + this.width, this.y); + var product = this.vector().dot((new Line(this.start, p)).vector()); + var cpNormalizedLength = min(1, max(0, product / this.squaredLength())); + + // cpNormalizedLength returns `NaN` if this line has zero length + // we can work with that - if `NaN`, return 0 + if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN` + // (`NaN` is the only value that is not equal to itself) + + return cpNormalizedLength; }, - toJSON: function() { + closestPointTangent: function(p) { - return { x: this.x, y: this.y, width: this.width, height: this.height }; + return this.tangentAt(this.closestPointNormalizedLength(p)); }, - toString: function() { + equals: function(l) { - return this.origin().toString() + ' ' + this.corner().toString(); + return !!l && + 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 {rect} representing the union of both rectangles. - union: function(rect) { + intersectionWithLine: function(line) { - rect = Rect(rect); - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = rect.origin(); - var rCorner = rect.corner(); + var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y); + var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y); + var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); + var deltaPt = new Point(line.start.x - this.start.x, line.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); - 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); + if (det === 0 || alpha * det < 0 || beta * det < 0) { + // No intersection found. + return null; + } - return Rect(originX, originY, cornerX - originX, cornerY - originY); - } - }; + if (det > 0) { + if (alpha > det || beta > det) { + return null; + } - var Polyline = g.Polyline = function(points) { + } else { + if (alpha < det || beta < det) { + return null; + } + } - if (!(this instanceof Polyline)) { - return new Polyline(points); - } + return [new Point( + this.start.x + (alpha * pt1Dir.x / det), + this.start.y + (alpha * pt1Dir.y / det) + )]; + }, - this.points = (Array.isArray(points)) ? points.map(Point) : []; - }; + // @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(shape, opt) { - Polyline.prototype = { + if (shape instanceof Line || + shape instanceof Rect || + shape instanceof Polyline || + shape instanceof Ellipse || + shape instanceof Path + ) { + var intersection = shape.intersectionWithLine(this, opt); - pointAtLength: function(length) { - var points = this.points; - var l = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var a = points[i]; - var b = points[i+1]; - var d = a.distance(b); - l += d; - if (length <= l) { - return Line(b, a).pointAt(d ? (l - length) / d : 0); + // Backwards compatibility + if (intersection && (shape instanceof Line)) { + intersection = intersection[0]; } + + return intersection; } + return null; }, + isDifferentiable: function() { + + return !this.start.equals(this.end); + }, + + // @return {double} length of the line length: function() { - var points = this.points; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - length += points[i].distance(points[i+1]); - } - return length; + + return sqrt(this.squaredLength()); }, - closestPoint: function(p) { - return this.pointAtLength(this.closestPointLength(p)); + // @return {point} my midpoint + midpoint: function() { + + return new Point( + (this.start.x + this.end.x) / 2, + (this.start.y + this.end.y) / 2 + ); }, - closestPointLength: function(p) { - var points = this.points; - var pointLength; - var minSqrDistance = Infinity; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var line = Line(points[i], points[i+1]); - var lineLength = line.length(); - var cpNormalizedLength = line.closestPointNormalizedLength(p); - var cp = line.pointAt(cpNormalizedLength); - var sqrDistance = cp.squaredDistance(p); - if (sqrDistance < minSqrDistance) { - minSqrDistance = sqrDistance; - pointLength = length + cpNormalizedLength * lineLength; - } - length += lineLength; - } - return pointLength; - }, + // @return {point} my point at 't' <0,1> + pointAt: function(t) { - toString: function() { + var start = this.start; + var end = this.end; - return this.points + ''; - }, + if (t <= 0) return start.clone(); + if (t >= 1) return end.clone(); - // Returns a convex-hull polyline from this polyline. - // this function implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan) - // output polyline starts at the first element of the original polyline that is on the hull - // output polyline then continues clockwise from that point - convexHull: function() { + return start.lerp(end, t); + }, - var i; - var n; + pointAtLength: function(length) { - var points = this.points; + var start = this.start; + var end = this.end; - // step 1: find the starting point - point with the lowest y (if equality, highest x) - var startPoint; - n = points.length; - for (i = 0; i < n; i++) { - if (startPoint === undefined) { - // if this is the first point we see, set it as start point - startPoint = points[i]; - } else if (points[i].y < startPoint.y) { - // start point should have lowest y from all points - startPoint = points[i]; - } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { - // if two points have the lowest y, choose the one that has highest x - // there are no points to the right of startPoint - no ambiguity about theta 0 - // if there are several coincident start point candidates, first one is reported - startPoint = points[i]; - } + fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - // step 2: sort the list of points - // sorting by angle between line from startPoint to point and the x-axis (theta) - - // step 2a: create the point records = [point, originalIndex, angle] - var sortedPointRecords = []; - n = points.length; - for (i = 0; i < n; i++) { - var angle = startPoint.theta(points[i]); - if (angle === 0) { - angle = 360; // give highest angle to start point - // the start point will end up at end of sorted list - // the start point will end up at beginning of hull points list - } - - var entry = [points[i], i, angle]; - sortedPointRecords.push(entry); - } + var lineLength = this.length(); + if (length >= lineLength) return (fromStart ? end.clone() : start.clone()); - // step 2b: sort the list in place - sortedPointRecords.sort(function(record1, record2) { - // returning a negative number here sorts record1 before record2 - // if first angle is smaller than second, first angle should come before second - var sortOutput = record1[2] - record2[2]; // negative if first angle smaller - if (sortOutput === 0) { - // if the two angles are equal, sort by originalIndex - sortOutput = record2[1] - record1[1]; // negative if first index larger - // coincident points will be sorted in reverse-numerical order - // so the coincident points with lower original index will be considered first - } - return sortOutput; - }); + return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength); + }, - // step 2c: duplicate start record from the top of the stack to the bottom of the stack - if (sortedPointRecords.length > 2) { - var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; - sortedPointRecords.unshift(startPointRecord); - } + // @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) { - // step 3a: go through sorted points in order and find those with right turns - // we want to get our results in clockwise order - var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull - var hullPointRecords = []; // stack of records with right turns - hull point candidates + // Find the sign of the determinant of vectors (start,end), where p is the query point. + p = new g.Point(p); + var start = this.start; + var end = this.end; + var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x)); - var currentPointRecord; - var currentPoint; - var lastHullPointRecord; - var lastHullPoint; - var secondLastHullPointRecord; - var secondLastHullPoint; - while (sortedPointRecords.length !== 0) { - currentPointRecord = sortedPointRecords.pop(); - currentPoint = currentPointRecord[0]; + return determinant / this.length(); + }, - // check if point has already been discarded - // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' - if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { - // this point had an incorrect turn at some previous iteration of this loop - // this disqualifies it from possibly being on the hull - continue; - } + rotate: function(origin, angle) { - var correctTurnFound = false; - while (!correctTurnFound) { - if (hullPointRecords.length < 2) { - // not enough points for comparison, just add current point - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; - - } else { - lastHullPointRecord = hullPointRecords.pop(); - lastHullPoint = lastHullPointRecord[0]; - secondLastHullPointRecord = hullPointRecords.pop(); - secondLastHullPoint = secondLastHullPointRecord[0]; + this.start.rotate(origin, angle); + this.end.rotate(origin, angle); + return this; + }, - var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); + round: function(precision) { - if (crossProduct < 0) { - // found a right turn - hullPointRecords.push(secondLastHullPointRecord); - hullPointRecords.push(lastHullPointRecord); - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; + var f = pow(10, precision || 0); + this.start.x = round(this.start.x * f) / f; + this.start.y = round(this.start.y * f) / f; + this.end.x = round(this.end.x * f) / f; + this.end.y = round(this.end.y * f) / f; + return this; + }, - } else if (crossProduct === 0) { - // the three points are collinear - // three options: - // there may be a 180 or 0 degree angle at lastHullPoint - // or two of the three points are coincident - var THRESHOLD = 1e-10; // we have to take rounding errors into account - var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); - if (Math.abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 - // if the cross product is 0 because the angle is 180 degrees - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { - // if the cross product is 0 because two points are the same - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (Math.abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 - // if the cross product is 0 because the angle is 0 degrees - // remove last hull point from hull BUT do not discard it - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // put last hull point back into the sorted point records list - sortedPointRecords.push(lastHullPointRecord); - // we are switching the order of the 0deg and 180deg points - // correct turn not found - } + scale: function(sx, sy, origin) { - } else { - // found a left turn - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter of loop) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - } - } - } - } - // at this point, hullPointRecords contains the output points in clockwise order - // the points start with lowest-y,highest-x startPoint, and end at the same point + this.start.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - // step 3b: remove duplicated startPointRecord from the end of the array - if (hullPointRecords.length > 2) { - hullPointRecords.pop(); - } + // @return {number} scale the line so that it has the requested length + setLength: function(length) { - // step 4: find the lowest originalIndex record and put it at the beginning of hull - var lowestHullIndex; // the lowest originalIndex on the hull - var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex - n = hullPointRecords.length; - for (i = 0; i < n; i++) { - var currentHullIndex = hullPointRecords[i][1]; + var currentLength = this.length(); + if (!currentLength) return this; - if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { - lowestHullIndex = currentHullIndex; - indexOfLowestHullIndexRecord = i; - } - } + var scaleFactor = length / currentLength; + return this.scale(scaleFactor, scaleFactor, this.start); + }, - var hullPointRecordsReordered = []; - if (indexOfLowestHullIndexRecord > 0) { - var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); - var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); - hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - } else { - hullPointRecordsReordered = hullPointRecords; - } + // @return {integer} length without sqrt + // @note for applications where the exact length is not necessary (e.g. compare only) + squaredLength: function() { - var hullPoints = []; - n = hullPointRecordsReordered.length; - for (i = 0; i < n; i++) { - hullPoints.push(hullPointRecordsReordered[i][0]); - } + 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; + }, - return Polyline(hullPoints); - } - }; + tangentAt: function(t) { + if (!this.isDifferentiable()) return null; - g.scale = { + var start = this.start; + var end = this.end; - // Return the `value` from the `domain` interval scaled to the `range` interval. - linear: function(domain, range, value) { + var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1 - var domainSpan = domain[1] - domain[0]; - var rangeSpan = range[1] - range[0]; - return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; - } - }; + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - var normalizeAngle = g.normalizeAngle = function(angle) { + return tangentLine; + }, - return (angle % 360) + (angle < 0 ? 360 : 0); - }; + tangentAtLength: function(length) { - var snapToGrid = g.snapToGrid = function(value, gridSize) { + if (!this.isDifferentiable()) return null; - return gridSize * Math.round(value / gridSize); - }; + var start = this.start; + var end = this.end; - var toDeg = g.toDeg = function(rad) { + var tangentStart = this.pointAtLength(length); - return (180 * rad / PI) % 360; - }; + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - var toRad = g.toRad = function(deg, over360) { + return tangentLine; + }, - over360 = over360 || false; - deg = over360 ? deg : (deg % 360); - return deg * PI / 180; + translate: function(tx, ty) { + + this.start.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, + + // @return vector {point} of the line + vector: function() { + + return new Point(this.end.x - this.start.x, this.end.y - this.start.y); + }, + + toString: function() { + + return this.start.toString() + ' ' + this.end.toString(); + } }; // For backwards compatibility: - g.ellipse = g.Ellipse; - g.line = g.Line; - g.point = g.Point; - g.rect = g.Rect; + Line.prototype.intersection = Line.prototype.intersect; - return g; + // Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. + // Path created is not guaranteed to be a valid (serializable) path (might not start with an M). + var Path = g.Path = function(arg) { -})(); + if (!(this instanceof Path)) { + return new Path(arg); + } -// Vectorizer. -// ----------- + if (typeof arg === 'string') { // create from a path data string + return new Path.parse(arg); + } -// A tiny library for making your life easier when dealing with SVG. -// The only Vectorizer dependency is the Geometry library. + this.segments = []; + var i; + var n; -var V; -var Vectorizer; + if (!arg) { + // don't do anything -V = Vectorizer = (function() { + } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array + n = arg.length; + if (arg[0].isSegment) { // create from an array of segments + for (i = 0; i < n; i++) { - 'use strict'; + var segment = arg[i]; - var hasSvg = typeof window === 'object' && - !!( - window.SVGAngle || - document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') - ); + this.appendSegment(segment); + } - // SVG support is required. - if (!hasSvg) { + } else { // create from an array of Curves and/or Lines + var previousObj = null; + for (i = 0; i < n; i++) { - // Return a function that throws an error when it is used. - return function() { - throw new Error('SVG is required to use Vectorizer.'); - }; - } + var obj = arg[i]; - // 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' + if (!((obj instanceof Line) || (obj instanceof Curve))) { + throw new Error('Cannot construct a path segment from the provided object.'); + } + + if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); + + // if objects do not link up, moveto segments are inserted to cover the gaps + if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); + + if (obj instanceof Line) { + this.appendSegment(Path.createSegment('L', obj.end)); + + } else if (obj instanceof Curve) { + this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); + } + + previousObj = obj; + } + } + + } else if (arg.isSegment) { // create from a single segment + this.appendSegment(arg); + + } else if (arg instanceof Line) { // create from a single Line + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('L', arg.end)); + + } else if (arg instanceof Curve) { // create from a single Curve + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); + + } else if (arg instanceof Polyline && arg.points && arg.points.length !== 0) { // create from a Polyline + n = arg.points.length; + for (i = 0; i < n; i++) { + + var point = arg.points[i]; + + if (i === 0) this.appendSegment(Path.createSegment('M', point)); + else this.appendSegment(Path.createSegment('L', point)); + } + } }; - var SVGversion = '1.1'; + // More permissive than V.normalizePathData and Path.prototype.serialize. + // Allows path data strings that do not start with a Moveto command (unlike SVG specification). + // Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). + // Allows for command argument chaining. + // Throws an error if wrong number of arguments is provided with a command. + // Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). + Path.parse = function(pathData) { - var V = function(el, attrs, children) { + if (!pathData) return new Path(); - // This allows using V() without the new keyword. - if (!(this instanceof V)) { - return V.apply(Object.create(V.prototype), arguments); + var path = new Path(); + + var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; + var commands = pathData.match(commandRe); + + var numCommands = commands.length; + for (var i = 0; i < numCommands; i++) { + + var command = commands[i]; + var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?))|(?:(?:-?\.\d+))/g; + var args = command.match(argRe); + + var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] + path.appendSegment(segment); } - if (!el) return; + return path; + }; - if (V.isV(el)) { - el = el.node; + // Create a segment or an array of segments. + // Accepts unlimited points/coords arguments after `type`. + Path.createSegment = function(type) { + + if (!type) throw new Error('Type must be provided.'); + + var segmentConstructor = Path.segmentTypes[type]; + if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); + + var args = []; + var n = arguments.length; + for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array + args.push(arguments[i]); } - attrs = attrs || {}; + return applyToNew(segmentConstructor, args); + }, - if (V.isString(el)) { + Path.prototype = { - if (el.toLowerCase() === 'svg') { + // Accepts one segment or an array of segments as argument. + // Throws an error if argument is not a segment or an array of segments. + appendSegment: function(arg) { - // Create a new SVG canvas. - el = V.createSvgDocument(); + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - } else if (el[0] === '<') { + var currentSegment; - // Create element from an SVG string. - // Allows constructs of type: `document.appendChild(V('').node)`. + var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null + var nextSegment = null; - var svgDoc = V.createSvgDocument(el); + if (!Array.isArray(arg)) { // arg is a segment + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - // 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) { + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.push(currentSegment); - // Map child nodes to `V`s. - var arrayOfVels = []; - var i, len; + } else { // arg is an array of segments + if (!arg[0].isSegment) throw new Error('Segments required.'); - for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { + var n = arg.length; + for (var i = 0; i < n; i++) { - var childNode = svgDoc.childNodes[i]; - arrayOfVels.push(new V(document.importNode(childNode, true))); - } + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.push(currentSegment); + previousSegment = currentSegment; + } + } + }, - return arrayOfVels; + // Returns the bbox of the path. + // If path has no segments, returns null. + // If path has only invisible segments, returns bbox of the end point of last segment. + bbox: function() { + + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array + + var bbox; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + if (segment.isVisible) { + var segmentBBox = segment.bbox(); + bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; } + } - el = document.importNode(svgDoc.firstChild, true); + if (bbox) return bbox; - } else { + // if the path has only invisible elements, return end point of last segment + var lastSegment = segments[numSegments - 1]; + return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); + }, - el = document.createElementNS(ns.xmlns, el); + // Returns a new path that is a clone of this path. + clone: function() { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + var path = new Path(); + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i].clone(); + path.appendSegment(segment); } - V.ensureId(el); - } + return path; + }, - this.node = el; + closestPoint: function(p, opt) { - this.setAttributes(attrs); + var t = this.closestPointT(p, opt); + if (!t) return null; - if (children) { - this.append(children); - } + return this.pointAtT(t); + }, - return this; - }; + closestPointLength: function(p, opt) { - /** - * @param {SVGGElement} toElem - * @returns {SVGMatrix} - */ - V.prototype.getTransformToElement = function(toElem) { - toElem = V.toNode(toElem); - return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - /** - * @param {SVGMatrix} matrix - * @param {Object=} opt - * @returns {Vectorizer|SVGMatrix} Setter / Getter - */ - V.prototype.transform = function(matrix, opt) { + var t = this.closestPointT(p, localOpt); + if (!t) return 0; - var node = this.node; - if (V.isUndefined(matrix)) { - return V.transformStringToMatrix(this.attr('transform')); - } + return this.lengthAtT(t, localOpt); + }, - if (opt && opt.absolute) { - return this.attr('transform', V.matrixToTransformString(matrix)); - } + closestPointNormalizedLength: function(p, opt) { - var svgTransform = V.createSVGTransform(matrix); - node.transform.baseVal.appendItem(svgTransform); - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - V.prototype.translate = function(tx, ty, opt) { + var cpLength = this.closestPointLength(p, localOpt); + if (cpLength === 0) return 0; // shortcut - opt = opt || {}; - ty = ty || 0; + var length = this.length(localOpt); + if (length === 0) return 0; // prevents division by zero - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; - // Is it a getter? - if (V.isUndefined(tx)) { - return transform.translate; - } + return cpLength / length; + }, - transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); + // Private function. + closestPointT: function(p, opt) { - var newTx = opt.absolute ? tx : transform.translate.tx + tx; - var newTy = opt.absolute ? ty : transform.translate.ty + ty; - var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - // Note that `translate()` is always the first transformation. This is - // usually the desired case. - this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - V.prototype.rotate = function(angle, cx, cy, opt) { + var closestPointT; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { - opt = opt || {}; + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; + if (segment.isVisible) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); - // Is it a getter? - if (V.isUndefined(angle)) { - return transform.rotate; - } + if (squaredDistance < minSquaredDistance) { + closestPointT = { segmentIndex: i, value: segmentClosestPointT }; + minSquaredDistance = squaredDistance; + } + } + } - transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); + if (closestPointT) return closestPointT; - angle %= 360; + // if no visible segment, return end of last segment + return { segmentIndex: numSegments - 1, value: 1 }; + }, - var newAngle = opt.absolute ? angle : transform.rotate.angle + angle; - var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : ''; - var newRotate = 'rotate(' + newAngle + newOrigin + ')'; + closestPointTangent: function(p, opt) { - this.attr('transform', (transformAttr + ' ' + newRotate).trim()); - return this; - }; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - // Note that `scale` as the only transformation does not combine with previous values. - V.prototype.scale = function(sx, sy) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - sy = V.isUndefined(sy) ? sx : sy; + var closestPointTangent; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; - // Is it a getter? - if (V.isUndefined(sx)) { - return transform.scale; - } + if (segment.isDifferentiable()) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); - transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim(); + if (squaredDistance < minSquaredDistance) { + closestPointTangent = segment.tangentAtT(segmentClosestPointT); + minSquaredDistance = squaredDistance; + } + } + } - var newScale = 'scale(' + sx + ',' + sy + ')'; + if (closestPointTangent) return closestPointTangent; - this.attr('transform', (transformAttr + ' ' + newScale).trim()); - return this; - }; + // if no valid segment, return null + return null; + }, - // 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) { + // Checks whether two paths are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { - var box; - var node = this.node; - var ownerSVGElement = node.ownerSVGElement; + if (!p) return false; - // 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); - } + var segments = this.segments; + var otherSegments = p.segments; - try { + var numSegments = segments.length; + if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal - box = node.getBBox(); + for (var i = 0; i < numSegments; i++) { - } catch (e) { + var segment = segments[i]; + var otherSegment = otherSegments[i]; - // Fallback for IE. - box = { - x: node.clientLeft, - y: node.clientTop, - width: node.clientWidth, - height: node.clientHeight - }; - } + // as soon as an inequality is found in segments, return false + if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; + } - if (withoutTransformations) { - return g.Rect(box); - } + // if no inequality found in segments, return true + return true; + }, - var matrix = this.getTransformToElement(target || ownerSVGElement); + // Accepts negative indices. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + getSegment: function(index) { - return V.transformRect(box, matrix); - }; - - // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, - // i.e. after transformations are applied. - // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. - // Takes an (Object) `opt` argument (optional) with the following attributes: - // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this - // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); - V.prototype.getBBox = function(opt) { + var segments = this.segments; + var numSegments = segments.length; + if (!numSegments === 0) throw new Error('Path has no segments.'); - var options = {}; + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - var outputBBox; - var node = this.node; - var ownerSVGElement = node.ownerSVGElement; + return segments[index]; + }, - // 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); - } + // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. + getSegmentSubdivisions: function(opt) { - if (opt) { - if (opt.target) { // check if target exists - options.target = V.toNode(opt.target); // works for V objects, jquery objects, and node objects - } - if (opt.recursive) { - options.recursive = opt.recursive; + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.segmentSubdivisions + // not using localOpt + + var segmentSubdivisions = []; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segment.getSubdivisions({ precision: precision }); + segmentSubdivisions.push(subdivisions); } - } - if (!options.recursive) { - try { - outputBBox = node.getBBox(); - } catch (e) { - // Fallback for IE. - outputBBox = { - x: node.clientLeft, - y: node.clientTop, - width: node.clientWidth, - height: node.clientHeight - }; - } - - if (!options.target) { - // transform like this (that is, not at all) - return g.Rect(outputBBox); - } else { - // transform like target - var matrix = this.getTransformToElement(options.target); - return V.transformRect(outputBBox, matrix); - } - } else { // if we want to calculate the bbox recursively - // browsers report correct bbox around svg elements (one that envelops the path lines tightly) - // but some browsers fail to report the same bbox when the elements are in a group (returning a looser bbox that also includes control points, like node.getClientRect()) - // this happens even if we wrap a single svg element into a group! - // this option setting makes the function recursively enter all the groups from this and deeper, get bboxes of the elements inside, then return a union of those bboxes + return segmentSubdivisions; + }, - var children = this.children(); - var n = children.length; - - if (n === 0) { - return this.getBBox({ target: options.target, recursive: false }); + // Insert `arg` at given `index`. + // `index = 0` means insert at the beginning. + // `index = segments.length` means insert at the end. + // Accepts negative indices, from `-1` to `-(segments.length + 1)`. + // Accepts one segment or an array of segments as argument. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + insertSegment: function(index, arg) { + + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments + + // note that these are incremented comapared to getSegments() + // we can insert after last element (note that this changes the meaning of index -1) + if (index < 0) index = numSegments + index + 1; // convert negative indices to positive + if (index > numSegments || index < 0) throw new Error('Index out of range.'); + + var currentSegment; + + var previousSegment = null; + var nextSegment = null; + + if (numSegments !== 0) { + if (index >= 1) { + previousSegment = segments[index - 1]; + nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null + + } else { // if index === 0 + // previousSegment is null + nextSegment = segments[0]; + } } - // recursion's initial pass-through setting: - // recursive passes-through just keep the target as whatever was set up here during the initial pass-through - if (!options.target) { - // transform children/descendants like this (their parent/ancestor) - options.target = this; - } // else transform children/descendants like target + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - for (var i = 0; i < n; i++) { - var currentChild = children[i]; + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 0, currentSegment); - var childBBox; + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); - // if currentChild is not a group element, get its bbox with a nonrecursive call - if (currentChild.children().length === 0) { - childBBox = currentChild.getBBox({ target: options.target, recursive: false }); - } - else { - // if currentChild is a group element (determined by checking the number of children), enter it with a recursive call - childBBox = currentChild.getBBox({ target: options.target, recursive: true }); - } + var n = arg.length; + for (var i = 0; i < n; i++) { - if (!outputBBox) { - // if this is the first iteration - outputBBox = childBBox; - } else { - // make a new bounding box rectangle that contains this child's bounding box and previous bounding box - outputBBox = outputBBox.union(childBBox); + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; } } + }, - return outputBBox; - } - }; + isDifferentiable: function() { - V.prototype.text = function(content, opt) { + var segments = this.segments; + var numSegments = segments.length; - // 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 eol = opt.eol; - var lines = content.split('\n'); - var tspan; + for (var i = 0; i < numSegments; i++) { - // 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'); - } + var segment = segments[i]; + // as soon as a differentiable segment is found in segments, return true + if (segment.isDifferentiable()) return true; + } - // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. - this.attr('xml:space', 'preserve'); + // if no differentiable segment is found in segments, return false + return false; + }, - // Easy way to erase all `` children; - this.node.textContent = ''; + // Checks whether current path segments are valid. + // Note that d is allowed to be empty - should disable rendering of the path. + isValid: function() { - var textNode = this.node; + var segments = this.segments; + var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto + return isValid; + }, - if (opt.textPath) { + // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. + // If path has no segments, returns 0. + length: function(opt) { - // 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); - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array - // 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); - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - 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); - } + var length = 0; + for (var i = 0; i < numSegments; i++) { - if (Object(opt.textPath) === opt.textPath) { - textPath.attr(opt.textPath); + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + length += segment.length({ subdivisions: subdivisions }); } - this.append(textPath); - // Now all the ``s will be inside the ``. - textNode = textPath.node; - } - var offset = 0; - var x = ((opt.x !== undefined) ? opt.x : this.attr('x')) || 0; + return length; + }, - // Shift all the but first by one line (`1em`) - var lineHeight = opt.lineHeight || '1em'; - if (opt.lineHeight === 'auto') { - lineHeight = '1.5em'; - } + // Private function. + lengthAtT: function(t, opt) { - var firstLineHeight = 0; - for (var i = 0; i < lines.length; i++) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array - var vLineAttributes = { 'class': 'v-line' }; - if (i === 0) { - vLineAttributes.dy = '0em'; - } else { - vLineAttributes.dy = lineHeight; - vLineAttributes.x = x; + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return 0; // regardless of t.value + + var tValue = t.value; + if (segmentIndex >= numSegments) { + segmentIndex = numSegments - 1; + tValue = 1; } - var vLine = V('tspan', vLineAttributes); + else if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - var lastI = lines.length - 1; - var line = lines[i]; - if (line) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - // Get the line height based on the biggest font size in the annotations for this line. - var maxFontSize = 0; - if (opt.annotations) { + var subdivisions; + var length = 0; + for (var i = 0; i < segmentIndex; i++) { - // 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 }); + var segment = segments[i]; + subdivisions = segmentSubdivisions[i]; + length += segment.length({ precisison: precision, subdivisions: subdivisions }); + } - var lastJ = lineAnnotations.length - 1; - for (var j = 0; j < lineAnnotations.length; j++) { + segment = segments[segmentIndex]; + subdivisions = segmentSubdivisions[segmentIndex]; + length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); - var annotation = lineAnnotations[j]; - if (V.isObject(annotation)) { + return length; + }, - var fontSize = parseFloat(annotation.attrs['font-size']); - if (fontSize && fontSize > maxFontSize) { - maxFontSize = fontSize; - } + // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + pointAt: function(ratio, opt) { - 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']); - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (eol && j === lastJ && i !== lastI) { - annotation.t += eol; - } - tspan.node.textContent = annotation.t; + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); - } else { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - if (eol && j === lastJ && i !== lastI) { - annotation += eol; - } - tspan = document.createTextNode(annotation || ' '); - } - vLine.append(tspan); - } + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) { + return this.pointAtLength(length, localOpt); + }, - vLine.attr('dy', (maxFontSize * 1.2) + 'px'); - } + // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + pointAtLength: function(length, opt) { - } else { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (eol && i !== lastI) { - line += eol; - } + if (length === 0) return this.start.clone(); - vLine.node.textContent = line; - } + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - if (i === 0) { - firstLineHeight = maxFontSize; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var lastVisibleSegment; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); + + if (segment.isVisible) { + if (length <= (l + d)) { + return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } + + lastVisibleSegment = segment; } - } 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 = '-'; + l += d; } - V(textNode).append(vLine); - - offset += line.length + 1; // + 1 = newline character. - } + // if length requested is higher than the length of the path, return last visible segment endpoint + if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); - // `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 === null) { - this.attr('y', firstLineHeight || '0.8em'); - } + // if no visible segment, return last segment end point (no matter if fromStart or no) + var lastSegment = segments[numSegments - 1]; + return lastSegment.end.clone(); + }, - return this; - }; + // Private function. + pointAtT: function(t) { - /** - * @public - * @param {string} name - * @returns {Vectorizer} - */ - V.prototype.removeAttr = function(name) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - var qualifiedName = V.qualifyAttr(name); - var el = this.node; + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].pointAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); - 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; - }; + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - V.prototype.attr = function(name, value) { + return segments[segmentIndex].pointAtT(tValue); + }, - if (V.isUndefined(name)) { + // Helper method for adding segments. + prepareSegment: function(segment, previousSegment, nextSegment) { - // Return all attributes. - var attributes = this.node.attributes; - var attrs = {}; + // insert after previous segment and before previous segment's next segment + segment.previousSegment = previousSegment; + segment.nextSegment = nextSegment; + if (previousSegment) previousSegment.nextSegment = segment; + if (nextSegment) nextSegment.previousSegment = segment; - for (var i = 0; i < attributes.length; i++) { - attrs[attributes[i].name] = attributes[i].value; + var updateSubpathStart = segment; + if (segment.isSubpathStart) { + segment.subpathStartSegment = segment; // assign self as subpath start segment + updateSubpathStart = nextSegment; // start updating from next segment } - return attrs; - } + // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments + if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); - if (V.isString(name) && V.isUndefined(value)) { - return this.node.getAttribute(name); - } + return segment; + }, - if (typeof name === 'object') { + // Default precision + PRECISION: 3, - for (var attrName in name) { - if (name.hasOwnProperty(attrName)) { - this.setAttribute(attrName, name[attrName]); - } - } + // Remove the segment at `index`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + removeSegment: function(index) { - } else { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); - this.setAttribute(name, value); - } + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - return this; - }; + var removedSegment = segments.splice(index, 1)[0]; + var previousSegment = removedSegment.previousSegment; + var nextSegment = removedSegment.nextSegment; - V.prototype.remove = function() { + // link the previous and next segments together (if present) + if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null + if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null - if (this.node.parentNode) { - this.node.parentNode.removeChild(this.node); - } + // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached + if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, - return this; - }; + // Replace the segment at `index` with `arg`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Accepts one segment or an array of segments as argument. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + replaceSegment: function(index, arg) { - V.prototype.empty = function() { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); - while (this.node.firstChild) { - this.node.removeChild(this.node.firstChild); - } + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - return this; - }; + var currentSegment; - /** - * @private - * @param {object} attrs - * @returns {Vectorizer} - */ - V.prototype.setAttributes = function(attrs) { + var replacedSegment = segments[index]; + var previousSegment = replacedSegment.previousSegment; + var nextSegment = replacedSegment.nextSegment; - for (var key in attrs) { - if (attrs.hasOwnProperty(key)) { - this.setAttribute(key, attrs[key]); - } - } + var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? - return this; - }; + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - V.prototype.append = function(els) { + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 1, currentSegment); // directly replace - if (!V.isArray(els)) { - els = [els]; - } + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` - for (var i = 0, len = els.length; i < len; i++) { - this.node.appendChild(V.toNode(els[i])); - } + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); - return this; - }; + segments.splice(index, 1); - V.prototype.prepend = function(els) { + var n = arg.length; + for (var i = 0; i < n; i++) { - var child = this.node.firstChild; - return child ? V(child).before(els) : this.append(els); - }; + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; - V.prototype.before = function(els) { + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + } + } - var node = this.node; - var parent = node.parentNode; + // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached + if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, - if (parent) { + scale: function(sx, sy, origin) { - if (!V.isArray(els)) { - els = [els]; - } + var segments = this.segments; + var numSegments = segments.length; - for (var i = 0, len = els.length; i < len; i++) { - parent.insertBefore(V.toNode(els[i]), node); + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + segment.scale(sx, sy, origin); } - } - return this; - }; + return this; + }, - V.prototype.appendTo = function(node) { - V.toNode(node).appendChild(this.node); - return this; - }, + segmentAt: function(ratio, opt) { - V.prototype.svg = function() { + var index = this.segmentIndexAt(ratio, opt); + if (!index) return null; - return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); - }; + return this.getSegment(index); + }, - V.prototype.defs = function() { + // Accepts negative length. + segmentAtLength: function(length, opt) { - var defs = this.svg().node.getElementsByTagName('defs'); + var index = this.segmentIndexAtLength(length, opt); + if (!index) return null; - return (defs && defs.length) ? V(defs[0]) : undefined; - }; + return this.getSegment(index); + }, - V.prototype.clone = function() { + segmentIndexAt: function(ratio, opt) { - 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; - }; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - V.prototype.findOne = function(selector) { + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - var found = this.node.querySelector(selector); - return found ? V(found) : undefined; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - V.prototype.find = function(selector) { + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - var vels = []; - var nodes = this.node.querySelectorAll(selector); + return this.segmentIndexAtLength(length, localOpt); + }, - if (nodes) { + toPoints: function(opt) { - // Map DOM elements to `V`s. - for (var i = 0; i < nodes.length; i++) { - vels.push(V(nodes[i])); - } - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - return vels; - }; - - // Returns an array of V elements made from children of this.node. - V.prototype.children = function() { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + + var points = []; + var partialPoints = []; + for (var i = 0; i < numSegments; i++) { + var segment = segments[i]; + if (segment.isVisible) { + var currentSegmentSubdivisions = segmentSubdivisions[i]; + if (currentSegmentSubdivisions.length > 0) { + var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { + return curve.start; + }); + Array.prototype.push.apply(partialPoints, subdivisionPoints); + } else { + partialPoints.push(segment.start); + } + } else if (partialPoints.length > 0) { + partialPoints.push(segments[i - 1].end); + points.push(partialPoints); + partialPoints = []; + } + } - var children = this.node.childNodes; - - var outputArray = []; - for (var i = 0; i < children.length; i++) { - var currentChild = children[i]; - if (currentChild.nodeType === 1) { - outputArray.push(V(children[i])); + if (partialPoints.length > 0) { + partialPoints.push(this.end); + points.push(partialPoints); } - } - return outputArray; - }; + return points; + }, - // Find an index of an element inside its container. - V.prototype.index = function() { + toPolylines: function(opt) { - var index = 0; - var node = this.node.previousSibling; + var polylines = []; + var points = this.toPoints(opt); + if (!points) return null; + for (var i = 0, n = points.length; i < n; i++) { + polylines.push(new Polyline(points[i])); + } - while (node) { - // nodeType 1 for ELEMENT_NODE - if (node.nodeType === 1) index++; - node = node.previousSibling; - } + return polylines; + }, - return index; - }; + intersectionWithLine: function(line, opt) { + + var intersection = null; + var polylines = this.toPolylines(opt); + if (!polylines) return null; + for (var i = 0, n = polylines.length; i < n; i++) { + var polyline = polylines[i]; + var polylineIntersection = line.intersect(polyline); + if (polylineIntersection) { + intersection || (intersection = []); + if (Array.isArray(polylineIntersection)) { + Array.prototype.push.apply(intersection, polylineIntersection); + } else { + intersection.push(polylineIntersection); + } + } + } - V.prototype.findParentByClass = function(className, terminator) { + return intersection; + }, - var ownerSVGElement = this.node.ownerSVGElement; - var node = this.node.parentNode; + // Accepts negative length. + segmentIndexAtLength: function(length, opt) { - while (node && node !== terminator && node !== ownerSVGElement) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - var vel = V(node); - if (vel.hasClass(className)) { - return vel; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - node = node.parentNode; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - return null; - }; + var lastVisibleSegmentIndex = null; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - // https://jsperf.com/get-common-parent - V.prototype.contains = function(el) { + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - var a = this.node; - var b = V.toNode(el); - var bup = b && b.parentNode; + if (segment.isVisible) { + if (length <= (l + d)) return i; + lastVisibleSegmentIndex = i; + } - return (a === bup) || !!(bup && bup.nodeType === 1 && (a.compareDocumentPosition(bup) & 16)); - }; + l += d; + } - // Convert global point into the coordinate space of this element. - V.prototype.toLocalPoint = function(x, y) { + // if length requested is higher than the length of the path, return last visible segment index + // if no visible segment, return null + return lastVisibleSegmentIndex; + }, - var svg = this.svg().node; + // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + tangentAt: function(ratio, opt) { - var p = svg.createSVGPoint(); - p.x = x; - p.y = y; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - try { + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse()); - var globalToLocalMatrix = this.getTransformToElement(svg).inverse(); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - } 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; - } + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - return globalPoint.matrixTransform(globalToLocalMatrix); - }; + return this.tangentAtLength(length, localOpt); + }, - V.prototype.translateCenterToPoint = function(p) { + // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + tangentAtLength: function(length, opt) { - var bbox = this.getBBox({ target: this.svg() }); - var center = bbox.center(); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - this.translate(p.x - center.x, p.y - center.y); - return this; - }; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // 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) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - // 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 lastValidSegment; // visible AND differentiable (with a tangent) + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - var s = this.scale(); - this.attr('transform', ''); - this.scale(s.sx, s.sy); + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - var svg = this.svg().node; - var bbox = this.getBBox({ target: target || svg }); + if (segment.isDifferentiable()) { + if (length <= (l + d)) { + return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } - // 1. Translate to origin. - var translateToOrigin = svg.createSVGTransform(); - translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2); + lastValidSegment = segment; + } - // 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); + l += d; + } - // 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)); + // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment + if (lastValidSegment) { + var t = (fromStart ? 1 : 0); + return lastValidSegment.tangentAtT(t); + } - // 4. Apply transformations. - var ctm = this.getTransformToElement(target || svg); - var transform = svg.createSVGTransform(); - transform.setMatrix( - translateFinal.matrix.multiply( - rotateAroundOrigin.matrix.multiply( - translateToOrigin.matrix.multiply( - ctm))) - ); + // if no valid segment, return null + return null; + }, - // 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); + // Private function. + tangentAtT: function(t) { - var decomposition = V.decomposeMatrix(transform.matrix); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - 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); + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].tangentAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); - return this; - }; + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - V.prototype.animateAlongPath = function(attrs, path) { + return segments[segmentIndex].tangentAtT(tValue); + }, - path = V.toNode(path); + translate: function(tx, ty) { - var id = V.ensureId(path); - var animateMotion = V('animateMotion', attrs); - var mpath = V('mpath', { 'xlink:href': '#' + id }); + var segments = this.segments; + var numSegments = segments.length; - animateMotion.append(mpath); + for (var i = 0; i < numSegments; i++) { - 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') { + var segment = segments[i]; + segment.translate(tx, ty); + } - // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) - var animation = animateMotion.node; - animation.animators = []; + return this; + }, - var animationID = animation.getAttribute('id'); - if (animationID) id2anim[animationID] = animation; + // Helper method for updating subpath start of segments, starting with the one provided. + updateSubpathStartSegment: function(segment) { - 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(); - } + var previousSegment = segment.previousSegment; // may be null + while (segment && !segment.isSubpathStart) { + + // assign previous segment's subpath start segment to this segment + if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null + else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! + + previousSegment = segment; + segment = segment.nextSegment; // move on to the segment after etc. } - } - return this; - }; + }, - V.prototype.hasClass = function(className) { + // Returns a string that can be used to reconstruct the path. + // Additional error checking compared to toString (must start with M segment). + serialize: function() { - return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); - }; + if (!this.isValid()) throw new Error('Invalid path segments.'); - V.prototype.addClass = function(className) { + return this.toString(); + }, - if (!this.hasClass(className)) { - var prevClasses = this.node.getAttribute('class') || ''; - this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); - } + toString: function() { - return this; - }; + var segments = this.segments; + var numSegments = segments.length; - V.prototype.removeClass = function(className) { + var pathData = ''; + for (var i = 0; i < numSegments; i++) { - if (this.hasClass(className)) { - var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2'); - this.node.setAttribute('class', newClasses); - } + var segment = segments[i]; + pathData += segment.serialize() + ' '; + } - return this; + return pathData.trim(); + } }; - V.prototype.toggleClass = function(className, toAdd) { + Object.defineProperty(Path.prototype, 'start', { + // Getter for the first visible endpoint of the path. - var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; + configurable: true, - if (toRemove) { - this.removeClass(className); - } else { - this.addClass(className); - } + enumerable: true, - return this; - }; + get: function() { - // 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) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; - 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; - }; + for (var i = 0; i < numSegments; i++) { - V.prototype.convertToPath = function() { + var segment = segments[i]; + if (segment.isVisible) return segment.start; + } - var path = V('path'); - path.attr(this.attr()); - var d = this.convertToPathData(); - if (d) { - path.attr('d', d); + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; } - return path; - }; + }); - V.prototype.convertToPathData = function() { + Object.defineProperty(Path.prototype, 'end', { + // Getter for the last visible endpoint of the path. - var tagName = this.node.tagName.toUpperCase(); + configurable: true, - 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); - } + enumerable: true, - throw new Error(tagName + ' cannot be converted to PATH.'); - }; + get: function() { - // 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 segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; - var svg = this.svg().node; - target = target || svg; - var bbox = this.getBBox({ target: target }); - var center = bbox.center(); + for (var i = numSegments - 1; i >= 0; i--) { - if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; + var segment = segments[i]; + if (segment.isVisible) return segment.end; + } - var spot; - var tagName = this.node.localName.toUpperCase(); + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); - // 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') { + /* + Point is the most basic object consisting of x/y coordinate. - 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); + Possible instantiations are: + * `Point(10, 20)` + * `new Point(10, 20)` + * `Point('10 20')` + * `Point(Point(10, 20))` + */ + var Point = g.Point = function(x, y) { - } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { + if (!(this instanceof Point)) { + return new Point(x, y); + } - var pathNode = (tagName === 'PATH') ? this : this.convertToPath(); - var samples = pathNode.sample(); - var minDistance = Infinity; - var closestSamples = []; + if (typeof x === 'string') { + var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@'); + x = parseFloat(xy[0]); + y = parseFloat(xy[1]); - var i, sample, gp, centerDistance, refDistance, distance; + } else if (Object(x) === x) { + y = x.y; + x = x.x; + } - for (i = 0; i < samples.length; i++) { + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; + }; - 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; + // Alternative constructor, from polar coordinates. + // @param {number} Distance. + // @param {number} Angle in radians. + // @param {point} [optional] Origin. + Point.fromPolar = function(distance, angle, origin) { - if (distance < minDistance) { - minDistance = distance; - closestSamples = [{ sample: sample, refDistance: refDistance }]; - } else if (distance < minDistance + 1) { - closestSamples.push({ sample: sample, refDistance: refDistance }); - } - } + origin = (origin && new Point(origin)) || new Point(0, 0); + var x = abs(distance * cos(angle)); + var y = abs(distance * sin(angle)); + var deg = normalizeAngle(toDeg(angle)); - closestSamples.sort(function(a, b) { - return a.refDistance - b.refDistance; - }); + if (deg < 90) { + y = -y; - if (closestSamples[0]) { - spot = closestSamples[0].sample; - } + } else if (deg < 180) { + x = -x; + y = -y; + + } else if (deg < 270) { + x = -x; } - return spot; + return new Point(origin.x + x, origin.y + y); }; - /** - * @private - * @param {string} name - * @param {string} value - * @returns {Vectorizer} - */ - V.prototype.setAttribute = function(name, value) { + // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. + Point.random = function(x1, x2, y1, y2) { - var el = this.node; + return new Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); + }; - if (value === null) { - this.removeAttr(name); - return this; - } + Point.prototype = { - var qualifiedName = V.qualifyAttr(name); + // 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 (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); - } + if (r.containsPoint(this)) { + return this; + } - return this; - }; + this.x = min(max(this.x, r.x), r.x + r.width); + this.y = min(max(this.y, r.y), r.y + r.height); + return this; + }, - // Create an SVG document element. - // If `content` is passed, it will be used as the SVG content of the `` root element. - V.createSvgDocument = function(content) { + // Return the bearing between me and the given point. + bearing: function(point) { - var svg = '' + (content || '') + ''; - var xml = V.parseXML(svg, { async: false }); - return xml.documentElement; - }; + return (new Line(this, point)).bearing(); + }, - V.idCounter = 0; + // Returns change in angle from my previous position (-dx, -dy) to my new position + // relative to ref point. + changeInAngle: function(dx, dy, ref) { - // A function returning a unique identifier for this client session with every call. - V.uniqueId = function() { + // Revert the translation and measure the change in angle around x-axis. + return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref); + }, - return 'v-' + (++V.idCounter); - }; + clone: function() { - V.toNode = function(el) { - return V.isV(el) ? el.node : (el.nodeName && el || el[0]); - }; + return new Point(this); + }, - V.ensureId = function(node) { - node = V.toNode(node); - return node.id || (node.id = V.uniqueId()); - }; - // 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. This is used in the text() method but it is - // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests - // when you want to compare the actual DOM text content without having to add the unicode character in - // the place of all spaces. - V.sanitizeText = function(text) { + difference: function(dx, dy) { - return (text || '').replace(/ /g, '\u00A0'); - }; + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } - V.isUndefined = function(value) { + return new Point(this.x - (dx || 0), this.y - (dy || 0)); + }, - return typeof value === 'undefined'; - }; + // Returns distance between me and point `p`. + distance: function(p) { - V.isString = function(value) { + return (new Line(this, p)).length(); + }, - return typeof value === 'string'; - }; + squaredDistance: function(p) { - V.isObject = function(value) { + return (new Line(this, p)).squaredLength(); + }, - return value && (typeof value === 'object'); - }; + equals: function(p) { - V.isArray = Array.isArray; + return !!p && + this.x === p.x && + this.y === p.y; + }, - V.parseXML = function(data, opt) { + magnitude: function() { - opt = opt || {}; + return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; + }, - var xml; + // Returns a manhattan (taxi-cab) distance between me and point `p`. + manhattanDistance: function(p) { - try { - var parser = new DOMParser(); + return abs(p.x - this.x) + abs(p.y - this.y); + }, - if (!V.isUndefined(opt.async)) { - parser.async = opt.async; - } + // Move point on line starting from ref ending at me by + // distance distance. + move: function(ref, distance) { - xml = parser.parseFromString(data, 'text/xml'); - } catch (error) { - xml = undefined; - } + var theta = toRad((new Point(ref)).theta(this)); + var offset = this.offset(cos(theta) * distance, -sin(theta) * distance); + return offset; + }, - if (!xml || xml.getElementsByTagName('parsererror').length) { - throw new Error('Invalid XML: ' + data); - } + // 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) { - return xml; - }; + var scale = (length || 1) / this.magnitude(); + return this.scale(scale, scale); + }, - /** - * @param {string} name - * @returns {{ns: string|null, local: string}} namespace and attribute name - */ - V.qualifyAttr = function(name) { + // Offset me by the specified amount. + offset: function(dx, dy) { - if (name.indexOf(':') !== -1) { - var combinedKey = name.split(':'); - return { - ns: ns[combinedKey[0]], - local: combinedKey[1] - }; - } + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } - return { - ns: null, - local: name - }; - }; + this.x += dx || 0; + this.y += dy || 0; + return this; + }, - V.transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi; - V.transformSeparatorRegex = /[ ,]+/; - V.transformationListRegex = /^(\w+)\((.*)\)/; + // Returns a point that is the reflection of me with + // the center of inversion in ref point. + reflection: function(ref) { - V.transformStringToMatrix = function(transform) { + return (new Point(ref)).move(this, this.distance(ref)); + }, - var transformationMatrix = V.createSVGMatrix(); - var matches = transform && transform.match(V.transformRegex); - if (!matches) { - return transformationMatrix; - } + // Rotate point by angle around origin. + // Angle is flipped because this is a left-handed coord system (y-axis points downward). + rotate: function(origin, angle) { - for (var i = 0, n = matches.length; i < n; i++) { - var transformationString = matches[i]; + origin = origin || new g.Point(0, 0); - var transformationMatch = transformationString.match(V.transformationListRegex); - if (transformationMatch) { - var sx, sy, tx, ty, angle; - var ctm = V.createSVGMatrix(); - var args = transformationMatch[2].split(V.transformSeparatorRegex); - switch (transformationMatch[1].toLowerCase()) { - case 'scale': - sx = parseFloat(args[0]); - sy = (args[1] === undefined) ? sx : parseFloat(args[1]); - ctm = ctm.scaleNonUniform(sx, sy); - break; - case 'translate': - tx = parseFloat(args[0]); - ty = parseFloat(args[1]); - ctm = ctm.translate(tx, ty); - break; - case 'rotate': - angle = parseFloat(args[0]); - tx = parseFloat(args[1]) || 0; - ty = parseFloat(args[2]) || 0; - if (tx !== 0 || ty !== 0) { - ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty); - } else { - ctm = ctm.rotate(angle); - } - break; - case 'skewx': - angle = parseFloat(args[0]); - ctm = ctm.skewX(angle); - break; - case 'skewy': - angle = parseFloat(args[0]); - ctm = ctm.skewY(angle); - break; - case 'matrix': - ctm.a = parseFloat(args[0]); - ctm.b = parseFloat(args[1]); - ctm.c = parseFloat(args[2]); - ctm.d = parseFloat(args[3]); - ctm.e = parseFloat(args[4]); - ctm.f = parseFloat(args[5]); - break; - default: - continue; - } + angle = toRad(normalizeAngle(-angle)); + var cosAngle = cos(angle); + var sinAngle = sin(angle); - transformationMatrix = transformationMatrix.multiply(ctm); - } + var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x; + var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y; - } - return transformationMatrix; - }; + this.x = x; + this.y = y; + return this; + }, - V.matrixToTransformString = function(matrix) { - matrix || (matrix = true); + round: function(precision) { - return 'matrix(' + - (matrix.a !== undefined ? matrix.a : 1) + ',' + - (matrix.b !== undefined ? matrix.b : 0) + ',' + - (matrix.c !== undefined ? matrix.c : 0) + ',' + - (matrix.d !== undefined ? matrix.d : 1) + ',' + - (matrix.e !== undefined ? matrix.e : 0) + ',' + - (matrix.f !== undefined ? matrix.f : 0) + - ')'; - }; + var f = pow(10, precision || 0); + this.x = round(this.x * f) / f; + this.y = round(this.y * f) / f; + return this; + }, - V.parseTransformString = function(transform) { + // Scale point with origin. + scale: function(sx, sy, origin) { - var translate, rotate, scale; + origin = (origin && new Point(origin)) || new Point(0, 0); + this.x = origin.x + sx * (this.x - origin.x); + this.y = origin.y + sy * (this.y - origin.y); + return this; + }, - if (transform) { + snapToGrid: function(gx, gy) { - var separator = V.transformSeparatorRegex; + this.x = snapToGrid(this.x, gx); + this.y = snapToGrid(this.y, gy || gx); + return this; + }, - // Allow reading transform string with a single matrix - if (transform.trim().indexOf('matrix') >= 0) { + // Compute the angle between me and `p` and the x axis. + // (cartesian-to-polar coordinates conversion) + // Return theta angle in degrees. + theta: function(p) { - var matrix = V.transformStringToMatrix(transform); - var decomposedMatrix = V.decomposeMatrix(matrix); + p = new Point(p); - translate = [decomposedMatrix.translateX, decomposedMatrix.translateY]; - scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]; - rotate = [decomposedMatrix.rotation]; + // Invert the y-axis. + var y = -(p.y - this.y); + var x = p.x - this.x; + var rad = atan2(y, x); // defined for all 0 corner cases - var transformations = []; - if (translate[0] !== 0 || translate[0] !== 0) { - transformations.push('translate(' + translate + ')'); - } - if (scale[0] !== 1 || scale[1] !== 1) { - transformations.push('scale(' + scale + ')'); - } - if (rotate[0] !== 0) { - transformations.push('rotate(' + rotate + ')'); - } - transform = transformations.join(' '); + // Correction for III. and IV. quadrant. + if (rad < 0) { + rad = 2 * PI + rad; + } - } else { + return 180 * rad / PI; + }, - var translateMatch = transform.match(/translate\((.*?)\)/); - if (translateMatch) { - translate = translateMatch[1].split(separator); - } - var rotateMatch = transform.match(/rotate\((.*?)\)/); - if (rotateMatch) { - rotate = rotateMatch[1].split(separator); - } - var scaleMatch = transform.match(/scale\((.*?)\)/); - if (scaleMatch) { - scale = scaleMatch[1].split(separator); - } - } - } + // Compute the angle between vector from me to p1 and the vector from me to p2. + // ordering of points p1 and p2 is important! + // theta function's angle convention: + // returns angles between 0 and 180 when the angle is counterclockwise + // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones + // returns NaN if any of the points p1, p2 is coincident with this point + angleBetween: function(p1, p2) { - var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; + var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - return { - value: transform, - translate: { - tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, - ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 - }, - rotate: { - angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, - cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, - cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined - }, - scale: { - sx: sx, - sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx + if (angleBetween < 0) { + angleBetween += 360; // correction to keep angleBetween between 0 and 360 } - }; - }; - V.deltaTransformPoint = function(matrix, point) { + return angleBetween; + }, - var dx = point.x * matrix.a + point.y * matrix.c + 0; - var dy = point.x * matrix.b + point.y * matrix.d + 0; - return { x: dx, y: dy }; - }; + // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. + // Returns NaN if p is at 0,0. + vectorAngle: function(p) { - V.decomposeMatrix = function(matrix) { + var zero = new Point(0,0); + return zero.angleBetween(this, p); + }, - // @see https://gist.github.com/2052247 + toJSON: function() { - // calculate delta transform point - var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); - var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); + return { x: this.x, y: this.y }; + }, - // calculate skew - var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); - var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); + // Converts rectangular to polar coordinates. + // An origin can be specified, otherwise it's 0@0. + toPolar: function(o) { - return { + o = (o && new Point(o)) || new 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(new Point(x, y))); + return this; + }, - translateX: matrix.e, - translateY: matrix.f, - scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), - scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), - skewX: skewX, - skewY: skewY, - rotation: skewX // rotation is the same as skew x - }; - }; + toString: function() { - // Return the `scale` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToScale = function(matrix) { + return this.x + '@' + this.y; + }, - var a,b,c,d; - if (matrix) { - a = V.isUndefined(matrix.a) ? 1 : matrix.a; - d = V.isUndefined(matrix.d) ? 1 : matrix.d; - b = matrix.b; - c = matrix.c; - } else { - a = d = 1; - } - return { - sx: b ? Math.sqrt(a * a + b * b) : a, - sy: c ? Math.sqrt(c * c + d * d) : d - }; - }, + update: function(x, y) { - // Return the `rotate` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToRotate = function(matrix) { + this.x = x || 0; + this.y = y || 0; + return this; + }, - var p = { x: 0, y: 1 }; - if (matrix) { - p = V.deltaTransformPoint(matrix, p); - } + // Returns the dot product of this point with given other point + dot: function(p) { - return { - angle: g.normalizeAngle(g.toDeg(Math.atan2(p.y, p.x)) - 90) - }; - }, + return p ? (this.x * p.x + this.y * p.y) : NaN; + }, - // Return the `translate` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToTranslate = function(matrix) { + // Returns the cross product of this point relative to two other points + // this point is the common point + // point p1 lies on the first vector, point p2 lies on the second vector + // watch out for the ordering of points p1 and p2! + // positive result indicates a clockwise ("right") turn from first to second vector + // negative result indicates a counterclockwise ("left") turn from first to second vector + // note that the above directions are reversed from the usual answer on the Internet + // that is because we are in a left-handed coord system (because the y-axis points downward) + cross: function(p1, p2) { - return { - tx: (matrix && matrix.e) || 0, - ty: (matrix && matrix.f) || 0 - }; - }, + return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; + }, - V.isV = function(object) { - return object instanceof V; + // Linear interpolation + lerp: function(p, t) { + + var x = this.x; + var y = this.y; + return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y); + } }; - // For backwards compatibility: - V.isVElement = V.isV; + Point.prototype.translate = Point.prototype.offset; - var svgDocument = V('svg').node; + var Rect = g.Rect = function(x, y, w, h) { - V.createSVGMatrix = function(matrix) { + if (!(this instanceof Rect)) { + return new Rect(x, y, w, h); + } - var svgMatrix = svgDocument.createSVGMatrix(); - for (var component in matrix) { - svgMatrix[component] = matrix[component]; + if ((Object(x) === x)) { + y = x.y; + w = x.width; + h = x.height; + x = x.x; } - return svgMatrix; + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; + this.width = w === undefined ? 0 : w; + this.height = h === undefined ? 0 : h; }; - V.createSVGTransform = function(matrix) { - - if (!V.isUndefined(matrix)) { - - if (!(matrix instanceof SVGMatrix)) { - matrix = V.createSVGMatrix(matrix); - } - - return svgDocument.createSVGTransformFromMatrix(matrix); - } + Rect.fromEllipse = function(e) { - return svgDocument.createSVGTransform(); + e = new Ellipse(e); + return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); }; - V.createSVGPoint = function(x, y) { + Rect.prototype = { - var p = svgDocument.createSVGPoint(); - p.x = x; - p.y = y; - return p; - }; + // 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) { - V.transformRect = function(r, matrix) { + if (!angle) return this.clone(); - var p = svgDocument.createSVGPoint(); + 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 new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); + }, - p.x = r.x; - p.y = r.y; - var corner1 = p.matrixTransform(matrix); + bottomLeft: function() { - p.x = r.x + r.width; - p.y = r.y; - var corner2 = p.matrixTransform(matrix); + return new Point(this.x, this.y + this.height); + }, - p.x = r.x + r.width; - p.y = r.y + r.height; - var corner3 = p.matrixTransform(matrix); + bottomLine: function() { - p.x = r.x; - p.y = r.y + r.height; - var corner4 = p.matrixTransform(matrix); + return new Line(this.bottomLeft(), this.bottomRight()); + }, - var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x); - var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x); - var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y); - var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y); + bottomMiddle: function() { - return g.Rect(minX, minY, maxX - minX, maxY - minY); - }; + return new Point(this.x + this.width / 2, this.y + this.height); + }, - V.transformPoint = function(p, matrix) { + center: function() { - return g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); - }; + return new Point(this.x + this.width / 2, this.y + this.height / 2); + }, - // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to - // an object (`{ fill: 'blue', stroke: 'red' }`). - V.styleToObject = function(styleString) { - var ret = {}; - var styles = styleString.split(';'); - for (var i = 0; i < styles.length; i++) { - var style = styles[i]; - var pair = style.split('='); - ret[pair[0].trim()] = pair[1].trim(); - } - return ret; - }; + clone: function() { - // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js - V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { + return new Rect(this); + }, - var svgArcMax = 2 * Math.PI - 1e-6; - var r0 = innerRadius; - var r1 = outerRadius; - var a0 = startAngle; - var a1 = endAngle; - var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); - var df = da < Math.PI ? '0' : '1'; - var c0 = Math.cos(a0); - var s0 = Math.sin(a0); - var c1 = Math.cos(a1); - var s1 = Math.sin(a1); + // @return {bool} true if point p is insight me + containsPoint: function(p) { - return (da >= svgArcMax) - ? (r0 - ? 'M0,' + r1 - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 - + 'M0,' + r0 - + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) - + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 - + 'Z' - : 'M0,' + r1 - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 - + 'Z') - : (r0 - ? 'M' + r1 * c0 + ',' + r1 * s0 - + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 - + 'L' + r0 * c1 + ',' + r0 * s1 - + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 - + 'Z' - : 'M' + r1 * c0 + ',' + r1 * s0 - + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 - + 'L0,0' - + 'Z'); - }; + p = new Point(p); + return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + }, - // Merge attributes from object `b` with attributes in object `a`. - // Note that this modifies the object `a`. - // Also important to note that attributes are merged but CSS classes are concatenated. - V.mergeAttrs = function(a, b) { + // @return {bool} true if rectangle `r` is inside me. + containsRect: function(r) { - for (var attr in b) { + var r0 = new Rect(this).normalize(); + var r1 = new Rect(r).normalize(); + var w0 = r0.width; + var h0 = r0.height; + var w1 = r1.width; + var h1 = r1.height; - if (attr === 'class') { - // Concatenate classes. - a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; - } else if (attr === 'style') { - // `style` attribute can be an object. - if (V.isObject(a[attr]) && V.isObject(b[attr])) { - // `style` stored in `a` is an object. - a[attr] = V.mergeAttrs(a[attr], b[attr]); - } else if (V.isObject(a[attr])) { - // `style` in `a` is an object but it's a string in `b`. - // Convert the style represented as a string to an object in `b`. - a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); - } else if (V.isObject(b[attr])) { - // `style` in `a` is a string, in `b` it's an object. - a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); - } else { - // Both styles are strings. - a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); - } - } else { - a[attr] = b[attr]; + if (!w0 || !h0 || !w1 || !h1) { + // At least one of the dimensions is 0 + return false; } - } - return a; - }; + var x0 = r0.x; + var y0 = r0.y; + var x1 = r1.x; + var y1 = r1.y; - V.annotateString = function(t, annotations, opt) { + w1 += x1; + w0 += x0; + h1 += y1; + h0 += y0; - annotations = annotations || []; - opt = opt || {}; + return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; + }, - var offset = opt.offset || 0; - var compacted = []; - var batch; - var ret = []; - var item; - var prev; + corner: function() { - for (var i = 0; i < t.length; i++) { + return new Point(this.x + this.width, this.y + this.height); + }, - item = ret[i] = t[i]; + // @return {boolean} true if rectangles are equal. + equals: function(r) { - for (var j = 0; j < annotations.length; j++) { + var mr = (new Rect(this)).normalize(); + var nr = (new Rect(r)).normalize(); + return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height; + }, - var annotation = annotations[j]; - var start = annotation.start + offset; - var end = annotation.end + offset; + // @return {rect} if rectangles intersect, {null} if not. + intersect: function(r) { - if (i >= start && i < end) { - // Annotation applies. - if (V.isObject(item)) { - // There is more than one annotation to be applied => Merge attributes. - item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); - } else { - item = ret[i] = { t: t[i], attrs: annotation.attrs }; - } - if (opt.includeAnnotationIndices) { - (item.annotations || (item.annotations = [])).push(j); - } - } - } + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = r.origin(); + var rCorner = r.corner(); - prev = ret[i - 1]; + // No intersection found + if (rCorner.x <= myOrigin.x || + rCorner.y <= myOrigin.y || + rOrigin.x >= myCorner.x || + rOrigin.y >= myCorner.y) return null; - if (!prev) { + var x = max(myOrigin.x, rOrigin.x); + var y = max(myOrigin.y, rOrigin.y); - batch = item; + return new Rect(x, y, min(myCorner.x, rCorner.x) - x, min(myCorner.y, rCorner.y) - y); + }, - } else if (V.isObject(item) && V.isObject(prev)) { - // Both previous item and the current one are annotations. If the attributes - // didn't change, merge the text. - if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { - batch.t += item.t; - } else { - compacted.push(batch); - batch = item; - } + intersectionWithLine: function(line) { - } else if (V.isObject(item)) { - // Previous item was a string, current item is an annotation. - compacted.push(batch); - batch = item; + var r = this; + var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; + var points = []; + var dedupeArr = []; + var pt, i; - } else if (V.isObject(prev)) { - // Previous item was an annotation, current item is a string. - compacted.push(batch); - batch = item; + var n = rectLines.length; + for (i = 0; i < n; i ++) { - } else { - // Both previous and current item are strings. - batch = (batch || '') + item; + pt = line.intersect(rectLines[i]); + if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) { + points.push(pt); + dedupeArr.push(pt.toString()); + } } - } - if (batch) { - compacted.push(batch); - } - - return compacted; - }; + return points.length > 0 ? points : null; + }, - V.findAnnotationsAtIndex = function(annotations, index) { + // 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) { - var found = []; + p = new Point(p); + var center = new Point(this.x + this.width / 2, this.y + this.height / 2); + var result; - if (annotations) { + if (angle) p.rotate(center, angle); - annotations.forEach(function(annotation) { + // (clockwise, starting from the top side) + var sides = [ + this.topLine(), + this.rightLine(), + this.bottomLine(), + this.leftLine() + ]; + var connector = new Line(center, p); - if (annotation.start < index && index <= annotation.end) { - found.push(annotation); + 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; + }, - return found; - }; + leftLine: function() { - V.findAnnotationsBetweenIndexes = function(annotations, start, end) { + return new Line(this.topLeft(), this.bottomLeft()); + }, - var found = []; + leftMiddle: function() { - if (annotations) { + return new Point(this.x , this.y + this.height / 2); + }, - annotations.forEach(function(annotation) { + // Move and expand me. + // @param r {rectangle} representing deltas + moveAndExpand: function(r) { - if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { - found.push(annotation); - } - }); - } + this.x += r.x || 0; + this.y += r.y || 0; + this.width += r.width || 0; + this.height += r.height || 0; + return this; + }, - return found; - }; + // Offset me by the specified amount. + offset: function(dx, dy) { - // Shift all the text annotations after character `index` by `offset` positions. - V.shiftAnnotations = function(annotations, index, offset) { + // pretend that this is a point and call offset() + // rewrites x and y according to dx and dy + return Point.prototype.offset.call(this, dx, dy); + }, - if (annotations) { + // 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) { - annotations.forEach(function(annotation) { + if (dx === undefined) { + dx = 0; + } - if (annotation.start < index && annotation.end >= index) { - annotation.end += offset; - } else if (annotation.start >= index) { - annotation.start += offset; - annotation.end += offset; - } - }); - } + if (dy === undefined) { + dy = dx; + } - return annotations; - }; + this.x -= dx; + this.y -= dy; + this.width += 2 * dx; + this.height += 2 * dy; - V.convertLineToPathData = function(line) { + return this; + }, - line = V(line); - var d = [ - 'M', line.attr('x1'), line.attr('y1'), - 'L', line.attr('x2'), line.attr('y2') - ].join(' '); - return d; - }; + // 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() { - V.convertPolygonToPathData = function(polygon) { + 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; + }, - var points = V.getPointsFromSvgNode(V(polygon).node); + origin: function() { - if (!(points.length > 0)) return null; + return new Point(this.x, this.y); + }, - return V.svgPointsToPath(points) + ' Z'; - }; + // @return {point} a point on my boundary nearest to the given point. + // @see Squeak Smalltalk, Rectangle>>pointNearestTo: + pointNearestToPoint: function(point) { - V.convertPolylineToPathData = function(polyline) { + point = new Point(point); + if (this.containsPoint(point)) { + var side = this.sideNearestToPoint(point); + switch (side){ + case 'right': return new Point(this.x + this.width, point.y); + case 'left': return new Point(this.x, point.y); + case 'bottom': return new Point(point.x, this.y + this.height); + case 'top': return new Point(point.x, this.y); + } + } + return point.adhereToRect(this); + }, - var points = V.getPointsFromSvgNode(V(polyline).node); + rightLine: function() { - if (!(points.length > 0)) return null; + return new Line(this.topRight(), this.bottomRight()); + }, - return V.svgPointsToPath(points); - }; + rightMiddle: function() { - V.svgPointsToPath = function(points) { + return new Point(this.x + this.width, this.y + this.height / 2); + }, - var i; + round: function(precision) { - for (i = 0; i < points.length; i++) { - points[i] = points[i].x + ' ' + points[i].y; - } + 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; + }, - return 'M ' + points.join(' L'); - }; + // Scale rectangle with origin. + scale: function(sx, sy, origin) { - V.getPointsFromSvgNode = function(node) { + origin = this.origin().scale(sx, sy, origin); + this.x = origin.x; + this.y = origin.y; + this.width *= sx; + this.height *= sy; + return this; + }, - node = V.toNode(node); - var points = []; - var nodePoints = node.points; - if (nodePoints) { - for (var i = 0; i < nodePoints.numberOfItems; i++) { - points.push(nodePoints.getItem(i)); - } - } + maxRectScaleToFit: function(rect, origin) { - return points; - }; + rect = new Rect(rect); + origin || (origin = rect.center()); - V.KAPPA = 0.5522847498307935; + var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; + var ox = origin.x; + var oy = origin.y; - V.convertCircleToPathData = function(circle) { + // 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. - circle = V(circle); - var cx = parseFloat(circle.attr('cx')) || 0; - var cy = parseFloat(circle.attr('cy')) || 0; - var r = parseFloat(circle.attr('r')); - var cd = r * V.KAPPA; // Control distance. + sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; - var d = [ - 'M', cx, cy - r, // Move to the first point. - 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. - 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. - 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. - 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. - 'Z' - ].join(' '); - return d; - }; + // Top Left + var p1 = rect.topLeft(); + 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.bottomRight(); + 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); + } - V.convertEllipseToPathData = function(ellipse) { + return { + sx: min(sx1, sx2, sx3, sx4), + sy: min(sy1, sy2, sy3, sy4) + }; + }, - ellipse = V(ellipse); - var cx = parseFloat(ellipse.attr('cx')) || 0; - var cy = parseFloat(ellipse.attr('cy')) || 0; - var rx = parseFloat(ellipse.attr('rx')); - var ry = parseFloat(ellipse.attr('ry')) || rx; - var cdx = rx * V.KAPPA; // Control distance x. - var cdy = ry * V.KAPPA; // Control distance y. + maxRectUniformScaleToFit: function(rect, origin) { - var d = [ - 'M', cx, cy - ry, // Move to the first point. - 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. - 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. - 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. - 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. - 'Z' - ].join(' '); - return d; - }; + var scale = this.maxRectScaleToFit(rect, origin); + return min(scale.sx, scale.sy); + }, - V.convertRectToPathData = function(rect) { + // @return {string} (left|right|top|bottom) side which is nearest to point + // @see Squeak Smalltalk, Rectangle>>sideNearestTo: + sideNearestToPoint: function(point) { - rect = V(rect); + point = new 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'; - return V.rectToPath({ - x: parseFloat(rect.attr('x')) || 0, - y: parseFloat(rect.attr('y')) || 0, - width: parseFloat(rect.attr('width')) || 0, - height: parseFloat(rect.attr('height')) || 0, - rx: parseFloat(rect.attr('rx')) || 0, - ry: parseFloat(rect.attr('ry')) || 0 - }); - }; - - // Convert a rectangle to SVG path commands. `r` is an object of the form: - // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, - // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for - // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle - // that has only `rx` and `ry` attributes). - V.rectToPath = function(r) { - - var d; - var x = r.x; - var y = r.y; - var width = r.width; - var height = r.height; - var topRx = Math.min(r.rx || r['top-rx'] || 0, width / 2); - var bottomRx = Math.min(r.rx || r['bottom-rx'] || 0, width / 2); - var topRy = Math.min(r.ry || r['top-ry'] || 0, height / 2); - var bottomRy = Math.min(r.ry || r['bottom-ry'] || 0, height / 2); + if (distToRight < closest) { + closest = distToRight; + side = 'right'; + } + if (distToTop < closest) { + closest = distToTop; + side = 'top'; + } + if (distToBottom < closest) { + closest = distToBottom; + side = 'bottom'; + } + return side; + }, - if (topRx || bottomRx || topRy || bottomRy) { - d = [ - 'M', x, y + topRy, - 'v', height - topRy - bottomRy, - 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, - 'h', width - 2 * bottomRx, - 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, - 'v', -(height - bottomRy - topRy), - 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, - 'h', -(width - 2 * topRx), - 'a', topRx, topRy, 0, 0, 0, -topRx, topRy, - 'Z' - ]; - } else { - d = [ - 'M', x, y, - 'H', x + width, - 'V', y + height, - 'H', x, - 'V', y, - 'Z' - ]; - } + snapToGrid: function(gx, gy) { - return d.join(' '); - }; + 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; + }, - V.namespace = ns; + topLine: function() { - return V; + return new Line(this.topLeft(), this.topRight()); + }, -})(); + topMiddle: function() { + return new Point(this.x + this.width / 2, this.y); + }, -// Global namespace. + topRight: function() { -var joint = { + return new Point(this.x + this.width, this.y); + }, - version: '2.0.1', + toJSON: function() { - config: { - // The class name prefix config is for advanced use only. - // Be aware that if you change the prefix, the JointJS CSS will no longer function properly. - classNamePrefix: 'joint-', - defaultTheme: 'default' - }, + return { x: this.x, y: this.y, width: this.width, height: this.height }; + }, - // `joint.dia` namespace. - dia: {}, + toString: function() { - // `joint.ui` namespace. - ui: {}, + return this.origin().toString() + ' ' + this.corner().toString(); + }, - // `joint.layout` namespace. - layout: {}, + // @return {rect} representing the union of both rectangles. + union: function(rect) { - // `joint.shapes` namespace. - shapes: {}, + rect = new Rect(rect); + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = rect.origin(); + var rCorner = rect.corner(); - // `joint.format` namespace. - format: {}, + var originX = min(myOrigin.x, rOrigin.x); + var originY = min(myOrigin.y, rOrigin.y); + var cornerX = max(myCorner.x, rCorner.x); + var cornerY = max(myCorner.y, rCorner.y); - // `joint.connectors` namespace. - connectors: {}, + return new Rect(originX, originY, cornerX - originX, cornerY - originY); + } + }; - // `joint.highlighters` namespace. - highlighters: {}, + Rect.prototype.bottomRight = Rect.prototype.corner; - // `joint.routers` namespace. - routers: {}, + Rect.prototype.topLeft = Rect.prototype.origin; - // `joint.mvc` namespace. - mvc: { - views: {} - }, + Rect.prototype.translate = Rect.prototype.offset; - setTheme: function(theme, opt) { + var Polyline = g.Polyline = function(points) { - opt = opt || {}; + if (!(this instanceof Polyline)) { + return new Polyline(points); + } - joint.util.invoke(joint.mvc.views, 'setTheme', theme, opt); + if (typeof points === 'string') { + return new Polyline.parse(points); + } - // Update the default theme on the view prototype. - joint.mvc.View.prototype.defaultTheme = theme; - }, + this.points = (Array.isArray(points) ? points.map(Point) : []); + }; - // `joint.env` namespace. - env: { + Polyline.parse = function(svgString) { - _results: {}, + if (svgString === '') return new Polyline(); - _tests: { + var points = []; - svgforeignobject: function() { - return !!document.createElementNS && - /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); - } - }, + var coords = svgString.split(/\s|,/); + var n = coords.length; + for (var i = 0; i < n; i += 2) { + points.push({ x: +coords[i], y: +coords[i + 1] }); + } - addTest: function(name, fn) { + return new Polyline(points); + }; - return joint.env._tests[name] = fn; - }, + Polyline.prototype = { - test: function(name) { + bbox: function() { - var fn = joint.env._tests[name]; + var x1 = Infinity; + var x2 = -Infinity; + var y1 = Infinity; + var y2 = -Infinity; - if (!fn) { - throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - var result = joint.env._results[name]; + for (var i = 0; i < numPoints; i++) { - if (typeof result !== 'undefined') { - return result; - } + var point = points[i]; + var x = point.x; + var y = point.y; - try { - result = fn(); - } catch (error) { - result = false; + if (x < x1) x1 = x; + if (x > x2) x2 = x; + if (y < y1) y1 = y; + if (y > y2) y2 = y; } - // Cache the test result. - joint.env._results[name] = result; + return new Rect(x1, y1, x2 - x1, y2 - y1); + }, - return result; - } - }, + clone: function() { - util: { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty - // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. - hashCode: function(str) { + var newPoints = []; + for (var i = 0; i < numPoints; i++) { - var hash = 0; - if (str.length == 0) return hash; - for (var i = 0; i < str.length; i++) { - var c = str.charCodeAt(i); - hash = ((hash << 5) - hash) + c; - hash = hash & hash; // Convert to 32bit integer + var point = points[i].clone(); + newPoints.push(point); } - return hash; - }, - - getByPath: function(obj, path, delim) { - - var keys = Array.isArray(path) ? path.slice() : path.split(delim || '/'); - var key; - while (keys.length) { - key = keys.shift(); - if (Object(obj) === obj && key in obj) { - obj = obj[key]; - } else { - return undefined; - } - } - return obj; + return new Polyline(newPoints); }, - setByPath: function(obj, path, value, delim) { - - var keys = Array.isArray(path) ? path : path.split(delim || '/'); - - var diver = obj; - var i = 0; + closestPoint: function(p) { - for (var len = keys.length; i < len - 1; i++) { - // diver creates an empty object if there is no nested object under such a key. - // This means that one can populate an empty nested object with setByPath(). - diver = diver[keys[i]] || (diver[keys[i]] = {}); - } - diver[keys[len - 1]] = value; + var cpLength = this.closestPointLength(p); - return obj; + return this.pointAtLength(cpLength); }, - unsetByPath: function(obj, path, delim) { + closestPointLength: function(p) { - delim = delim || '/'; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + if (numPoints === 1) return 0; // if there is only one point - var pathArray = Array.isArray(path) ? path.slice() : path.split(delim); + var cpLength; + var minSqrDistance = Infinity; + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { - var propertyToRemove = pathArray.pop(); - if (pathArray.length > 0) { + var line = new Line(points[i], points[i + 1]); + var lineLength = line.length(); - // unsetting a nested attribute - var parent = joint.util.getByPath(obj, pathArray, delim); + var cpNormalizedLength = line.closestPointNormalizedLength(p); + var cp = line.pointAt(cpNormalizedLength); - if (parent) { - delete parent[propertyToRemove]; + var sqrDistance = cp.squaredDistance(p); + if (sqrDistance < minSqrDistance) { + minSqrDistance = sqrDistance; + cpLength = length + (cpNormalizedLength * lineLength); } - } else { - - // unsetting a primitive attribute - delete obj[propertyToRemove]; + length += lineLength; } - return obj; + return cpLength; }, - flattenObject: function(obj, delim, stop) { - - delim = delim || '/'; - var ret = {}; + closestPointNormalizedLength: function(p) { - for (var key in obj) { + var cpLength = this.closestPointLength(p); + if (cpLength === 0) return 0; // shortcut - if (!obj.hasOwnProperty(key)) continue; + var length = this.length(); + if (length === 0) return 0; // prevents division by zero - var shouldGoDeeper = typeof obj[key] === 'object'; - if (shouldGoDeeper && stop && stop(obj[key])) { - shouldGoDeeper = false; - } + return cpLength / length; + }, - if (shouldGoDeeper) { + closestPointTangent: function(p) { - var flatObject = this.flattenObject(obj[key], delim, stop); + var cpLength = this.closestPointLength(p); - for (var flatKey in flatObject) { - if (!flatObject.hasOwnProperty(flatKey)) continue; - ret[key + delim + flatKey] = flatObject[flatKey]; - } + return this.tangentAtLength(cpLength); + }, - } else { + // Returns a convex-hull polyline from this polyline. + // Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan). + // Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise. + // Minimal polyline is found (only vertices of the hull are reported, no collinear points). + convexHull: function() { - ret[key] = obj[key]; + var i; + var n; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + // step 1: find the starting point - point with the lowest y (if equality, highest x) + var startPoint; + for (i = 0; i < numPoints; i++) { + if (startPoint === undefined) { + // if this is the first point we see, set it as start point + startPoint = points[i]; + + } else if (points[i].y < startPoint.y) { + // start point should have lowest y from all points + startPoint = points[i]; + + } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { + // if two points have the lowest y, choose the one that has highest x + // there are no points to the right of startPoint - no ambiguity about theta 0 + // if there are several coincident start point candidates, first one is reported + startPoint = points[i]; } } - return ret; - }, + // step 2: sort the list of points + // sorting by angle between line from startPoint to point and the x-axis (theta) - uuid: function() { + // step 2a: create the point records = [point, originalIndex, angle] + var sortedPointRecords = []; + for (i = 0; i < numPoints; i++) { - // credit: http://stackoverflow.com/posts/2117523/revisions + var angle = startPoint.theta(points[i]); + if (angle === 0) { + angle = 360; // give highest angle to start point + // the start point will end up at end of sorted list + // the start point will end up at beginning of hull points list + } - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random() * 16|0; - var v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); + var entry = [points[i], i, angle]; + sortedPointRecords.push(entry); + } + + // step 2b: sort the list in place + sortedPointRecords.sort(function(record1, record2) { + // returning a negative number here sorts record1 before record2 + // if first angle is smaller than second, first angle should come before second + + var sortOutput = record1[2] - record2[2]; // negative if first angle smaller + if (sortOutput === 0) { + // if the two angles are equal, sort by originalIndex + sortOutput = record2[1] - record1[1]; // negative if first index larger + // coincident points will be sorted in reverse-numerical order + // so the coincident points with lower original index will be considered first + } + + return sortOutput; }); - }, - // Generate global unique id for obj and store it as a property of the object. - guid: function(obj) { + // step 2c: duplicate start record from the top of the stack to the bottom of the stack + if (sortedPointRecords.length > 2) { + var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; + sortedPointRecords.unshift(startPointRecord); + } - this.guid.id = this.guid.id || 1; - obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); - return obj.id; - }, + // step 3a: go through sorted points in order and find those with right turns + // we want to get our results in clockwise order + var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull + var hullPointRecords = []; // stack of records with right turns - hull point candidates - toKebabCase: function(string) { + var currentPointRecord; + var currentPoint; + var lastHullPointRecord; + var lastHullPoint; + var secondLastHullPointRecord; + var secondLastHullPoint; + while (sortedPointRecords.length !== 0) { - return string.replace(/[A-Z]/g, '-$&').toLowerCase(); - }, + currentPointRecord = sortedPointRecords.pop(); + currentPoint = currentPointRecord[0]; - // Copy all the properties to the first argument from the following arguments. - // All the properties will be overwritten by the properties from the following - // arguments. Inherited properties are ignored. - mixin: _.assign, + // check if point has already been discarded + // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' + if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { + // this point had an incorrect turn at some previous iteration of this loop + // this disqualifies it from possibly being on the hull + continue; + } - // Copy all properties to the first argument from the following - // arguments only in case if they don't exists in the first argument. - // All the function propererties in the first argument will get - // additional property base pointing to the extenders same named - // property function's call method. - supplement: _.defaults, + var correctTurnFound = false; + while (!correctTurnFound) { - // Same as `mixin()` but deep version. - deepMixin: _.mixin, + if (hullPointRecords.length < 2) { + // not enough points for comparison, just add current point + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; - // Same as `supplement()` but deep version. - deepSupplement: _.defaultsDeep, + } else { + lastHullPointRecord = hullPointRecords.pop(); + lastHullPoint = lastHullPointRecord[0]; + secondLastHullPointRecord = hullPointRecords.pop(); + secondLastHullPoint = secondLastHullPointRecord[0]; - normalizeEvent: function(evt) { + var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); - var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; - if (touchEvt) { - for (var property in evt) { - // copy all the properties from the input event that are not - // defined on the touch event (functions included). - if (touchEvt[property] === undefined) { - touchEvt[property] = evt[property]; - } - } - return touchEvt; - } + if (crossProduct < 0) { + // found a right turn + hullPointRecords.push(secondLastHullPointRecord); + hullPointRecords.push(lastHullPointRecord); + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; - return evt; - }, + } else if (crossProduct === 0) { + // the three points are collinear + // three options: + // there may be a 180 or 0 degree angle at lastHullPoint + // or two of the three points are coincident + var THRESHOLD = 1e-10; // we have to take rounding errors into account + var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); + if (abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 + // if the cross product is 0 because the angle is 180 degrees + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found - nextFrame: (function() { + } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { + // if the cross product is 0 because two points are the same + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found - var raf; + } else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 + // if the cross product is 0 because the angle is 0 degrees + // remove last hull point from hull BUT do not discard it + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // put last hull point back into the sorted point records list + sortedPointRecords.push(lastHullPointRecord); + // we are switching the order of the 0deg and 180deg points + // correct turn not found + } - if (typeof window !== 'undefined') { + } else { + // found a left turn + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter of loop) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + } + } + } + } + // at this point, hullPointRecords contains the output points in clockwise order + // the points start with lowest-y,highest-x startPoint, and end at the same point - raf = window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame; + // step 3b: remove duplicated startPointRecord from the end of the array + if (hullPointRecords.length > 2) { + hullPointRecords.pop(); } - if (!raf) { + // step 4: find the lowest originalIndex record and put it at the beginning of hull + var lowestHullIndex; // the lowest originalIndex on the hull + var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex + n = hullPointRecords.length; + for (i = 0; i < n; i++) { - var lastTime = 0; + var currentHullIndex = hullPointRecords[i][1]; - raf = function(callback) { + if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { + lowestHullIndex = currentHullIndex; + indexOfLowestHullIndexRecord = i; + } + } - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + var hullPointRecordsReordered = []; + if (indexOfLowestHullIndexRecord > 0) { + var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); + var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); + hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - lastTime = currTime + timeToCall; + } else { + hullPointRecordsReordered = hullPointRecords; + } - return id; - }; + var hullPoints = []; + n = hullPointRecordsReordered.length; + for (i = 0; i < n; i++) { + hullPoints.push(hullPointRecordsReordered[i][0]); } - return function(callback, context) { - return context - ? raf(callback.bind(context)) - : raf(callback); - }; + return new Polyline(hullPoints); + }, - })(), + // Checks whether two polylines are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { - cancelFrame: (function() { + if (!p) return false; - var caf; - var client = typeof window != 'undefined'; + var points = this.points; + var otherPoints = p.points; - if (client) { + var numPoints = points.length; + if (otherPoints.length !== numPoints) return false; // if the two polylines have different number of points, they cannot be equal - caf = window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.webkitCancelRequestAnimationFrame || - window.msCancelAnimationFrame || - window.msCancelRequestAnimationFrame || - window.oCancelAnimationFrame || - window.oCancelRequestAnimationFrame || - window.mozCancelAnimationFrame || - window.mozCancelRequestAnimationFrame; - } + for (var i = 0; i < numPoints; i++) { - caf = caf || clearTimeout; + var point = points[i]; + var otherPoint = p.points[i]; - return client ? caf.bind(window) : caf; + // as soon as an inequality is found in points, return false + if (!point.equals(otherPoint)) return false; + } - })(), + // if no inequality found in points, return true + return true; + }, - shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { + isDifferentiable: function() { - var bbox; - var spot; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return false; - if (!magnet) { + var n = numPoints - 1; + for (var i = 0; i < n; i++) { - // There is no magnet, try to make the best guess what is the - // wrapping SVG element. This is because we want this "smart" - // connection points to work out of the box without the - // programmer to put magnet marks to any of the subelements. - // For example, we want the functoin to work on basic.Path elements - // without any special treatment of such elements. - // The code below guesses the wrapping element based on - // one simple assumption. The wrapping elemnet is the - // first child of the scalable group if such a group exists - // or the first child of the rotatable group if not. - // This makese sense because usually the wrapping element - // is below any other sub element in the shapes. - var scalable = view.$('.scalable')[0]; - var rotatable = view.$('.rotatable')[0]; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); - if (scalable && scalable.firstChild) { + // as soon as a differentiable line is found between two points, return true + if (line.isDifferentiable()) return true; + } - magnet = scalable.firstChild; + // if no differentiable line is found between pairs of points, return false + return false; + }, - } else if (rotatable && rotatable.firstChild) { + length: function() { - magnet = rotatable.firstChild; - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + length += points[i].distance(points[i + 1]); } - if (magnet) { + return length; + }, - spot = V(magnet).findIntersection(reference, linkView.paper.viewport); - if (!spot) { - bbox = V(magnet).getBBox({ target: linkView.paper.viewport }); - } + pointAt: function(ratio) { - } else { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point - bbox = view.model.getBBox(); - spot = bbox.intersectionWithLineFromCenterToPoint(reference); - } - return spot || bbox.center(); + if (ratio <= 0) return points[0].clone(); + if (ratio >= 1) return points[numPoints - 1].clone(); + + var polylineLength = this.length(); + var length = polylineLength * ratio; + + return this.pointAtLength(length); }, - parseCssNumeric: function(strValue, restrictUnits) { + pointAtLength: function(length) { - restrictUnits = restrictUnits || []; - var cssNumeric = { value: parseFloat(strValue) }; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point - if (Number.isNaN(cssNumeric.value)) { - return null; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - var validUnitsExp = restrictUnits.join('|'); + var l = 0; + var n = numPoints - 1; + for (var i = (fromStart ? 0 : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - if (joint.util.isString(strValue)) { - var matches = new RegExp('(\\d+)(' + validUnitsExp + ')$').exec(strValue); - if (!matches) { - return null; - } - if (matches[2]) { - cssNumeric.unit = matches[2]; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (length <= (l + d)) { + return line.pointAtLength((fromStart ? 1 : -1) * (length - l)); } + + l += d; } - return cssNumeric; - }, - breakText: function(text, size, styles, opt) { + // if length requested is higher than the length of the polyline, return last endpoint + var lastPoint = (fromStart ? points[numPoints - 1] : points[0]); + return lastPoint.clone(); + }, - opt = opt || {}; + scale: function(sx, sy, origin) { - var width = size.width; - var height = size.height; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty - var svgDocument = opt.svgDocument || V('svg').node; - var textElement = V('').attr(styles || {}).node; - var textSpan = textElement.firstChild; - var textNode = document.createTextNode(''); + for (var i = 0; i < numPoints; i++) { + points[i].scale(sx, sy, origin); + } - // Prevent flickering - textElement.style.opacity = 0; - // Prevent FF from throwing an uncaught exception when `getBBox()` - // called on element that is not in the render tree (is not measurable). - // .getComputedTextLength() returns always 0 in this case. - // Note that the `textElement` resp. `textSpan` can become hidden - // when it's appended to the DOM and a `display: none` CSS stylesheet - // rule gets applied. - textElement.style.display = 'block'; - textSpan.style.display = 'block'; + return this; + }, - textSpan.appendChild(textNode); - svgDocument.appendChild(textElement); + tangentAt: function(ratio) { - if (!opt.svgDocument) { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point - document.body.appendChild(svgDocument); - } + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - var words = text.split(' '); - var full = []; - var lines = []; - var p; - var lineHeight; + var polylineLength = this.length(); + var length = polylineLength * ratio; - for (var i = 0, l = 0, len = words.length; i < len; i++) { + return this.tangentAtLength(length); + }, - var word = words[i]; + tangentAtLength: function(length) { - textNode.data = lines[l] ? lines[l] + ' ' + word : word; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point - if (textSpan.getComputedTextLength() <= width) { + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // the current line fits - lines[l] = textNode.data; + var lastValidLine; // differentiable (with a tangent) + var l = 0; // length so far + var n = numPoints - 1; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - if (p) { - // We were partitioning. Put rest of the word onto next line - full[l++] = true; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); - // cancel partitioning - p = 0; + if (line.isDifferentiable()) { // has a tangent line (line length is not 0) + if (length <= (l + d)) { + return line.tangentAtLength((fromStart ? 1 : -1) * (length - l)); } - } else { + lastValidLine = line; + } - if (!lines[l] || p) { + l += d; + } - var partition = !!p; + // if length requested is higher than the length of the polyline, return last valid endpoint + if (lastValidLine) { + var ratio = (fromStart ? 1 : 0); + return lastValidLine.tangentAt(ratio); + } - p = word.length - 1; + // if no valid line, return null + return null; + }, - if (partition || !p) { + intersectionWithLine: function(l) { + var line = new Line(l); + var intersections = []; + var points = this.points; + for (var i = 0, n = points.length - 1; i < n; i++) { + var a = points[i]; + var b = points[i+1]; + var l2 = new Line(a, b); + var int = line.intersectionWithLine(l2); + if (int) intersections.push(int[0]); + } + return (intersections.length > 0) ? intersections : null; + }, - // word has only one character. - if (!p) { + translate: function(tx, ty) { - if (!lines[l]) { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty - // we won't fit this text within our rect - lines = []; + for (var i = 0; i < numPoints; i++) { + points[i].translate(tx, ty); + } - break; - } + return this; + }, - // partitioning didn't help on the non-empty line - // try again, but this time start with a new line + // Return svgString that can be used to recreate this line. + serialize: function() { - // cancel partitions created - words.splice(i, 2, word + words[i + 1]); + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return ''; // if points array is empty - // adjust word length - len--; + var output = ''; + for (var i = 0; i < numPoints; i++) { - full[l++] = true; - i--; + var point = points[i]; + output += point.x + ',' + point.y + ' '; + } - continue; - } + return output.trim(); + }, - // move last letter to the beginning of the next word - words[i] = word.substring(0, p); - words[i + 1] = word.substring(p) + words[i + 1]; + toString: function() { - } else { + return this.points + ''; + } + }; - // We initiate partitioning - // split the long word into two words - words.splice(i, 1, word.substring(0, p), word.substring(p)); + Object.defineProperty(Polyline.prototype, 'start', { + // Getter for the first point of the polyline. - // adjust words length - len++; + configurable: true, - if (l && !full[l - 1]) { - // if the previous line is not full, try to fit max part of - // the current word there - l--; - } - } + enumerable: true, - i--; + get: function() { - continue; - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - l++; - i--; - } + return this.points[0]; + }, + }); - // if size.height is defined we have to check whether the height of the entire - // text exceeds the rect height - if (height !== undefined) { + Object.defineProperty(Polyline.prototype, 'end', { + // Getter for the last point of the polyline. - if (lineHeight === undefined) { + configurable: true, - var heightValue; + enumerable: true, - // use the same defaults as in V.prototype.text - if (styles.lineHeight === 'auto') { - heightValue = { value: 1.5, unit: 'em' }; - } else { - heightValue = joint.util.parseCssNumeric(styles.lineHeight, ['em']) || { value: 1, unit: 'em' }; - } + get: function() { - lineHeight = heightValue.value; - if (heightValue.unit === 'em' ) { - lineHeight *= textElement.getBBox().height; - } - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - if (lineHeight * lines.length > height) { + return this.points[numPoints - 1]; + }, + }); - // remove overflowing lines - lines.splice(Math.floor(height / lineHeight)); + g.scale = { - break; - } - } - } + // Return the `value` from the `domain` interval scaled to the `range` interval. + linear: function(domain, range, value) { - if (opt.svgDocument) { + var domainSpan = domain[1] - domain[0]; + var rangeSpan = range[1] - range[0]; + return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; + } + }; - // svg document was provided, remove the text element only - svgDocument.removeChild(textElement); + var normalizeAngle = g.normalizeAngle = function(angle) { - } else { + return (angle % 360) + (angle < 0 ? 360 : 0); + }; - // clean svg document - document.body.removeChild(svgDocument); - } + var snapToGrid = g.snapToGrid = function(value, gridSize) { - return lines.join('\n'); - }, + return gridSize * round(value / gridSize); + }; - imageToDataUri: function(url, callback) { + var toDeg = g.toDeg = function(rad) { - if (!url || url.substr(0, 'data:'.length) === 'data:') { - // No need to convert to data uri if it is already in data uri. + return (180 * rad / PI) % 360; + }; - // This not only convenient but desired. For example, - // IE throws a security error if data:image/svg+xml is used to render - // an image to the canvas and an attempt is made to read out data uri. - // Now if our image is already in data uri, there is no need to render it to the canvas - // and so we can bypass this error. + var toRad = g.toRad = function(deg, over360) { - // Keep the async nature of the function. - return setTimeout(function() { - callback(null, url); - }, 0); - } + over360 = over360 || false; + deg = over360 ? deg : (deg % 360); + return deg * PI / 180; + }; - // chrome IE10 IE11 - var modernHandler = function(xhr, callback) { + // For backwards compatibility: + g.ellipse = g.Ellipse; + g.line = g.Line; + g.point = g.Point; + g.rect = g.Rect; - if (xhr.status === 200) { + // Local helper function. + // Use an array of arguments to call a constructor (function called with `new`). + // Adapted from https://stackoverflow.com/a/8843181/2263595 + // It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). + // - If that is the case, use `new constructor(arg1, arg2)`, for example. + // It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. + // - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. + function applyToNew(constructor, argsArray) { + // The `new` keyword can only be applied to functions that take a limited number of arguments. + // - We can fake that with .bind(). + // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. + // - So `new (constructor.bind(thisArg, arg1, arg2...))` + // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. + // We need to pass in a variable number of arguments to the bind() call. + // - We can use .apply(). + // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, arg2...]))` + // - `thisArg` can still be anything because `new` overwrites it. + // Finally, to make sure that constructor.bind overwriting is not a problem, we switch to `Function.prototype.bind`. + // - So, the final version is `new (Function.prototype.bind.apply(constructor, [thisArg, arg1, arg2...]))` + + // The function expects `argsArray[0]` to be `thisArg`. + // - This means that whatever is sent as the first element will be ignored. + // - The constructor will only see arguments starting from argsArray[1]. + // - So, a new dummy element is inserted at the start of the array. + argsArray.unshift(null); + + return new (Function.prototype.bind.apply(constructor, argsArray)); + } - var reader = new FileReader(); + // Local helper function. + // Add properties from arguments on top of properties from `obj`. + // This allows for rudimentary inheritance. + // - The `obj` argument acts as parent. + // - This function creates a new object that inherits all `obj` properties and adds/replaces those that are present in arguments. + // - A high-level example: calling `extend(Vehicle, Car)` would be akin to declaring `class Car extends Vehicle`. + function extend(obj) { + // In JavaScript, the combination of a constructor function (e.g. `g.Line = function(...) {...}`) and prototype (e.g. `g.Line.prototype = {...}) is akin to a C++ class. + // - When inheritance is not necessary, we can leave it at that. (This would be akin to calling extend with only `obj`.) + // - But, what if we wanted the `g.Line` quasiclass to inherit from another quasiclass (let's call it `g.GeometryObject`) in JavaScript? + // - First, realize that both of those quasiclasses would still have their own separate constructor function. + // - So what we are actually saying is that we want the `g.Line` prototype to inherit from `g.GeometryObject` prototype. + // - This method provides a way to do exactly that. + // - It copies parent prototype's properties, then adds extra ones from child prototype/overrides parent prototype properties with child prototype properties. + // - Therefore, to continue with the example above: + // - `g.Line.prototype = extend(g.GeometryObject.prototype, linePrototype)` + // - Where `linePrototype` is a properties object that looks just like `g.Line.prototype` does right now. + // - Then, `g.Line` would allow the programmer to access to all methods currently in `g.Line.Prototype`, plus any non-overriden methods from `g.GeometryObject.prototype`. + // - In that aspect, `g.GeometryObject` would then act like the parent of `g.Line`. + // - Multiple inheritance is also possible, if multiple arguments are provided. + // - What if we wanted to add another level of abstraction between `g.GeometryObject` and `g.Line` (let's call it `g.LinearObject`)? + // - `g.Line.prototype = extend(g.GeometryObject.prototype, g.LinearObject.prototype, linePrototype)` + // - The ancestors are applied in order of appearance. + // - That means that `g.Line` would have inherited from `g.LinearObject` that would have inherited from `g.GeometryObject`. + // - Any number of ancestors may be provided. + // - Note that neither `obj` nor any of the arguments need to actually be prototypes of any JavaScript quasiclass, that was just a simplified explanation. + // - We can create a new object composed from the properties of any number of other objects (since they do not have a constructor, we can think of those as interfaces). + // - `extend({ a: 1, b: 2 }, { b: 10, c: 20 }, { c: 100, d: 200 })` gives `{ a: 1, b: 10, c: 100, d: 200 }`. + // - Basically, with this function, we can emulate the `extends` keyword as well as the `implements` keyword. + // - Therefore, both of the following are valid: + // - `Lineto.prototype = extend(Line.prototype, segmentPrototype, linetoPrototype)` + // - `Moveto.prototype = extend(segmentPrototype, movetoPrototype)` - reader.onload = function(evt) { - var dataUri = evt.target.result; - callback(null, dataUri); - }; + var i; + var n; - reader.onerror = function() { - callback(new Error('Failed to load image ' + url)); - }; + var args = []; + n = arguments.length; + for (i = 1; i < n; i++) { // skip over obj + args.push(arguments[i]); + } - reader.readAsDataURL(xhr.response); - } else { - callback(new Error('Failed to load image ' + url)); + if (!obj) throw new Error('Missing a parent object.'); + var child = Object.create(obj); + + n = args.length; + for (i = 0; i < n; i++) { + + var src = args[i]; + + var inheritedProperty; + var key; + for (key in src) { + + if (src.hasOwnProperty(key)) { + delete child[key]; // delete property inherited from parent + inheritedProperty = Object.getOwnPropertyDescriptor(src, key); // get new definition of property from src + Object.defineProperty(child, key, inheritedProperty); // re-add property with new definition (includes getter/setter methods) } + } + } - }; + return child; + } - var legacyHandler = function(xhr, callback) { + // Path segment interface: + var segmentPrototype = { - var Uint8ToString = function(u8a) { - var CHUNK_SZ = 0x8000; - var c = []; - for (var i = 0; i < u8a.length; i += CHUNK_SZ) { - c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); - } - return c.join(''); - }; + // Redirect calls to closestPointNormalizedLength() function if closestPointT() is not defined for segment. + closestPointT: function(p) { + if (this.closestPointNormalizedLength) return this.closestPointNormalizedLength(p); - if (xhr.status === 200) { + throw new Error('Neither closestPointT() nor closestPointNormalizedLength() function is implemented.'); + }, - var bytes = new Uint8Array(xhr.response); + isSegment: true, - var suffix = (url.split('.').pop()) || 'png'; - var map = { - 'svg': 'svg+xml' - }; - var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,'; - var b64encoded = meta + btoa(Uint8ToString(bytes)); - callback(null, b64encoded); - } else { - callback(new Error('Failed to load image ' + url)); - } - }; + isSubpathStart: false, // true for Moveto segments - var xhr = new XMLHttpRequest(); + isVisible: true, // false for Moveto segments - xhr.open('GET', url, true); - xhr.addEventListener('error', function() { - callback(new Error('Failed to load image ' + url)); - }); + nextSegment: null, // needed for subpath start segment updating - xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer'; + // Return a fraction of result of length() function if lengthAtT() is not defined for segment. + lengthAtT: function(t) { - xhr.addEventListener('load', function() { - if (window.FileReader) { - modernHandler(xhr, callback); - } else { - legacyHandler(xhr, callback); - } - }); + if (t <= 0) return 0; - xhr.send(); + var length = this.length(); + + if (t >= 1) return length; + + return length * t; }, - getElementBBox: function(el) { + // Redirect calls to pointAt() function if pointAtT() is not defined for segment. + pointAtT: function(t) { - var $el = $(el); - if ($el.length === 0) { - throw new Error('Element not found') - } + if (this.pointAt) return this.pointAt(t); - var element = $el[0]; - var doc = element.ownerDocument; - var clientBBox = element.getBoundingClientRect(); + throw new Error('Neither pointAtT() nor pointAt() function is implemented.'); + }, - var strokeWidthX = 0; - var strokeWidthY = 0; + previousSegment: null, // needed to get segment start property - // Firefox correction - if (element.ownerSVGElement) { + subpathStartSegment: null, // needed to get closepath segment end property - var vel = V(element); - var bbox = vel.getBBox({ target: vel.svg() }); + // Redirect calls to tangentAt() function if tangentAtT() is not defined for segment. + tangentAtT: function(t) { - // if FF getBoundingClientRect includes stroke-width, getBBox doesn't. - // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value. - strokeWidthX = (clientBBox.width - bbox.width); - strokeWidthY = (clientBBox.height - bbox.height); - } + if (this.tangentAt) return this.tangentAt(t); - return { - x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2, - y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2, - width: clientBBox.width - strokeWidthX, - height: clientBBox.height - strokeWidthY - }; + throw new Error('Neither tangentAtT() nor tangentAt() function is implemented.'); }, + // VIRTUAL PROPERTIES (must be overriden by actual Segment implementations): - // Highly inspired by the jquery.sortElements plugin by Padolsey. - // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. - sortElements: function(elements, comparator) { + // type - var $elements = $(elements); - var placements = $elements.map(function() { + // start // getter, always throws error for Moveto - var sortElement = this; - var parentNode = sortElement.parentNode; - // Since the element itself will change position, we have - // to have some way of storing it's original position in - // the DOM. The easiest way is to have a 'flag' node: - var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); + // end // usually directly assigned, getter for Closepath - return function() { + bbox: function() { - if (parentNode === this) { - throw new Error('You can\'t sort elements if any one is a descendant of another.'); - } + throw new Error('Declaration missing for virtual function.'); + }, - // Insert before flag: - parentNode.insertBefore(this, nextSibling); - // Remove flag: - parentNode.removeChild(nextSibling); - }; - }); + clone: function() { - return Array.prototype.sort.call($elements, comparator).each(function(i) { - placements[i].call(this); - }); + throw new Error('Declaration missing for virtual function.'); }, - // Sets attributes on the given element and its descendants based on the selector. - // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} - setAttributesBySelector: function(element, attrs) { + closestPoint: function() { - var $element = $(element); + throw new Error('Declaration missing for virtual function.'); + }, - joint.util.forIn(attrs, function(attrs, selector) { - var $elements = $element.find(selector).addBack().filter(selector); - // Make a special case for setting classes. - // We do not want to overwrite any existing class. - if (joint.util.has(attrs, 'class')) { - $elements.addClass(attrs['class']); - attrs = joint.util.omit(attrs, 'class'); - } - $elements.attr(attrs); - }); + closestPointLength: function() { + + throw new Error('Declaration missing for virtual function.'); }, - // Return a new object with all for sides (top, bottom, left and right) in it. - // Value of each side is taken from the given argument (either number or object). - // Default value for a side is 0. - // Examples: - // joint.util.normalizeSides(5) --> { top: 5, left: 5, right: 5, bottom: 5 } - // joint.util.normalizeSides({ left: 5 }) --> { top: 0, left: 5, right: 0, bottom: 0 } - normalizeSides: function(box) { + closestPointNormalizedLength: function() { - if (Object(box) !== box) { - box = box || 0; - return { top: box, bottom: box, left: box, right: box }; - } + throw new Error('Declaration missing for virtual function.'); + }, - return { - top: box.top || 0, - bottom: box.bottom || 0, - left: box.left || 0, - right: box.right || 0 - }; + closestPointTangent: function() { + + throw new Error('Declaration missing for virtual function.'); }, - timing: { + equals: function() { - linear: function(t) { - return t; - }, + throw new Error('Declaration missing for virtual function.'); + }, - quad: function(t) { - return t * t; - }, + getSubdivisions: function() { - cubic: function(t) { - return t * t * t; - }, + throw new Error('Declaration missing for virtual function.'); + }, - inout: function(t) { - if (t <= 0) return 0; - if (t >= 1) return 1; - var t2 = t * t; - var t3 = t2 * t; - return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); - }, + isDifferentiable: function() { - exponential: function(t) { - return Math.pow(2, 10 * (t - 1)); - }, + throw new Error('Declaration missing for virtual function.'); + }, - bounce: function(t) { - for (var a = 0, b = 1; 1; a += b, b /= 2) { - if (t >= (7 - 4 * a) / 11) { - var q = (11 - 6 * a - 11 * t) / 4; - return -q * q + b * b; - } - } - }, + length: function() { - reverse: function(f) { - return function(t) { - return 1 - f(1 - t); - }; - }, + throw new Error('Declaration missing for virtual function.'); + }, - reflect: function(f) { - return function(t) { - return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); - }; - }, + pointAt: function() { - clamp: function(f, n, x) { - n = n || 0; - x = x || 1; - return function(t) { - var r = f(t); - return r < n ? n : r > x ? x : r; - }; - }, + throw new Error('Declaration missing for virtual function.'); + }, - back: function(s) { - if (!s) s = 1.70158; - return function(t) { - return t * t * ((s + 1) * t - s); - }; - }, + pointAtLength: function() { - elastic: function(x) { - if (!x) x = 1.5; - return function(t) { - return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); - }; - } + throw new Error('Declaration missing for virtual function.'); }, - interpolate: { + scale: function() { - number: function(a, b) { - var d = b - a; - return function(t) { return a + d * t; }; - }, + throw new Error('Declaration missing for virtual function.'); + }, - object: function(a, b) { - var s = Object.keys(a); - return function(t) { - var i, p; - var r = {}; - for (i = s.length - 1; i != -1; i--) { - p = s[i]; - r[p] = a[p] + (b[p] - a[p]) * t; - } - return r; - }; - }, + tangentAt: function() { - hexColor: function(a, b) { + throw new Error('Declaration missing for virtual function.'); + }, - var ca = parseInt(a.slice(1), 16); - var cb = parseInt(b.slice(1), 16); - var ra = ca & 0x0000ff; - var rd = (cb & 0x0000ff) - ra; - var ga = ca & 0x00ff00; - var gd = (cb & 0x00ff00) - ga; - var ba = ca & 0xff0000; - var bd = (cb & 0xff0000) - ba; + tangentAtLength: function() { - return function(t) { + throw new Error('Declaration missing for virtual function.'); + }, - var r = (ra + rd * t) & 0x000000ff; - var g = (ga + gd * t) & 0x0000ff00; - var b = (ba + bd * t) & 0x00ff0000; + translate: function() { - return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); - }; - }, + throw new Error('Declaration missing for virtual function.'); + }, - unit: function(a, b) { + serialize: function() { - var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; - var ma = r.exec(a); - var mb = r.exec(b); - var p = mb[1].indexOf('.'); - var f = p > 0 ? mb[1].length - p - 1 : 0; - a = +ma[1]; - var d = +mb[1] - a; - var u = ma[2]; - - return function(t) { - return (a + d * t).toFixed(f) + u; - }; - } + throw new Error('Declaration missing for virtual function.'); }, - // SVG filters. - filter: { + toString: function() { - // `color` ... outline color - // `width`... outline width - // `opacity` ... outline opacity - // `margin` ... gap between outline and the element - outline: function(args) { + throw new Error('Declaration missing for virtual function.'); + } + }; - var tpl = ''; + // Path segment implementations: + var Lineto = function() { - var margin = Number.isFinite(args.margin) ? args.margin : 2; - var width = Number.isFinite(args.width) ? args.width : 1; + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - return joint.util.template(tpl)({ - color: args.color || 'blue', - opacity: Number.isFinite(args.opacity) ? args.opacity : 1, - outerRadius: margin + width, - innerRadius: margin - }); - }, + if (!(this instanceof Lineto)) { // switching context of `this` to Lineto when called without `new` + return applyToNew(Lineto, args); + } - // `color` ... color - // `width`... width - // `blur` ... blur - // `opacity` ... opacity - highlight: function(args) { + if (n === 0) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (none provided).'); + } - var tpl = ''; + var outputArray; - return joint.util.template(tpl)({ - color: args.color || 'red', - width: Number.isFinite(args.width) ? args.width : 1, - blur: Number.isFinite(args.blur) ? args.blur : 0, - opacity: Number.isFinite(args.opacity) ? args.opacity : 1 - }); - }, + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; - // `x` ... horizontal blur - // `y` ... vertical blur (optional) - blur: function(args) { + } else if (n < 2) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); - var x = Number.isFinite(args.x) ? args.x : 2; + } else { // this is a poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two - return joint.util.template('')({ - stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x - }); - }, + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; + } - // `dx` ... horizontal shift - // `dy` ... vertical shift - // `blur` ... blur - // `color` ... color - // `opacity` ... opacity - dropShadow: function(args) { + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; - var tpl = 'SVGFEDropShadowElement' in window - ? '' - : ''; + } else { // this is a poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { - return joint.util.template(tpl)({ - dx: args.dx || 0, - dy: args.dy || 0, - opacity: Number.isFinite(args.opacity) ? args.opacity : 1, - color: args.color || 'black', - blur: Number.isFinite(args.blur) ? args.blur : 4 - }); - }, + segmentPoint = args[i]; + outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } + }; - // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. - grayscale: function(args) { + var linetoPrototype = { - var amount = Number.isFinite(args.amount) ? args.amount : 1; + clone: function() { - return joint.util.template('')({ - a: 0.2126 + 0.7874 * (1 - amount), - b: 0.7152 - 0.7152 * (1 - amount), - c: 0.0722 - 0.0722 * (1 - amount), - d: 0.2126 - 0.2126 * (1 - amount), - e: 0.7152 + 0.2848 * (1 - amount), - f: 0.0722 - 0.0722 * (1 - amount), - g: 0.2126 - 0.2126 * (1 - amount), - h: 0.0722 + 0.9278 * (1 - amount) - }); - }, + return new Lineto(this.end); + }, - // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. - sepia: function(args) { + getSubdivisions: function() { - var amount = Number.isFinite(args.amount) ? args.amount : 1; + return []; + }, - return joint.util.template('')({ - a: 0.393 + 0.607 * (1 - amount), - b: 0.769 - 0.769 * (1 - amount), - c: 0.189 - 0.189 * (1 - amount), - d: 0.349 - 0.349 * (1 - amount), - e: 0.686 + 0.314 * (1 - amount), - f: 0.168 - 0.168 * (1 - amount), - g: 0.272 - 0.272 * (1 - amount), - h: 0.534 - 0.534 * (1 - amount), - i: 0.131 + 0.869 * (1 - amount) - }); - }, + isDifferentiable: function() { - // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. - saturate: function(args) { + if (!this.previousSegment) return false; - var amount = Number.isFinite(args.amount) ? args.amount : 1; + return !this.start.equals(this.end); + }, - return joint.util.template('')({ - amount: 1 - amount - }); - }, + scale: function(sx, sy, origin) { - // `angle` ... the number of degrees around the color circle the input samples will be adjusted. - hueRotate: function(args) { + this.end.scale(sx, sy, origin); + return this; + }, - return joint.util.template('')({ - angle: args.angle || 0 - }); - }, + translate: function(tx, ty) { - // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. - invert: function(args) { + this.end.translate(tx, ty); + return this; + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + type: 'L', - return joint.util.template('')({ - amount: amount, - amount2: 1 - amount - }); - }, + serialize: function() { - // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. - brightness: function(args) { + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, - return joint.util.template('')({ - amount: Number.isFinite(args.amount) ? args.amount : 1 - }); - }, + toString: function() { - // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. - contrast: function(args) { + return this.type + ' ' + this.start + ' ' + this.end; + } + }; - var amount = Number.isFinite(args.amount) ? args.amount : 1; + Object.defineProperty(linetoPrototype, 'start', { + // get a reference to the end point of previous segment - return joint.util.template('')({ - amount: amount, - amount2: .5 - amount / 2 - }); - } - }, + configurable: true, - format: { + enumerable: true, - // Formatting numbers via the Python Format Specification Mini-language. - // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. - // Heavilly inspired by the D3.js library implementation. - number: function(specifier, value, locale) { + get: function() { - locale = locale || { + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - currency: ['$', ''], - decimal: '.', - thousands: ',', - grouping: [3] - }; + return this.previousSegment.end; + } + }); - // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. - // [[fill]align][sign][symbol][0][width][,][.precision][type] - var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + Lineto.prototype = extend(segmentPrototype, Line.prototype, linetoPrototype); - var match = re.exec(specifier); - var fill = match[1] || ' '; - var align = match[2] || '>'; - var sign = match[3] || ''; - var symbol = match[4] || ''; - var zfill = match[5]; - var width = +match[6]; - var comma = match[7]; - var precision = match[8]; - var type = match[9]; - var scale = 1; - var prefix = ''; - var suffix = ''; - var integer = false; + var Curveto = function() { - if (precision) precision = +precision.substring(1); + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - if (zfill || fill === '0' && align === '=') { - zfill = fill = '0'; - align = '='; - if (comma) width -= Math.floor((width - 1) / 4); - } + if (!(this instanceof Curveto)) { // switching context of `this` to Curveto when called without `new` + return applyToNew(Curveto, args); + } - switch (type) { - case 'n': - comma = true; type = 'g'; - break; - case '%': - scale = 100; suffix = '%'; type = 'f'; - break; - case 'p': - scale = 100; suffix = '%'; type = 'r'; - break; - case 'b': - case 'o': - case 'x': - case 'X': - if (symbol === '#') prefix = '0' + type.toLowerCase(); - break; - case 'c': - case 'd': - integer = true; precision = 0; - break; - case 's': - scale = -1; type = 'r'; - break; - } + if (n === 0) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (none provided).'); + } - if (symbol === '$') { - prefix = locale.currency[0]; - suffix = locale.currency[1]; + var outputArray; + + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 6) { + this.controlPoint1 = new Point(+args[0], +args[1]); + this.controlPoint2 = new Point(+args[2], +args[3]); + this.end = new Point(+args[4], +args[5]); + return this; + + } else if (n < 6) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' coordinates provided).'); + + } else { // this is a poly-bezier segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 6) { // coords come in groups of six + + segmentCoords = args.slice(i, i + 6); // will send fewer than six coords if args.length not divisible by 6 + outputArray.push(applyToNew(Curveto, segmentCoords)); } + return outputArray; + } - // If no precision is specified for `'r'`, fallback to general notation. - if (type == 'r' && !precision) type = 'g'; + } else { // points provided + if (n === 3) { + this.controlPoint1 = new Point(args[0]); + this.controlPoint2 = new Point(args[1]); + this.end = new Point(args[2]); + return this; - // Ensure that the requested precision is in the supported range. - if (precision != null) { - if (type == 'g') precision = Math.max(1, Math.min(21, precision)); - else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); + } else if (n < 3) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' points provided).'); + + } else { // this is a poly-bezier segment + var segmentPoints; + outputArray = []; + for (i = 0; i < n; i += 3) { // points come in groups of three + + segmentPoints = args.slice(i, i + 3); // will send fewer than three points if args.length is not divisible by 3 + outputArray.push(applyToNew(Curveto, segmentPoints)); } + return outputArray; + } + } + }; - var zcomma = zfill && comma; + var curvetoPrototype = { - // Return the empty string for floats formatted as ints. - if (integer && (value % 1)) return ''; + clone: function() { - // Convert negative to positive, and record the sign prefix. - var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; + return new Curveto(this.controlPoint1, this.controlPoint2, this.end); + }, - var fullSuffix = suffix; + isDifferentiable: function() { - // Apply the scale, computing it from the value's exponent for si format. - // Preserve the existing suffix, if any, such as the currency symbol. - if (scale < 0) { - var unit = this.prefix(value, precision); - value = unit.scale(value); - fullSuffix = unit.symbol + suffix; - } else { - value *= scale; - } - - // Convert to the desired precision. - value = this.convert(type, value, precision); + if (!this.previousSegment) return false; - // Break the value into the integer part (before) and decimal part (after). - var i = value.lastIndexOf('.'); - var before = i < 0 ? value : value.substring(0, i); - var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; - function formatGroup(value) { + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); + }, - var i = value.length; - var t = []; - var j = 0; - var g = locale.grouping[0]; - while (i > 0 && g > 0) { - t.push(value.substring(i -= g, i + g)); - g = locale.grouping[j = (j + 1) % locale.grouping.length]; - } - return t.reverse().join(locale.thousands); - } + scale: function(sx, sy, origin) { - // If the fill character is not `'0'`, grouping is applied before padding. - if (!zfill && comma && locale.grouping) { + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - before = formatGroup(before); - } + translate: function(tx, ty) { - var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); - var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, - // If the fill character is `'0'`, grouping is applied after padding. - if (zcomma) before = formatGroup(padding + before); + type: 'C', - // Apply prefix. - negative += prefix; + serialize: function() { - // Rejoin integer and decimal parts. - value = before + after; + var c1 = this.controlPoint1; + var c2 = this.controlPoint2; + var end = this.end; + return this.type + ' ' + c1.x + ' ' + c1.y + ' ' + c2.x + ' ' + c2.y + ' ' + end.x + ' ' + end.y; + }, - return (align === '<' ? negative + value + padding - : align === '>' ? padding + negative + value - : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) - : negative + (zcomma ? value : padding + value)) + fullSuffix; - }, + toString: function() { - // Formatting string via the Python Format string. - // See https://docs.python.org/2/library/string.html#format-string-syntax) - string: function(formatString, value) { + return this.type + ' ' + this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; - var fieldDelimiterIndex; - var fieldDelimiter = '{'; - var endPlaceholder = false; - var formattedStringArray = []; + Object.defineProperty(curvetoPrototype, 'start', { + // get a reference to the end point of previous segment - while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { + configurable: true, - var pieceFormatedString, formatSpec, fieldName; + enumerable: true, - pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); + get: function() { - if (endPlaceholder) { - formatSpec = pieceFormatedString.split(':'); - fieldName = formatSpec.shift().split('.'); - pieceFormatedString = value; + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - for (var i = 0; i < fieldName.length; i++) - pieceFormatedString = pieceFormatedString[fieldName[i]]; + return this.previousSegment.end; + } + }); - if (formatSpec.length) - pieceFormatedString = this.number(formatSpec, pieceFormatedString); - } + Curveto.prototype = extend(segmentPrototype, Curve.prototype, curvetoPrototype); - formattedStringArray.push(pieceFormatedString); + var Moveto = function() { - formatString = formatString.slice(fieldDelimiterIndex + 1); - fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; - } - formattedStringArray.push(formatString); + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - return formattedStringArray.join(''); - }, + if (!(this instanceof Moveto)) { // switching context of `this` to Moveto when called without `new` + return applyToNew(Moveto, args); + } - convert: function(type, value, precision) { + if (n === 0) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (none provided).'); + } - switch (type) { - case 'b': return value.toString(2); - case 'c': return String.fromCharCode(value); - case 'o': return value.toString(8); - case 'x': return value.toString(16); - case 'X': return value.toString(16).toUpperCase(); - case 'g': return value.toPrecision(precision); - case 'e': return value.toExponential(precision); - case 'f': return value.toFixed(precision); - case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); - default: return value + ''; - } - }, + var outputArray; - round: function(value, precision) { + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; - return precision - ? Math.round(value * (precision = Math.pow(10, precision))) / precision - : Math.round(value); - }, + } else if (n < 2) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); - precision: function(value, precision) { + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two - return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); - }, + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + if (i === 0) outputArray.push(applyToNew(Moveto, segmentCoords)); + else outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; + } - prefix: function(value, precision) { + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; - var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) { - var k = Math.pow(10, Math.abs(8 - i) * 3); - return { - scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, - symbol: d - }; - }); + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { // points come one by one - var i = 0; - if (value) { - if (value < 0) value *= -1; - if (precision) value = this.round(value, this.precision(value, precision)); - i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); - i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); + segmentPoint = args[i]; + if (i === 0) outputArray.push(new Moveto(segmentPoint)); + else outputArray.push(new Lineto(segmentPoint)); } - return prefixes[8 + i / 3]; + return outputArray; } - }, + } + }; - /* - Pre-compile the HTML to be used as a template. - */ - template: function(html) { + var movetoPrototype = { - /* - Must support the variation in templating syntax found here: - https://lodash.com/docs#template - */ - var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; + bbox: function() { - return function(data) { + return null; + }, - data = data || {}; + clone: function() { - return html.replace(regex, function(match) { + return new Moveto(this.end); + }, - var args = Array.from(arguments); - var attr = args.slice(1, 4).find(function(_attr) { - return !!_attr; - }); + closestPoint: function() { - var attrArray = attr.split('.'); - var value = data[attrArray.shift()]; + return this.end.clone(); + }, - while (value !== undefined && attrArray.length) { - value = value[attrArray.shift()]; - } + closestPointNormalizedLength: function() { - return value !== undefined ? value : ''; - }); - }; + return 0; }, - /** - * @param {Element=} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default. - */ - toggleFullScreen: function(el) { + closestPointLength: function() { - var topDocument = window.top.document; - el = el || topDocument.body; + return 0; + }, - function prefixedResult(el, prop) { + closestPointT: function() { - var prefixes = ['webkit', 'moz', 'ms', 'o', '']; - for (var i = 0; i < prefixes.length; i++) { - var prefix = prefixes[i]; - var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); - if (el[propName] !== undefined) { - return joint.util.isFunction(el[propName]) ? el[propName]() : el[propName]; - } - } - } + return 1; + }, - if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) { - prefixedResult(topDocument, 'ExitFullscreen') || // Spec. - prefixedResult(topDocument, 'CancelFullScreen'); // Firefox - } else { - prefixedResult(el, 'RequestFullscreen') || // Spec. - prefixedResult(el, 'RequestFullScreen'); // Firefox - } + closestPointTangent: function() { + + return null; }, - addClassNamePrefix: function(className) { + equals: function(m) { - if (!className) return className; + return this.end.equals(m.end); + }, - return className.toString().split(' ').map(function(_className) { + getSubdivisions: function() { - if (_className.substr(0, joint.config.classNamePrefix.length) !== joint.config.classNamePrefix) { - _className = joint.config.classNamePrefix + _className; - } + return []; + }, - return _className; + isDifferentiable: function() { - }).join(' '); + return false; }, - removeClassNamePrefix: function(className) { + isSubpathStart: true, - if (!className) return className; + isVisible: false, - return className.toString().split(' ').map(function(_className) { + length: function() { - if (_className.substr(0, joint.config.classNamePrefix.length) === joint.config.classNamePrefix) { - _className = _className.substr(joint.config.classNamePrefix.length); - } + return 0; + }, - return _className; + lengthAtT: function() { - }).join(' '); + return 0; }, - wrapWith: function(object, methods, wrapper) { + pointAt: function() { - if (joint.util.isString(wrapper)) { + return this.end.clone(); + }, - if (!joint.util.wrappers[wrapper]) { - throw new Error('Unknown wrapper: "' + wrapper + '"'); - } + pointAtLength: function() { - wrapper = joint.util.wrappers[wrapper]; - } + return this.end.clone(); + }, - if (!joint.util.isFunction(wrapper)) { - throw new Error('Wrapper must be a function.'); - } + pointAtT: function() { - this.toArray(methods).forEach(function(method) { - object[method] = wrapper(object[method]); - }); + return this.end.clone(); }, - wrappers: { + scale: function(sx, sy, origin) { - /* - Prepares a function with the following usage: + this.end.scale(sx, sy, origin); + return this; + }, - fn([cell, cell, cell], opt); - fn([cell, cell, cell]); - fn(cell, cell, cell, opt); - fn(cell, cell, cell); - fn(cell); - */ - cells: function(fn) { + tangentAt: function() { - return function() { + return null; + }, - var args = Array.from(arguments); - var n = args.length; - var cells = n > 0 && args[0] || []; - var opt = n > 1 && args[n - 1] || {}; + tangentAtLength: function() { - if (!Array.isArray(cells)) { + return null; + }, - if (opt instanceof joint.dia.Cell) { - cells = args; - } else if (cells instanceof joint.dia.Cell) { - if (args.length > 1) { - args.pop(); - } - cells = args; - } - } + tangentAtT: function() { - if (opt instanceof joint.dia.Cell) { - opt = {}; - } - - return fn.call(this, cells, opt); - }; - } - }, - // lodash 3 vs 4 incompatible - sortedIndex: _.sortedIndexBy || _.sortedIndex, - uniq: _.uniqBy || _.uniq, - uniqueId: _.uniqueId, - sortBy: _.sortBy, - isFunction: _.isFunction, - result: _.result, - union: _.union, - invoke: _.invokeMap || _.invoke, - difference: _.difference, - intersection: _.intersection, - omit: _.omit, - pick: _.pick, - has: _.has, - bindAll: _.bindAll, - assign: _.assign, - defaults: _.defaults, - defaultsDeep: _.defaultsDeep, - isPlainObject: _.isPlainObject, - isEmpty: _.isEmpty, - isEqual: _.isEqual, - noop: function() {}, - cloneDeep: _.cloneDeep, - toArray: _.toArray, - flattenDeep: _.flattenDeep, - camelCase: _.camelCase, - groupBy: _.groupBy, - forIn: _.forIn, - without: _.without, - debounce: _.debounce, - clone: _.clone, - - isBoolean: function(value) { - var toString = Object.prototype.toString; - return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]'); + return null; }, - isObject: function(value) { - return !!value && (typeof value === 'object' || typeof value === 'function'); - }, + translate: function(tx, ty) { - isNumber: function(value) { - var toString = Object.prototype.toString; - return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]'); + this.end.translate(tx, ty); + return this; }, - isString: function(value) { - var toString = Object.prototype.toString; - return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]'); - }, + type: 'M', - merge: function() { - if (_.mergeWith) { - var args = Array.from(arguments); - var last = args[args.length - 1]; + serialize: function() { - var customizer = this.isFunction(last) ? last : this.noop; - args.push(function(a,b) { - var customResult = customizer(a, b); - if (customResult !== undefined) { - return customResult; - } + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, - if (Array.isArray(a) && !Array.isArray(b)) { - return b; - } - }); + toString: function() { - return _.mergeWith.apply(this, args) - } - return _.merge.apply(this, arguments); + return this.type + ' ' + this.end; } - } -}; - - -joint.mvc.View = Backbone.View.extend({ - - options: {}, - theme: null, - themeClassNamePrefix: joint.util.addClassNamePrefix('theme-'), - requireSetThemeOverride: false, - defaultTheme: joint.config.defaultTheme, + }; - constructor: function(options) { + Object.defineProperty(movetoPrototype, 'start', { - this.requireSetThemeOverride = options && !!options.theme; - this.options = joint.util.assign({}, this.options, options); + configurable: true, - Backbone.View.call(this, options); - }, + enumerable: true, - initialize: function(options) { + get: function() { - joint.util.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); + throw new Error('Illegal access. Moveto segments should not need a start property.'); + } + }) - joint.mvc.views[this.cid] = this; + Moveto.prototype = extend(segmentPrototype, movetoPrototype); // does not inherit from any other geometry object - this.setTheme(this.options.theme || this.defaultTheme); - this.init(); - }, + var Closepath = function() { - // Override the Backbone `_ensureElement()` method in order to create an - // svg element (e.g., ``) node that wraps all the nodes of the Cell view. - // Expose class name setter as a separate method. - _ensureElement: function() { - if (!this.el) { - var tagName = joint.util.result(this, 'tagName'); - var attrs = joint.util.assign({}, joint.util.result(this, 'attributes')); - if (this.id) attrs.id = joint.util.result(this, 'id'); - this.setElement(this._createElement(tagName)); - this._setAttributes(attrs); - } else { - this.setElement(joint.util.result(this, 'el')); + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); } - this._ensureElClassName(); - }, - _setAttributes: function(attrs) { - if (this.svgElement) { - this.vel.attr(attrs); - } else { - this.$el.attr(attrs); + if (!(this instanceof Closepath)) { // switching context of `this` to Closepath when called without `new` + return applyToNew(Closepath, args); } - }, - _createElement: function(tagName) { - if (this.svgElement) { - return document.createElementNS(V.namespace.xmlns, tagName); - } else { - return document.createElement(tagName); + if (n > 0) { + throw new Error('Closepath constructor expects no arguments.'); } - }, - // Utilize an alternative DOM manipulation API by - // adding an element reference wrapped in Vectorizer. - _setElement: function(el) { - this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); - this.el = this.$el[0]; - if (this.svgElement) this.vel = V(this.el); - }, + return this; + }; - _ensureElClassName: function() { - var className = joint.util.result(this, 'className'); - var prefixedClassName = joint.util.addClassNamePrefix(className); - // Note: className removal here kept for backwards compatibility only - if (this.svgElement) { - this.vel.removeClass(className).addClass(prefixedClassName); - } else { - this.$el.removeClass(className).addClass(prefixedClassName); - } - }, + var closepathPrototype = { - init: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + clone: function() { - onRender: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + return new Closepath(); + }, - setTheme: function(theme, opt) { + getSubdivisions: function() { - opt = opt || {}; + return []; + }, - // Theme is already set, override is required, and override has not been set. - // Don't set the theme. - if (this.theme && this.requireSetThemeOverride && !opt.override) { - return this; - } + isDifferentiable: function() { - this.removeThemeClassName(); - this.addThemeClassName(theme); - this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); - this.theme = theme; + if (!this.previousSegment || !this.subpathStartSegment) return false; - return this; - }, + return !this.start.equals(this.end); + }, - addThemeClassName: function(theme) { + scale: function() { - theme = theme || this.theme; + return this; + }, - var className = this.themeClassNamePrefix + theme; + translate: function() { - this.$el.addClass(className); + return this; + }, - return this; - }, + type: 'Z', - removeThemeClassName: function(theme) { + serialize: function() { - theme = theme || this.theme; + return this.type; + }, - var className = this.themeClassNamePrefix + theme; + toString: function() { - this.$el.removeClass(className); + return this.type + ' ' + this.start + ' ' + this.end; + } + }; - return this; - }, + Object.defineProperty(closepathPrototype, 'start', { + // get a reference to the end point of previous segment - onSetTheme: function(oldTheme, newTheme) { - // Intentionally empty. - // This method is meant to be overriden. - }, + configurable: true, - remove: function() { + enumerable: true, - this.onRemove(); + get: function() { - joint.mvc.views[this.cid] = null; + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - Backbone.View.prototype.remove.apply(this, arguments); + return this.previousSegment.end; + } + }); - return this; - }, + Object.defineProperty(closepathPrototype, 'end', { + // get a reference to the end point of subpath start segment - onRemove: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + configurable: true, - getEventNamespace: function() { - // Returns a per-session unique namespace - return '.joint-event-ns-' + this.cid; - } + enumerable: true, -}, { + get: function() { - extend: function() { + if (!this.subpathStartSegment) throw new Error('Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)'); - var args = Array.from(arguments); + return this.subpathStartSegment.end; + } + }) - // Deep clone the prototype and static properties objects. - // This prevents unexpected behavior where some properties are overwritten outside of this function. - var protoProps = args[0] && joint.util.assign({}, args[0]) || {}; - var staticProps = args[1] && joint.util.assign({}, args[1]) || {}; + Closepath.prototype = extend(segmentPrototype, Line.prototype, closepathPrototype); - // Need the real render method so that we can wrap it and call it later. - var renderFn = protoProps.render || (this.prototype && this.prototype.render) || null; + var segmentTypes = Path.segmentTypes = { + L: Lineto, + C: Curveto, + M: Moveto, + Z: Closepath, + z: Closepath + }; - /* - Wrap the real render method so that: - .. `onRender` is always called. - .. `this` is always returned. - */ - protoProps.render = function() { + Path.regexSupportedData = new RegExp('^[\\s\\d' + Object.keys(segmentTypes).join('') + ',.]*$'); - if (renderFn) { - // Call the original render method. - renderFn.apply(this, arguments); - } + Path.isDataSupported = function(d) { + if (typeof d !== 'string') return false; + return this.regexSupportedData.test(d); + } - // Should always call onRender() method. - this.onRender(); + return g; - // Should always return itself. - return this; - }; +})(); - return Backbone.View.extend.call(this, protoProps, staticProps); - } -}); +// 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; -joint.dia.GraphCells = Backbone.Collection.extend({ +V = Vectorizer = (function() { - cellNamespace: joint.shapes, + 'use strict'; - initialize: function(models, opt) { + var hasSvg = typeof window === 'object' && + !!( + window.SVGAngle || + document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') + ); - // Set the optional namespace where all model classes are defined. - if (opt.cellNamespace) { - this.cellNamespace = opt.cellNamespace; - } - - this.graph = opt.graph; - }, - - model: function(attrs, options) { + // SVG support is required. + if (!hasSvg) { - var collection = options.collection; - var namespace = collection.cellNamespace; + // Return a function that throws an error when it is used. + return function() { + throw new Error('SVG is required to use Vectorizer.'); + }; + } - // Find the model class in the namespace or use the default one. - var ModelClass = (attrs.type === 'link') - ? joint.dia.Link - : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; + // 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', + xhtml: 'http://www.w3.org/1999/xhtml' + }; - var cell = new ModelClass(attrs, options); - // Add a reference to the graph. It is necessary to do this here because this is the earliest place - // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. - cell.graph = collection.graph; + var SVGversion = '1.1'; - return cell; - }, + // Declare shorthands to the most used math functions. + var math = Math; + var PI = math.PI; + var atan2 = math.atan2; + var sqrt = math.sqrt; + var min = math.min; + var max = math.max; + var cos = math.cos; + var sin = math.sin; - // `comparator` makes it easy to sort cells based on their `z` index. - comparator: function(model) { + var V = function(el, attrs, children) { - return model.get('z') || 0; - } -}); + // This allows using V() without the new keyword. + if (!(this instanceof V)) { + return V.apply(Object.create(V.prototype), arguments); + } + if (!el) return; -joint.dia.Graph = Backbone.Model.extend({ + if (V.isV(el)) { + el = el.node; + } - _batches: {}, + attrs = attrs || {}; - initialize: function(attrs, opt) { + if (V.isString(el)) { - opt = opt || {}; + if (el.toLowerCase() === 'svg') { - // Passing `cellModel` function in the options object to graph allows for - // setting models based on attribute objects. This is especially handy - // when processing JSON graphs that are in a different than JointJS format. - var cells = new joint.dia.GraphCells([], { - model: opt.cellModel, - cellNamespace: opt.cellNamespace, - graph: this - }); - Backbone.Model.prototype.set.call(this, 'cells', cells); + // Create a new SVG canvas. + el = V.createSvgDocument(); - // Make all the events fired in the `cells` collection available. - // to the outside world. - cells.on('all', this.trigger, this); + } else if (el[0] === '<') { - // Backbone automatically doesn't trigger re-sort if models attributes are changed later when - // they're already in the collection. Therefore, we're triggering sort manually here. - this.on('change:z', this._sortOnChangeZ, this); - this.on('batch:stop', this._onBatchStop, this); + // Create element from an SVG string. + // Allows constructs of type: `document.appendChild(V('').node)`. - // `joint.dia.Graph` keeps an internal data structure (an adjacency list) - // for fast graph queries. All changes that affect the structure of the graph - // must be reflected in the `al` object. This object provides fast answers to - // questions such as "what are the neighbours of this node" or "what - // are the sibling links of this link". + var svgDoc = V.createSvgDocument(el); - // Outgoing edges per node. Note that we use a hash-table for the list - // of outgoing edges for a faster lookup. - // [node ID] -> Object [edge] -> true - this._out = {}; - // Ingoing edges per node. - // [node ID] -> Object [edge] -> true - this._in = {}; - // `_nodes` is useful for quick lookup of all the elements in the graph, without - // having to go through the whole cells array. - // [node ID] -> true - this._nodes = {}; - // `_edges` is useful for quick lookup of all the links in the graph, without - // having to go through the whole cells array. - // [edge ID] -> true - this._edges = {}; + // 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) { - cells.on('add', this._restructureOnAdd, this); - cells.on('remove', this._restructureOnRemove, this); - cells.on('reset', this._restructureOnReset, this); - cells.on('change:source', this._restructureOnChangeSource, this); - cells.on('change:target', this._restructureOnChangeTarget, this); - cells.on('remove', this._removeCell, this); - }, + // Map child nodes to `V`s. + var arrayOfVels = []; + var i, len; - _sortOnChangeZ: function() { + for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { - if (!this.hasActiveBatch('to-front') && !this.hasActiveBatch('to-back')) { - this.get('cells').sort(); - } - }, + var childNode = svgDoc.childNodes[i]; + arrayOfVels.push(new V(document.importNode(childNode, true))); + } - _onBatchStop: function(data) { + return arrayOfVels; + } - var batchName = data && data.batchName; - if ((batchName === 'to-front' || batchName === 'to-back') && !this.hasActiveBatch(batchName)) { - this.get('cells').sort(); - } - }, + el = document.importNode(svgDoc.firstChild, true); - _restructureOnAdd: function(cell) { + } else { - if (cell.isLink()) { - this._edges[cell.id] = true; - var source = cell.get('source'); - var target = cell.get('target'); - if (source.id) { - (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; - } - if (target.id) { - (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; + el = document.createElementNS(ns.xmlns, el); } - } else { - this._nodes[cell.id] = true; - } - }, - - _restructureOnRemove: function(cell) { - if (cell.isLink()) { - delete this._edges[cell.id]; - var source = cell.get('source'); - var target = cell.get('target'); - if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { - delete this._out[source.id][cell.id]; - } - if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { - delete this._in[target.id][cell.id]; - } - } else { - delete this._nodes[cell.id]; + V.ensureId(el); } - }, - _restructureOnReset: function(cells) { + this.node = el; - // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. - cells = cells.models; + this.setAttributes(attrs); - this._out = {}; - this._in = {}; - this._nodes = {}; - this._edges = {}; + if (children) { + this.append(children); + } - cells.forEach(this._restructureOnAdd, this); - }, + return this; + }; - _restructureOnChangeSource: function(link) { + var VPrototype = V.prototype; - var prevSource = link.previous('source'); - if (prevSource.id && this._out[prevSource.id]) { - delete this._out[prevSource.id][link.id]; - } - var source = link.get('source'); - if (source.id) { - (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; + Object.defineProperty(VPrototype, 'id', { + enumerable: true, + get: function() { + return this.node.id; + }, + set: function(id) { + this.node.id = id; } - }, + }); - _restructureOnChangeTarget: function(link) { + /** + * @param {SVGGElement} toElem + * @returns {SVGMatrix} + */ + VPrototype.getTransformToElement = function(toElem) { + toElem = V.toNode(toElem); + return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); + }; - var prevTarget = link.previous('target'); - if (prevTarget.id && this._in[prevTarget.id]) { - delete this._in[prevTarget.id][link.id]; + /** + * @param {SVGMatrix} matrix + * @param {Object=} opt + * @returns {Vectorizer|SVGMatrix} Setter / Getter + */ + VPrototype.transform = function(matrix, opt) { + + var node = this.node; + if (V.isUndefined(matrix)) { + return V.transformStringToMatrix(this.attr('transform')); } - var target = link.get('target'); - if (target.id) { - (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; + + if (opt && opt.absolute) { + return this.attr('transform', V.matrixToTransformString(matrix)); } - }, - // Return all outbound edges for the node. Return value is an object - // of the form: [edge] -> true - getOutboundEdges: function(node) { + var svgTransform = V.createSVGTransform(matrix); + node.transform.baseVal.appendItem(svgTransform); + return this; + }; - return (this._out && this._out[node]) || {}; - }, + VPrototype.translate = function(tx, ty, opt) { - // Return all inbound edges for the node. Return value is an object - // of the form: [edge] -> true - getInboundEdges: function(node) { + opt = opt || {}; + ty = ty || 0; - return (this._in && this._in[node]) || {}; - }, + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; + // Is it a getter? + if (V.isUndefined(tx)) { + return transform.translate; + } - toJSON: function() { + transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); - // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. - // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. - var json = Backbone.Model.prototype.toJSON.apply(this, arguments); - json.cells = this.get('cells').toJSON(); - return json; - }, + var newTx = opt.absolute ? tx : transform.translate.tx + tx; + var newTy = opt.absolute ? ty : transform.translate.ty + ty; + var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; - fromJSON: function(json, opt) { + // Note that `translate()` is always the first transformation. This is + // usually the desired case. + this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); + return this; + }; - if (!json.cells) { + VPrototype.rotate = function(angle, cx, cy, opt) { - throw new Error('Graph JSON must contain cells array.'); - } + opt = opt || {}; - return this.set(json, opt); - }, + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; - set: function(key, val, opt) { + // Is it a getter? + if (V.isUndefined(angle)) { + return transform.rotate; + } - var attrs; + transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); - // Handle both `key`, value and {key: value} style arguments. - if (typeof key === 'object') { - attrs = key; - opt = val; - } else { - (attrs = {})[key] = val; - } + angle %= 360; - // Make sure that `cells` attribute is handled separately via resetCells(). - if (attrs.hasOwnProperty('cells')) { - this.resetCells(attrs.cells, opt); - attrs = joint.util.omit(attrs, 'cells'); - } + var newAngle = opt.absolute ? angle : transform.rotate.angle + angle; + var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : ''; + var newRotate = 'rotate(' + newAngle + newOrigin + ')'; - // The rest of the attributes are applied via original set method. - return Backbone.Model.prototype.set.call(this, attrs, opt); - }, + this.attr('transform', (transformAttr + ' ' + newRotate).trim()); + return this; + }; - clear: function(opt) { + // Note that `scale` as the only transformation does not combine with previous values. + VPrototype.scale = function(sx, sy) { - opt = joint.util.assign({}, opt, { clear: true }); + sy = V.isUndefined(sy) ? sx : sy; - var collection = this.get('cells'); + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; - if (collection.length === 0) return this; + // Is it a getter? + if (V.isUndefined(sx)) { + return transform.scale; + } - this.startBatch('clear', opt); + transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim(); - // The elements come after the links. - var cells = collection.sortBy(function(cell) { - return cell.isLink() ? 1 : 2; - }); + var newScale = 'scale(' + sx + ',' + sy + ')'; - do { + this.attr('transform', (transformAttr + ' ' + newScale).trim()); + return this; + }; - // Remove all the cells one by one. - // Note that all the links are removed first, so it's - // safe to remove the elements without removing the connected - // links first. - cells.shift().remove(opt); + // 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. + VPrototype.bbox = function(withoutTransformations, target) { - } while (cells.length > 0); + var box; + var node = this.node; + var ownerSVGElement = node.ownerSVGElement; - this.stopBatch('clear'); + // 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 new g.Rect(0, 0, 0, 0); + } - return this; - }, + try { - _prepareCell: function(cell, opt) { + box = node.getBBox(); - var attrs; - if (cell instanceof Backbone.Model) { - attrs = cell.attributes; - if (!cell.graph && (!opt || !opt.dry)) { - // An element can not be member of more than one graph. - // A cell stops being the member of the graph after it's explicitely removed. - cell.graph = this; - } - } else { - // In case we're dealing with a plain JS object, we have to set the reference - // to the `graph` right after the actual model is created. This happens in the `model()` function - // of `joint.dia.GraphCells`. - attrs = cell; + } catch (e) { + + // Fallback for IE. + box = { + x: node.clientLeft, + y: node.clientTop, + width: node.clientWidth, + height: node.clientHeight + }; } - if (!joint.util.isString(attrs.type)) { - throw new TypeError('dia.Graph: cell type must be a string.'); + if (withoutTransformations) { + return new g.Rect(box); } - return cell; - }, + var matrix = this.getTransformToElement(target || ownerSVGElement); - maxZIndex: function() { + return V.transformRect(box, matrix); + }; - var lastCell = this.get('cells').last(); - return lastCell ? (lastCell.get('z') || 0) : 0; - }, + // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, + // i.e. after transformations are applied. + // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. + // Takes an (Object) `opt` argument (optional) with the following attributes: + // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this + // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); + VPrototype.getBBox = function(opt) { - addCell: function(cell, opt) { + var options = {}; - if (Array.isArray(cell)) { + var outputBBox; + var node = this.node; + var ownerSVGElement = node.ownerSVGElement; - return this.addCells(cell, opt); + // 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 new g.Rect(0, 0, 0, 0); } - if (cell instanceof Backbone.Model) { - - if (!cell.has('z')) { - cell.set('z', this.maxZIndex() + 1); + if (opt) { + if (opt.target) { // check if target exists + options.target = V.toNode(opt.target); // works for V objects, jquery objects, and node objects + } + if (opt.recursive) { + options.recursive = opt.recursive; } - - } else if (cell.z === undefined) { - - cell.z = this.maxZIndex() + 1; } - this.get('cells').add(this._prepareCell(cell, opt), opt || {}); + if (!options.recursive) { + try { + outputBBox = node.getBBox(); + } catch (e) { + // Fallback for IE. + outputBBox = { + x: node.clientLeft, + y: node.clientTop, + width: node.clientWidth, + height: node.clientHeight + }; + } - return this; - }, + if (!options.target) { + // transform like this (that is, not at all) + return new g.Rect(outputBBox); + } else { + // transform like target + var matrix = this.getTransformToElement(options.target); + return V.transformRect(outputBBox, matrix); + } + } else { // if we want to calculate the bbox recursively + // browsers report correct bbox around svg elements (one that envelops the path lines tightly) + // but some browsers fail to report the same bbox when the elements are in a group (returning a looser bbox that also includes control points, like node.getClientRect()) + // this happens even if we wrap a single svg element into a group! + // this option setting makes the function recursively enter all the groups from this and deeper, get bboxes of the elements inside, then return a union of those bboxes - addCells: function(cells, opt) { + var children = this.children(); + var n = children.length; - if (cells.length) { + if (n === 0) { + return this.getBBox({ target: options.target, recursive: false }); + } - cells = joint.util.flattenDeep(cells); - opt.position = cells.length; + // recursion's initial pass-through setting: + // recursive passes-through just keep the target as whatever was set up here during the initial pass-through + if (!options.target) { + // transform children/descendants like this (their parent/ancestor) + options.target = this; + } // else transform children/descendants like target - this.startBatch('add'); - cells.forEach(function(cell) { - opt.position--; - this.addCell(cell, opt); - }, this); - this.stopBatch('add'); - } + for (var i = 0; i < n; i++) { + var currentChild = children[i]; - return this; - }, + var childBBox; - // When adding a lot of cells, it is much more efficient to - // reset the entire cells collection in one go. - // Useful for bulk operations and optimizations. - resetCells: function(cells, opt) { + // if currentChild is not a group element, get its bbox with a nonrecursive call + if (currentChild.children().length === 0) { + childBBox = currentChild.getBBox({ target: options.target, recursive: false }); + } + else { + // if currentChild is a group element (determined by checking the number of children), enter it with a recursive call + childBBox = currentChild.getBBox({ target: options.target, recursive: true }); + } - var preparedCells = joint.util.toArray(cells).map(function(cell) { - return this._prepareCell(cell, opt); - }, this); - this.get('cells').reset(preparedCells, opt); + if (!outputBBox) { + // if this is the first iteration + outputBBox = childBBox; + } else { + // make a new bounding box rectangle that contains this child's bounding box and previous bounding box + outputBBox = outputBBox.union(childBBox); + } + } - return this; - }, + return outputBBox; + } + }; - removeCells: function(cells, opt) { + // Text() helpers - if (cells.length) { + function createTextPathNode(attrs, vel) { + attrs || (attrs = {}); + var textPathElement = V('textPath'); + var d = attrs.d; + if (d && attrs['xlink:href'] === undefined) { + // If `opt.attrs` 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. + // Wrap the text in the SVG element that points + // to a path defined by `opt.attrs` inside the `` element. + var linkedPath = V('path').attr('d', d).appendTo(vel.defs()); + textPathElement.attr('xlink:href', '#' + linkedPath.id); + } + if (V.isObject(attrs)) { + // 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. + textPathElement.attr(attrs); + } + return textPathElement.node; + } - this.startBatch('remove'); - joint.util.invoke(cells, 'remove', opt); - this.stopBatch('remove'); + function annotateTextLine(lineNode, lineAnnotations, opt) { + opt || (opt = {}); + var includeAnnotationIndices = opt.includeAnnotationIndices; + var eol = opt.eol; + var lineHeight = opt.lineHeight; + var baseSize = opt.baseSize; + var maxFontSize = 0; + var fontMetrics = {}; + var lastJ = lineAnnotations.length - 1; + for (var j = 0; j <= lastJ; j++) { + var annotation = lineAnnotations[j]; + var fontSize = null; + if (V.isObject(annotation)) { + var annotationAttrs = annotation.attrs; + var vTSpan = V('tspan', annotationAttrs); + var tspanNode = vTSpan.node; + var t = annotation.t; + if (eol && j === lastJ) t += eol; + tspanNode.textContent = t; + // Per annotation className + var annotationClass = annotationAttrs['class']; + if (annotationClass) vTSpan.addClass(annotationClass); + // 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. + if (includeAnnotationIndices) vTSpan.attr('annotations', annotation.annotations); + // Check for max font size + fontSize = parseFloat(annotationAttrs['font-size']); + if (fontSize === undefined) fontSize = baseSize; + if (fontSize && fontSize > maxFontSize) maxFontSize = fontSize; + } else { + if (eol && j === lastJ) annotation += eol; + tspanNode = document.createTextNode(annotation || ' '); + if (baseSize && baseSize > maxFontSize) maxFontSize = baseSize; + } + lineNode.appendChild(tspanNode); } - return this; - }, + if (maxFontSize) fontMetrics.maxFontSize = maxFontSize; + if (lineHeight) { + fontMetrics.lineHeight = lineHeight; + } else if (maxFontSize) { + fontMetrics.lineHeight = (maxFontSize * 1.2); + } + return fontMetrics; + } - _removeCell: function(cell, collection, options) { + var emRegex = /em$/; - options = options || {}; + function convertEmToPx(em, fontSize) { + var numerical = parseFloat(em); + if (emRegex.test(em)) return numerical * fontSize; + return numerical; + } - if (!options.clear) { - // Applications might provide a `disconnectLinks` option set to `true` in order to - // disconnect links when a cell is removed rather then removing them. The default - // is to remove all the associated links. - if (options.disconnectLinks) { + function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) { + if (!Array.isArray(linesMetrics)) return 0; + var n = linesMetrics.length; + if (!n) return 0; + var lineMetrics = linesMetrics[0]; + var flMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var rLineHeights = 0; + var lineHeightPx = convertEmToPx(lineHeight, baseSizePx); + for (var i = 1; i < n; i++) { + lineMetrics = linesMetrics[i]; + var iLineHeight = convertEmToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx; + rLineHeights += iLineHeight; + } + var llMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var dy; + switch (alignment) { + case 'middle': + dy = (flMaxFont / 2) - (0.15 * llMaxFont) - (rLineHeights / 2); + break; + case 'bottom': + dy = -(0.25 * llMaxFont) - rLineHeights; + break; + default: + case 'top': + dy = (0.8 * flMaxFont) + break; + } + return dy; + } - this.disconnectLinks(cell, options); + VPrototype.text = function(content, opt) { - } else { + if (content && typeof content !== 'string') throw new Error('Vectorizer: text() expects the first argument to be a string.'); - this.removeLinks(cell, options); - } + // 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 = {}); + + // End of Line character + var eol = opt.eol; + // Text along path + var textPath = opt.textPath + // Vertical shift + var verticalAnchor = opt.textVerticalAnchor; + var namedVerticalAnchor = (verticalAnchor === 'middle' || verticalAnchor === 'bottom' || verticalAnchor === 'top'); + // Horizontal shift applied to all the lines but the first. + var x = opt.x; + if (x === undefined) x = this.attr('x') || 0; + // Annotations + var iai = opt.includeAnnotationIndices; + var annotations = opt.annotations; + if (annotations && !V.isArray(annotations)) annotations = [annotations]; + // Shift all the but first by one line (`1em`) + var defaultLineHeight = opt.lineHeight; + var autoLineHeight = (defaultLineHeight === 'auto'); + var lineHeight = (autoLineHeight) ? '1.5em' : (defaultLineHeight || '1em'); + // Clearing the element + this.empty(); + this.attr({ + // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. + 'xml:space': 'preserve', + // 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. + 'display': (content) ? null : 'none' + }); + // Set default font-size if none + var fontSize = parseFloat(this.attr('font-size')); + if (!fontSize) { + fontSize = 16; + if (namedVerticalAnchor || annotations) this.attr('font-size', fontSize); } - // Silently remove the cell from the cells collection. Silently, because - // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is - // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events - // would be triggered on the graph model. - this.get('cells').remove(cell, { silent: true }); - if (cell.graph === this) { - // Remove the element graph reference only if the cell is the member of this graph. - cell.graph = null; + var doc = document; + var containerNode; + if (textPath) { + // Now all the ``s will be inside the ``. + if (typeof textPath === 'string') textPath = { d: textPath }; + containerNode = createTextPathNode(textPath, this); + } else { + containerNode = doc.createDocumentFragment(); } - }, + var offset = 0; + var lines = content.split('\n'); + var linesMetrics = []; + var annotatedY; + for (var i = 0, lastI = lines.length - 1; i <= lastI; i++) { + var dy = lineHeight; + var lineClassName = 'v-line'; + var lineNode = doc.createElementNS(V.namespace.xmlns, 'tspan'); + var line = lines[i]; + var lineMetrics; + if (line) { + if (annotations) { + // Find the *compacted* annotations for this line. + var lineAnnotations = V.annotateString(line, annotations, { + offset: -offset, + includeAnnotationIndices: iai + }); + lineMetrics = annotateTextLine(lineNode, lineAnnotations, { + includeAnnotationIndices: iai, + eol: (i !== lastI && eol), + lineHeight: (autoLineHeight) ? null : lineHeight, + baseSize: fontSize + }); + // Get the line height based on the biggest font size in the annotations for this line. + var iLineHeight = lineMetrics.lineHeight; + if (iLineHeight && autoLineHeight && i !== 0) dy = iLineHeight; + if (i === 0) annotatedY = lineMetrics.maxFontSize * 0.8; + } else { + if (eol && i !== lastI) line += eol; + lineNode.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. + lineNode.textContent = '-'; + lineClassName += ' v-empty-line'; + // 'opacity' needs to be specified with fill, stroke. Opacity without specification + // is not applied in Firefox + var lineNodeStyle = lineNode.style; + lineNodeStyle.fillOpacity = 0; + lineNodeStyle.strokeOpacity = 0; + if (annotations) lineMetrics = {}; + } + if (lineMetrics) linesMetrics.push(lineMetrics); + if (i > 0) lineNode.setAttribute('dy', dy); + // Firefox requires 'x' to be set on the first line when inside a text path + if (i > 0 || textPath) lineNode.setAttribute('x', x); + lineNode.className.baseVal = lineClassName; + containerNode.appendChild(lineNode); + offset += line.length + 1; // + 1 = newline character. + } + // Y Alignment calculation + if (namedVerticalAnchor) { + if (annotations) { + dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight); + } else if (verticalAnchor === 'top') { + // A shortcut for top alignment. It does not depend on font-size nor line-height + dy = '0.8em'; + } else { + var rh; // remaining height + if (lastI > 0) { + rh = parseFloat(lineHeight) || 1; + rh *= lastI; + if (!emRegex.test(lineHeight)) rh /= fontSize; + } else { + // Single-line text + rh = 0; + } + switch (verticalAnchor) { + case 'middle': + dy = (0.3 - (rh / 2)) + 'em' + break; + case 'bottom': + dy = (-rh - 0.3) + 'em' + break; + } + } + } else { + if (verticalAnchor === 0) { + dy = '0em'; + } else if (verticalAnchor) { + dy = verticalAnchor; + } else { + // No vertical anchor is defined + dy = 0; + // Backwards compatibility - we change the `y` attribute instead of `dy`. + if (this.attr('y') === null) this.attr('y', annotatedY || '0.8em'); + } + } + containerNode.firstChild.setAttribute('dy', dy); + // Appending lines to the element. + this.append(containerNode); + return this; + }; - // Get a cell by `id`. - getCell: function(id) { + /** + * @public + * @param {string} name + * @returns {Vectorizer} + */ + VPrototype.removeAttr = function(name) { - return this.get('cells').get(id); - }, + var qualifiedName = V.qualifyAttr(name); + var el = this.node; - getCells: function() { + 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; + }; - return this.get('cells').toArray(); - }, + VPrototype.attr = function(name, value) { - getElements: function() { - return Object.keys(this._nodes).map(this.getCell, this); - }, + if (V.isUndefined(name)) { - getLinks: function() { - return Object.keys(this._edges).map(this.getCell, this); - }, + // Return all attributes. + var attributes = this.node.attributes; + var attrs = {}; - getFirstCell: function() { + for (var i = 0; i < attributes.length; i++) { + attrs[attributes[i].name] = attributes[i].value; + } - return this.get('cells').first(); - }, + return attrs; + } - getLastCell: function() { + if (V.isString(name) && V.isUndefined(value)) { + return this.node.getAttribute(name); + } - return this.get('cells').last(); - }, + if (typeof name === 'object') { - // Get all inbound and outbound links connected to the cell `model`. - getConnectedLinks: function(model, opt) { + for (var attrName in name) { + if (name.hasOwnProperty(attrName)) { + this.setAttribute(attrName, name[attrName]); + } + } - opt = opt || {}; + } else { - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; + this.setAttribute(name, value); } - // The final array of connected link models. - var links = []; - // Connected edges. This hash table ([edge] -> true) serves only - // for a quick lookup to check if we already added a link. - var edges = {}; - - if (outbound) { - joint.util.forIn(this.getOutboundEdges(model.id), function(exists, edge) { - if (!edges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); - } - if (inbound) { - joint.util.forIn(this.getInboundEdges(model.id), function(exists, edge) { - // Skip links that were already added. Those must be self-loop links - // because they are both inbound and outbond edges of the same element. - if (!edges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); - } + return this; + }; - // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells - // and are not descendents themselves. - if (opt.deep) { + VPrototype.normalizePath = function() { - var embeddedCells = model.getEmbeddedCells({ deep: true }); - // In the first round, we collect all the embedded edges so that we can exclude - // them from the final result. - var embeddedEdges = {}; - embeddedCells.forEach(function(cell) { - if (cell.isLink()) { - embeddedEdges[cell.id] = true; - } - }); - embeddedCells.forEach(function(cell) { - if (cell.isLink()) return; - if (outbound) { - joint.util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { - if (!edges[edge] && !embeddedEdges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); - } - if (inbound) { - joint.util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { - if (!edges[edge] && !embeddedEdges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); - } - }, this); + var tagName = this.tagName(); + if (tagName === 'PATH') { + this.attr('d', V.normalizePathData(this.attr('d'))); } - return links; - }, - - getNeighbors: function(model, opt) { + return this; + } - opt = opt || {}; + VPrototype.remove = function() { - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; + if (this.node.parentNode) { + this.node.parentNode.removeChild(this.node); } - var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { + return this; + }; - var source = link.get('source'); - var target = link.get('target'); - var loop = link.hasLoop(opt); + VPrototype.empty = function() { - // Discard if it is a point, or if the neighbor was already added. - if (inbound && joint.util.has(source, 'id') && !res[source.id]) { + while (this.node.firstChild) { + this.node.removeChild(this.node.firstChild); + } - var sourceElement = this.getCell(source.id); + return this; + }; - if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { - res[source.id] = sourceElement; - } + /** + * @private + * @param {object} attrs + * @returns {Vectorizer} + */ + VPrototype.setAttributes = function(attrs) { + + for (var key in attrs) { + if (attrs.hasOwnProperty(key)) { + this.setAttribute(key, attrs[key]); } + } - // Discard if it is a point, or if the neighbor was already added. - if (outbound && joint.util.has(target, 'id') && !res[target.id]) { + return this; + }; - var targetElement = this.getCell(target.id); + VPrototype.append = function(els) { - if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { - res[target.id] = targetElement; - } - } + if (!V.isArray(els)) { + els = [els]; + } - return res; - }.bind(this), {}); + for (var i = 0, len = els.length; i < len; i++) { + this.node.appendChild(V.toNode(els[i])); + } - return joint.util.toArray(neighbors); - }, + return this; + }; - getCommonAncestor: function(/* cells */) { + VPrototype.prepend = function(els) { - var cellsAncestors = Array.from(arguments).map(function(cell) { + var child = this.node.firstChild; + return child ? V(child).before(els) : this.append(els); + }; - var ancestors = []; - var parentId = cell.get('parent'); + VPrototype.before = function(els) { - while (parentId) { + var node = this.node; + var parent = node.parentNode; - ancestors.push(parentId); - parentId = this.getCell(parentId).get('parent'); + if (parent) { + + if (!V.isArray(els)) { + els = [els]; } - return ancestors; + for (var i = 0, len = els.length; i < len; i++) { + parent.insertBefore(V.toNode(els[i]), node); + } + } - }, this); + return this; + }; - cellsAncestors = cellsAncestors.sort(function(a, b) { - return a.length - b.length; - }); + VPrototype.appendTo = function(node) { + V.toNode(node).appendChild(this.node); + return this; + }, - var commonAncestor = joint.util.toArray(cellsAncestors.shift()).find(function(ancestor) { - return cellsAncestors.every(function(cellAncestors) { - return cellAncestors.includes(ancestor); - }); - }); + VPrototype.svg = function() { - return this.getCell(commonAncestor); - }, + return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); + }; - // Find the whole branch starting at `element`. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. - getSuccessors: function(element, opt) { + VPrototype.tagName = function() { - opt = opt || {}; - var res = []; - // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. - this.search(element, function(el) { - if (el !== element) { - res.push(el); - } - }, joint.util.assign({}, opt, { outbound: true })); - return res; - }, + return this.node.tagName.toUpperCase(); + }; - // Clone `cells` returning an object that maps the original cell ID to the clone. The number - // of clones is exactly the same as the `cells.length`. - // This function simply clones all the `cells`. However, it also reconstructs - // all the `source/target` and `parent/embed` references within the `cells`. - // This is the main difference from the `cell.clone()` method. The - // `cell.clone()` method works on one single cell only. - // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` - // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. - // the source and target of the link `L2` is changed to point to `A2` and `B2`. - cloneCells: function(cells) { + VPrototype.defs = function() { + var context = this.svg() || this; + var defsNode = context.node.getElementsByTagName('defs')[0]; + if (defsNode) return V(defsNode); + return V('defs').appendTo(context); + }; - cells = joint.util.uniq(cells); + VPrototype.clone = function() { - // A map of the form [original cell ID] -> [clone] helping - // us to reconstruct references for source/target and parent/embeds. - // This is also the returned value. - var cloneMap = joint.util.toArray(cells).reduce(function(map, cell) { - map[cell.id] = cell.clone(); - return map; - }, {}); + 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; + }; - joint.util.toArray(cells).forEach(function(cell) { + VPrototype.findOne = function(selector) { - var clone = cloneMap[cell.id]; - // assert(clone exists) + var found = this.node.querySelector(selector); + return found ? V(found) : undefined; + }; - if (clone.isLink()) { - var source = clone.get('source'); - var target = clone.get('target'); - if (source.id && cloneMap[source.id]) { - // Source points to an element and the element is among the clones. - // => Update the source of the cloned link. - clone.prop('source/id', cloneMap[source.id].id); - } - if (target.id && cloneMap[target.id]) { - // Target points to an element and the element is among the clones. - // => Update the target of the cloned link. - clone.prop('target/id', cloneMap[target.id].id); - } - } + VPrototype.find = function(selector) { - // Find the parent of the original cell - var parent = cell.get('parent'); - if (parent && cloneMap[parent]) { - clone.set('parent', cloneMap[parent].id); + 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])); } + } - // Find the embeds of the original cell - var embeds = joint.util.toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) { - // Embedded cells that are not being cloned can not be carried - // over with other embedded cells. - if (cloneMap[embed]) { - newEmbeds.push(cloneMap[embed].id); - } - return newEmbeds; - }, []); + return vels; + }; - if (!joint.util.isEmpty(embeds)) { - clone.set('embeds', embeds); + // Returns an array of V elements made from children of this.node. + VPrototype.children = function() { + + var children = this.node.childNodes; + + var outputArray = []; + for (var i = 0; i < children.length; i++) { + var currentChild = children[i]; + if (currentChild.nodeType === 1) { + outputArray.push(V(children[i])); } - }); + } + return outputArray; + }; - return cloneMap; - }, + // Find an index of an element inside its container. + VPrototype.index = function() { - // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). - // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. - // Return a map of the form: [original cell ID] -> [clone]. - cloneSubgraph: function(cells, opt) { + var index = 0; + var node = this.node.previousSibling; - var subgraph = this.getSubgraph(cells, opt); - return this.cloneCells(subgraph); - }, + while (node) { + // nodeType 1 for ELEMENT_NODE + if (node.nodeType === 1) index++; + node = node.previousSibling; + } - // Return `cells` and all the connected links that connect cells in the `cells` array. - // If `opt.deep` is `true`, return all the cells including all their embedded cells - // and all the links that connect any of the returned cells. - // For example, for a single shallow element, the result is that very same element. - // For two elements connected with a link: `A --- L ---> B`, the result for - // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. - getSubgraph: function(cells, opt) { + return index; + }; - opt = opt || {}; + VPrototype.findParentByClass = function(className, terminator) { - var subgraph = []; - // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. - var cellMap = {}; - var elements = []; - var links = []; + var ownerSVGElement = this.node.ownerSVGElement; + var node = this.node.parentNode; - joint.util.toArray(cells).forEach(function(cell) { - if (!cellMap[cell.id]) { - subgraph.push(cell); - cellMap[cell.id] = cell; - if (cell.isLink()) { - links.push(cell); - } else { - elements.push(cell); - } - } + while (node && node !== terminator && node !== ownerSVGElement) { - if (opt.deep) { - var embeds = cell.getEmbeddedCells({ deep: true }); - embeds.forEach(function(embed) { - if (!cellMap[embed.id]) { - subgraph.push(embed); - cellMap[embed.id] = embed; - if (embed.isLink()) { - links.push(embed); - } else { - elements.push(embed); - } - } - }); + var vel = V(node); + if (vel.hasClass(className)) { + return vel; } - }); - links.forEach(function(link) { - // For links, return their source & target (if they are elements - not points). - var source = link.get('source'); - var target = link.get('target'); - if (source.id && !cellMap[source.id]) { - var sourceElement = this.getCell(source.id); - subgraph.push(sourceElement); - cellMap[sourceElement.id] = sourceElement; - elements.push(sourceElement); - } - if (target.id && !cellMap[target.id]) { - var targetElement = this.getCell(target.id); - subgraph.push(this.getCell(target.id)); - cellMap[targetElement.id] = targetElement; - elements.push(targetElement); - } - }, this); + node = node.parentNode; + } - elements.forEach(function(element) { - // For elements, include their connected links if their source/target is in the subgraph; - var links = this.getConnectedLinks(element, opt); - links.forEach(function(link) { - var source = link.get('source'); - var target = link.get('target'); - if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { - subgraph.push(link); - cellMap[link.id] = link; - } - }); - }, this); + return null; + }; - return subgraph; - }, + // https://jsperf.com/get-common-parent + VPrototype.contains = function(el) { - // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. - getPredecessors: function(element, opt) { + var a = this.node; + var b = V.toNode(el); + var bup = b && b.parentNode; - opt = opt || {}; - var res = []; - // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. - this.search(element, function(el) { - if (el !== element) { - res.push(el); - } - }, joint.util.assign({}, opt, { inbound: true })); - return res; - }, + return (a === bup) || !!(bup && bup.nodeType === 1 && (a.compareDocumentPosition(bup) & 16)); + }; - // Perform search on the graph. - // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. - // By setting `opt.inbound` to `true`, you can reverse the direction of the search. - // If `opt.deep` is `true`, take into account embedded elements too. - // `iteratee` is a function of the form `function(element) {}`. - // If `iteratee` explicitely returns `false`, the searching stops. - search: function(element, iteratee, opt) { + // Convert global point into the coordinate space of this element. + VPrototype.toLocalPoint = function(x, y) { - opt = opt || {}; - if (opt.breadthFirst) { - this.bfs(element, iteratee, opt); - } else { - this.dfs(element, iteratee, opt); - } - }, + var svg = this.svg().node; - // Breadth-first search. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). - // `iteratee` is a function of the form `function(element, distance) {}`. - // where `element` is the currently visited element and `distance` is the distance of that element - // from the root `element` passed the `bfs()`, i.e. the element we started the search from. - // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels - // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. - // If `iteratee` explicitely returns `false`, the searching stops. - bfs: function(element, iteratee, opt) { + var p = svg.createSVGPoint(); + p.x = x; + p.y = y; - opt = opt || {}; - var visited = {}; - var distance = {}; - var queue = []; + try { - queue.push(element); - distance[element.id] = 0; + var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse()); + var globalToLocalMatrix = this.getTransformToElement(svg).inverse(); - while (queue.length > 0) { - var next = queue.shift(); - if (!visited[next.id]) { - visited[next.id] = true; - if (iteratee(next, distance[next.id]) === false) return; - this.getNeighbors(next, opt).forEach(function(neighbor) { - distance[neighbor.id] = distance[next.id] + 1; - queue.push(neighbor); - }); - } + } 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; } - }, - // Depth-first search. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). - // `iteratee` is a function of the form `function(element, distance) {}`. - // If `iteratee` explicitely returns `false`, the search stops. - dfs: function(element, iteratee, opt, _visited, _distance) { + return globalPoint.matrixTransform(globalToLocalMatrix); + }; - opt = opt || {}; - var visited = _visited || {}; - var distance = _distance || 0; - if (iteratee(element, distance) === false) return; - visited[element.id] = true; + VPrototype.translateCenterToPoint = function(p) { - this.getNeighbors(element, opt).forEach(function(neighbor) { - if (!visited[neighbor.id]) { - this.dfs(neighbor, iteratee, opt, visited, distance + 1); - } - }, this); - }, + var bbox = this.getBBox({ target: this.svg() }); + var center = bbox.center(); - // Get all the roots of the graph. Time complexity: O(|V|). - getSources: function() { + this.translate(p.x - center.x, p.y - center.y); + return this; + }; - var sources = []; - joint.util.forIn(this._nodes, function(exists, node) { - if (!this._in[node] || joint.util.isEmpty(this._in[node])) { - sources.push(this.getCell(node)); - } - }.bind(this)); - return sources; - }, + // 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. + VPrototype.translateAndAutoOrient = function(position, reference, target) { - // Get all the leafs of the graph. Time complexity: O(|V|). - getSinks: function() { + // 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 sinks = []; - joint.util.forIn(this._nodes, function(exists, node) { - if (!this._out[node] || joint.util.isEmpty(this._out[node])) { - sinks.push(this.getCell(node)); - } - }.bind(this)); - return sinks; - }, + var s = this.scale(); + this.attr('transform', ''); + this.scale(s.sx, s.sy); - // Return `true` if `element` is a root. Time complexity: O(1). - isSource: function(element) { + var svg = this.svg().node; + var bbox = this.getBBox({ target: target || svg }); - return !this._in[element.id] || joint.util.isEmpty(this._in[element.id]); - }, + // 1. Translate to origin. + var translateToOrigin = svg.createSVGTransform(); + translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2); - // Return `true` if `element` is a leaf. Time complexity: O(1). - isSink: function(element) { + // 2. Rotate around origin. + var rotateAroundOrigin = svg.createSVGTransform(); + var angle = (new g.Point(position)).changeInAngle(position.x - reference.x, position.y - reference.y, reference); + rotateAroundOrigin.setRotate(angle, 0, 0); - return !this._out[element.id] || joint.util.isEmpty(this._out[element.id]); - }, + // 3. Translate to the `position` + the offset (half my width) towards the `reference` point. + var translateFinal = svg.createSVGTransform(); + var finalPosition = (new g.Point(position)).move(reference, bbox.width / 2); + translateFinal.setTranslate(position.x + (position.x - finalPosition.x), position.y + (position.y - finalPosition.y)); - // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. - isSuccessor: function(elementA, elementB) { + // 4. Apply transformations. + var ctm = this.getTransformToElement(target || svg); + var transform = svg.createSVGTransform(); + transform.setMatrix( + translateFinal.matrix.multiply( + rotateAroundOrigin.matrix.multiply( + translateToOrigin.matrix.multiply( + ctm))) + ); - var isSuccessor = false; - this.search(elementA, function(element) { - if (element === elementB && element !== elementA) { - isSuccessor = true; - return false; - } - }, { outbound: true }); - return isSuccessor; - }, + // 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); - // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. - isPredecessor: function(elementA, elementB) { + var decomposition = V.decomposeMatrix(transform.matrix); - var isPredecessor = false; - this.search(elementA, function(element) { - if (element === elementB && element !== elementA) { - isPredecessor = true; - return false; - } - }, { inbound: true }); - return isPredecessor; - }, + 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 `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. - // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` - // for more details. - // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. - // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. - isNeighbor: function(elementA, elementB, opt) { + return this; + }; - opt = opt || {}; + VPrototype.animateAlongPath = function(attrs, path) { - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; - } + path = V.toNode(path); - var isNeighbor = false; + var id = V.ensureId(path); + var animateMotion = V('animateMotion', attrs); + var mpath = V('mpath', { 'xlink:href': '#' + id }); - this.getConnectedLinks(elementA, opt).forEach(function(link) { + animateMotion.append(mpath); - var source = link.get('source'); - var target = link.get('target'); + 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') { - // Discard if it is a point. - if (inbound && joint.util.has(source, 'id') && source.id === elementB.id) { - isNeighbor = true; - return false; - } + // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) + var animation = animateMotion.node; + animation.animators = []; - // Discard if it is a point, or if the neighbor was already added. - if (outbound && joint.util.has(target, 'id') && target.id === elementB.id) { - isNeighbor = true; - return false; + 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(); + } } - }); + } + return this; + }; - return isNeighbor; - }, + VPrototype.hasClass = function(className) { - // Disconnect links connected to the cell `model`. - disconnectLinks: function(model, opt) { + return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); + }; - this.getConnectedLinks(model).forEach(function(link) { + VPrototype.addClass = function(className) { - link.set(link.get('source').id === model.id ? 'source' : 'target', { x: 0, y: 0 }, opt); - }); - }, + if (!this.hasClass(className)) { + var prevClasses = this.node.getAttribute('class') || ''; + this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); + } - // Remove links connected to the cell `model` completely. - removeLinks: function(model, opt) { + return this; + }; - joint.util.invoke(this.getConnectedLinks(model), 'remove', opt); - }, + VPrototype.removeClass = function(className) { - // Find all elements at given point - findModelsFromPoint: function(p) { + 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.getElements().filter(function(el) { - return el.getBBox().containsPoint(p); - }); - }, + return this; + }; - // Find all elements in given area - findModelsInArea: function(rect, opt) { + VPrototype.toggleClass = function(className, toAdd) { - rect = g.rect(rect); - opt = joint.util.defaults(opt || {}, { strict: false }); + var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; - var method = opt.strict ? 'containsRect' : 'intersect'; + if (toRemove) { + this.removeClass(className); + } else { + this.addClass(className); + } - return this.getElements().filter(function(el) { - return rect[method](el.getBBox()); - }); - }, - - // Find all elements under the given element. - findModelsUnderElement: function(element, opt) { + return this; + }; - opt = joint.util.defaults(opt || {}, { searchBy: 'bbox' }); + // 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). + VPrototype.sample = function(interval) { - var bbox = element.getBBox(); - var elements = (opt.searchBy === 'bbox') - ? this.findModelsInArea(bbox) - : this.findModelsFromPoint(bbox[opt.searchBy]()); + 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; + }; - // don't account element itself or any of its descendents - return elements.filter(function(el) { - return element.id !== el.id && !el.isEmbeddedIn(element); - }); - }, + VPrototype.convertToPath = function() { + var path = V('path'); + path.attr(this.attr()); + var d = this.convertToPathData(); + if (d) { + path.attr('d', d); + } + return path; + }; - // Return bounding box of all elements. - getBBox: function(cells, opt) { + VPrototype.convertToPathData = function() { - return this.getCellsBBox(cells || this.getElements(), opt); - }, + var tagName = this.tagName(); - // Return the bounding box of all cells in array provided. - // Links are being ignored. - getCellsBBox: function(cells, opt) { + 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); + } - return joint.util.toArray(cells).reduce(function(memo, cell) { - if (cell.isLink()) return memo; - if (memo) { - return memo.union(cell.getBBox(opt)); - } else { - return cell.getBBox(opt); - } - }, null); - }, + throw new Error(tagName + ' cannot be converted to PATH.'); + }; - translate: function(dx, dy, opt) { + V.prototype.toGeometryShape = function() { + var x, y, width, height, cx, cy, r, rx, ry, points, d; + switch (this.tagName()) { - // Don't translate cells that are embedded in any other cell. - var cells = this.getCells().filter(function(cell) { - return !cell.isEmbedded(); - }); + case 'RECT': + x = parseFloat(this.attr('x')) || 0; + y = parseFloat(this.attr('y')) || 0; + width = parseFloat(this.attr('width')) || 0; + height = parseFloat(this.attr('height')) || 0; + return new g.Rect(x, y, width, height); - joint.util.invoke(cells, 'translate', dx, dy, opt); + case 'CIRCLE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + r = parseFloat(this.attr('r')) || 0; + return new g.Ellipse({ x: cx, y: cy }, r, r); - return this; - }, + case 'ELLIPSE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + rx = parseFloat(this.attr('rx')) || 0; + ry = parseFloat(this.attr('ry')) || 0; + return new g.Ellipse({ x: cx, y: cy }, rx, ry); - resize: function(width, height, opt) { + case 'POLYLINE': + points = V.getPointsFromSvgNode(this); + return new g.Polyline(points); - return this.resizeCells(width, height, this.getCells(), opt); - }, + case 'POLYGON': + points = V.getPointsFromSvgNode(this); + if (points.length > 1) points.push(points[0]); + return new g.Polyline(points); - resizeCells: function(width, height, cells, opt) { + case 'PATH': + d = this.attr('d'); + if (!g.Path.isDataSupported(d)) d = V.normalizePathData(d); + return new g.Path(d); - // `getBBox` method returns `null` if no elements provided. - // i.e. cells can be an array of links - var bbox = this.getCellsBBox(cells); - if (bbox) { - var sx = Math.max(width / bbox.width, 0); - var sy = Math.max(height / bbox.height, 0); - joint.util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); + case 'LINE': + x1 = parseFloat(this.attr('x1')) || 0; + y1 = parseFloat(this.attr('y1')) || 0; + x2 = parseFloat(this.attr('x2')) || 0; + y2 = parseFloat(this.attr('y2')) || 0; + return new g.Line({ x: x1, y: y1 }, { x: x2, y: y2 }); } - return this; + // Anything else is a rectangle + return this.getBBox(); }, - startBatch: function(name, data) { + // 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. + VPrototype.findIntersection = function(ref, target) { - data = data || {}; - this._batches[name] = (this._batches[name] || 0) + 1; + var svg = this.svg().node; + target = target || svg; + var bbox = this.getBBox({ target: target }); + var center = bbox.center(); - return this.trigger('batch:start', joint.util.assign({}, data, { batchName: name })); - }, + if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; - stopBatch: function(name, data) { + var spot; + var tagName = this.tagName(); - data = data || {}; - this._batches[name] = (this._batches[name] || 0) - 1; + // 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') { - return this.trigger('batch:stop', joint.util.assign({}, data, { batchName: name })); - }, + var gRect = new 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 = (new g.Rect(rect)).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation); - hasActiveBatch: function(name) { - if (name) { - return !!this._batches[name]; - } else { - return joint.util.toArray(this._batches).some(function(batches) { - return batches > 0; - }); - } - } -}); + } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { -joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); + var pathNode = (tagName === 'PATH') ? this : this.convertToPath(); + var samples = pathNode.sample(); + var minDistance = Infinity; + var closestSamples = []; -(function(joint, _, g, $, util) { + var i, sample, gp, centerDistance, refDistance, distance; - function isPercentage(val) { - return util.isString(val) && val.slice(-1) === '%'; - } + for (i = 0; i < samples.length; i++) { - function setWrapper(attrName, dimension) { - return function(value, refBBox) { - var isValuePercentage = isPercentage(value); - value = parseFloat(value); - if (isValuePercentage) { - value /= 100; - } + 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 = new 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; - var attrs = {}; - if (isFinite(value)) { - var attrValue = (isValuePercentage || value >= 0 && value <= 1) - ? value * refBBox[dimension] - : Math.max(value + refBBox[dimension], 0); - attrs[attrName] = attrValue; + if (distance < minDistance) { + minDistance = distance; + closestSamples = [{ sample: sample, refDistance: refDistance }]; + } else if (distance < minDistance + 1) { + closestSamples.push({ sample: sample, refDistance: refDistance }); + } } - return attrs; - }; - } - - function positionWrapper(axis, dimension, origin) { - return function(value, refBBox) { - var valuePercentage = isPercentage(value); - value = parseFloat(value); - if (valuePercentage) { - value /= 100; - } + closestSamples.sort(function(a, b) { + return a.refDistance - b.refDistance; + }); - var delta; - if (isFinite(value)) { - var refOrigin = refBBox[origin](); - if (valuePercentage || value > 0 && value < 1) { - delta = refOrigin[axis] + refBBox[dimension] * value; - } else { - delta = refOrigin[axis] + value; - } + if (closestSamples[0]) { + spot = closestSamples[0].sample; } + } - var point = g.Point(); - point[axis] = delta || 0; - return point; - }; - } + return spot; + }; - function offsetWrapper(axis, dimension, corner) { - return function(value, nodeBBox) { - var delta; - if (value === 'middle') { - delta = nodeBBox[dimension] / 2; - } else if (value === corner) { - delta = nodeBBox[dimension]; - } else if (isFinite(value)) { - // TODO: or not to do a breaking change? - delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value; - } else if (isPercentage(value)) { - delta = nodeBBox[dimension] * parseFloat(value) / 100; - } else { - delta = 0; - } + /** + * @private + * @param {string} name + * @param {string} value + * @returns {Vectorizer} + */ + VPrototype.setAttribute = function(name, value) { - var point = g.Point(); - point[axis] = -(nodeBBox[axis] + delta); - return point; - }; - } + var el = this.node; - var attributesNS = joint.dia.attributes = { + if (value === null) { + this.removeAttr(name); + return this; + } - xlinkHref: { - set: 'xlink:href' - }, + var qualifiedName = V.qualifyAttr(name); - xlinkShow: { - set: 'xlink:show' - }, + 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); + } - xlinkRole: { - set: 'xlink:role' - }, + return this; + }; - xlinkType: { - set: 'xlink:type' - }, + // Create an SVG document element. + // If `content` is passed, it will be used as the SVG content of the `` root element. + V.createSvgDocument = function(content) { - xlinkArcrole: { - set: 'xlink:arcrole' - }, + var svg = '' + (content || '') + ''; + var xml = V.parseXML(svg, { async: false }); + return xml.documentElement; + }; - xlinkTitle: { - set: 'xlink:title' - }, + V.idCounter = 0; - xlinkActuate: { - set: 'xlink:actuate' - }, + // A function returning a unique identifier for this client session with every call. + V.uniqueId = function() { - xmlSpace: { - set: 'xml:space' - }, + return 'v-' + (++V.idCounter); + }; - xmlBase: { - set: 'xml:base' - }, + V.toNode = function(el) { - xmlLang: { - set: 'xml:lang' - }, + return V.isV(el) ? el.node : (el.nodeName && el || el[0]); + }; - preserveAspectRatio: { - set: 'preserveAspectRatio' - }, + V.ensureId = function(node) { - requiredExtension: { - set: 'requiredExtension' - }, + node = V.toNode(node); + return node.id || (node.id = V.uniqueId()); + }; - requiredFeatures: { - set: 'requiredFeatures' - }, + // 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. This is used in the text() method but it is + // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests + // when you want to compare the actual DOM text content without having to add the unicode character in + // the place of all spaces. + V.sanitizeText = function(text) { - systemLanguage: { - set: 'systemLanguage' - }, + return (text || '').replace(/ /g, '\u00A0'); + }; - externalResourcesRequired: { - set: 'externalResourceRequired' - }, + V.isUndefined = function(value) { - filter: { - qualify: util.isPlainObject, - set: function(filter) { - return 'url(#' + this.paper.defineFilter(filter) + ')'; - } - }, + return typeof value === 'undefined'; + }; - fill: { - qualify: util.isPlainObject, - set: function(fill) { - return 'url(#' + this.paper.defineGradient(fill) + ')'; - } - }, + V.isString = function(value) { - stroke: { - qualify: util.isPlainObject, - set: function(stroke) { - return 'url(#' + this.paper.defineGradient(stroke) + ')'; - } - }, + return typeof value === 'string'; + }; - sourceMarker: { - qualify: util.isPlainObject, - set: function(marker) { - return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + V.isObject = function(value) { - targetMarker: { - qualify: util.isPlainObject, - set: function(marker) { - marker = util.assign({ transform: 'rotate(180)' }, marker); - return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + return value && (typeof value === 'object'); + }; - vertexMarker: { - qualify: util.isPlainObject, - set: function(marker) { - return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + V.isArray = Array.isArray; - text: { - set: function(text, refBBox, node, attrs) { - var $node = $(node); - var cacheName = 'joint-text'; - var cache = $node.data(cacheName); - var textAttrs = joint.util.pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'eol'); - var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize']; - var textHash = JSON.stringify([text, textAttrs]); - // Update the text only if there was a change in the string - // or any of its attributes. - if (cache === undefined || cache !== textHash) { - // Chrome bug: - // Tspans positions defined as `em` are not updated - // when container `font-size` change. - if (fontSize) { - node.setAttribute('font-size', fontSize); - } - V(node).text('' + text, textAttrs); - $node.data(cacheName, textHash); - } - } - }, + V.parseXML = function(data, opt) { - textWrap: { - qualify: util.isPlainObject, - set: function(value, refBBox, node, attrs) { - // option `width` - var width = value.width || 0; - if (isPercentage(width)) { - refBBox.width *= parseFloat(width) / 100; - } else if (width <= 0) { - refBBox.width += width; - } else { - refBBox.width = width; - } - // option `height` - var height = value.height || 0; - if (isPercentage(height)) { - refBBox.height *= parseFloat(height) / 100; - } else if (height <= 0) { - refBBox.height += height; - } else { - refBBox.height = height; - } - // option `text` - var wrappedText = joint.util.breakText('' + value.text, refBBox, { - 'font-weight': attrs['font-weight'] || attrs.fontWeight, - 'font-size': attrs['font-size'] || attrs.fontSize, - 'font-family': attrs['font-family'] || attrs.fontFamily - }, { - // Provide an existing SVG Document here - // instead of creating a temporary one over again. - svgDocument: this.paper.svg - }); + opt = opt || {}; - V(node).text(wrappedText); - } - }, + var xml; - lineHeight: { - qualify: function(lineHeight, node, attrs) { - return (attrs.text !== undefined); - } - }, + try { + var parser = new DOMParser(); - textPath: { - qualify: function(textPath, node, attrs) { - return (attrs.text !== undefined); + if (!V.isUndefined(opt.async)) { + parser.async = opt.async; } - }, - annotations: { - qualify: function(annotations, node, attrs) { - return (attrs.text !== undefined); - } - }, + xml = parser.parseFromString(data, 'text/xml'); + } catch (error) { + xml = undefined; + } - // `port` attribute contains the `id` of the port that the underlying magnet represents. - port: { - set: function(port) { - return (port === null || port.id === undefined) ? port : port.id; - } - }, + if (!xml || xml.getElementsByTagName('parsererror').length) { + throw new Error('Invalid XML: ' + data); + } - // `style` attribute is special in the sense that it sets the CSS style of the subelement. - style: { - qualify: util.isPlainObject, - set: function(styles, refBBox, node) { - $(node).css(styles); - } - }, + return xml; + }; - html: { - set: function(html, refBBox, node) { - $(node).html(html + ''); - } - }, + /** + * @param {string} name + * @returns {{ns: string|null, local: string}} namespace and attribute name + */ + V.qualifyAttr = function(name) { - ref: { - // We do not set `ref` attribute directly on an element. - // The attribute itself does not qualify for relative positioning. - }, + if (name.indexOf(':') !== -1) { + var combinedKey = name.split(':'); + return { + ns: ns[combinedKey[0]], + local: combinedKey[1] + }; + } - // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width - // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box - // otherwise, `refX` is the left coordinate of the bounding box + return { + ns: null, + local: name + }; + }; - refX: { - position: positionWrapper('x', 'width', 'origin') - }, + V.transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi; + V.transformSeparatorRegex = /[ ,]+/; + V.transformationListRegex = /^(\w+)\((.*)\)/; - refY: { - position: positionWrapper('y', 'height', 'origin') - }, + V.transformStringToMatrix = function(transform) { - // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom - // coordinate of the reference element. + var transformationMatrix = V.createSVGMatrix(); + var matches = transform && transform.match(V.transformRegex); + if (!matches) { + return transformationMatrix; + } - refDx: { - position: positionWrapper('x', 'width', 'corner') - }, + for (var i = 0, n = matches.length; i < n; i++) { + var transformationString = matches[i]; - refDy: { - position: positionWrapper('y', 'height', 'corner') - }, + var transformationMatch = transformationString.match(V.transformationListRegex); + if (transformationMatch) { + var sx, sy, tx, ty, angle; + var ctm = V.createSVGMatrix(); + var args = transformationMatch[2].split(V.transformSeparatorRegex); + switch (transformationMatch[1].toLowerCase()) { + case 'scale': + sx = parseFloat(args[0]); + sy = (args[1] === undefined) ? sx : parseFloat(args[1]); + ctm = ctm.scaleNonUniform(sx, sy); + break; + case 'translate': + tx = parseFloat(args[0]); + ty = parseFloat(args[1]); + ctm = ctm.translate(tx, ty); + break; + case 'rotate': + angle = parseFloat(args[0]); + tx = parseFloat(args[1]) || 0; + ty = parseFloat(args[2]) || 0; + if (tx !== 0 || ty !== 0) { + ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty); + } else { + ctm = ctm.rotate(angle); + } + break; + case 'skewx': + angle = parseFloat(args[0]); + ctm = ctm.skewX(angle); + break; + case 'skewy': + angle = parseFloat(args[0]); + ctm = ctm.skewY(angle); + break; + case 'matrix': + ctm.a = parseFloat(args[0]); + ctm.b = parseFloat(args[1]); + ctm.c = parseFloat(args[2]); + ctm.d = parseFloat(args[3]); + ctm.e = parseFloat(args[4]); + ctm.f = parseFloat(args[5]); + break; + default: + continue; + } - // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to - // the reference element size - // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width - // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 + transformationMatrix = transformationMatrix.multiply(ctm); + } - refWidth: { - set: setWrapper('width', 'width') - }, + } + return transformationMatrix; + }; - refHeight: { - set: setWrapper('height', 'height') - }, + V.matrixToTransformString = function(matrix) { + matrix || (matrix = true); - refRx: { - set: setWrapper('rx', 'width') - }, + return 'matrix(' + + (matrix.a !== undefined ? matrix.a : 1) + ',' + + (matrix.b !== undefined ? matrix.b : 0) + ',' + + (matrix.c !== undefined ? matrix.c : 0) + ',' + + (matrix.d !== undefined ? matrix.d : 1) + ',' + + (matrix.e !== undefined ? matrix.e : 0) + ',' + + (matrix.f !== undefined ? matrix.f : 0) + + ')'; + }; - refRy: { - set: setWrapper('ry', 'height') - }, + V.parseTransformString = function(transform) { - refCx: { - set: setWrapper('cx', 'width') - }, + var translate, rotate, scale; - refCy: { - set: setWrapper('cy', 'height') - }, + if (transform) { - // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. - // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. + var separator = V.transformSeparatorRegex; - xAlignment: { - offset: offsetWrapper('x', 'width', 'right') - }, + // Allow reading transform string with a single matrix + if (transform.trim().indexOf('matrix') >= 0) { - // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. - // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. + var matrix = V.transformStringToMatrix(transform); + var decomposedMatrix = V.decomposeMatrix(matrix); - yAlignment: { - offset: offsetWrapper('y', 'height', 'bottom') - }, + translate = [decomposedMatrix.translateX, decomposedMatrix.translateY]; + scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]; + rotate = [decomposedMatrix.rotation]; - resetOffset: { - offset: function(val, nodeBBox) { - return (val) - ? { x: -nodeBBox.x, y: -nodeBBox.y } - : { x: 0, y: 0 }; - } + var transformations = []; + if (translate[0] !== 0 || translate[0] !== 0) { + transformations.push('translate(' + translate + ')'); + } + if (scale[0] !== 1 || scale[1] !== 1) { + transformations.push('scale(' + scale + ')'); + } + if (rotate[0] !== 0) { + transformations.push('rotate(' + rotate + ')'); + } + transform = transformations.join(' '); + + } else { + var translateMatch = transform.match(/translate\((.*?)\)/); + if (translateMatch) { + translate = translateMatch[1].split(separator); + } + var rotateMatch = transform.match(/rotate\((.*?)\)/); + if (rotateMatch) { + rotate = rotateMatch[1].split(separator); + } + var scaleMatch = transform.match(/scale\((.*?)\)/); + if (scaleMatch) { + scale = scaleMatch[1].split(separator); + } + } } + + var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; + + return { + value: transform, + translate: { + tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, + ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 + }, + rotate: { + angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, + cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, + cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined + }, + scale: { + sx: sx, + sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx + } + }; }; - // This allows to combine both absolute and relative positioning - // refX: 50%, refX2: 20 - attributesNS.refX2 = attributesNS.refX; - attributesNS.refY2 = attributesNS.refY; + V.deltaTransformPoint = function(matrix, point) { - // Aliases for backwards compatibility - attributesNS['ref-x'] = attributesNS.refX; - attributesNS['ref-y'] = attributesNS.refY; - attributesNS['ref-dy'] = attributesNS.refDy; - attributesNS['ref-dx'] = attributesNS.refDx; - attributesNS['ref-width'] = attributesNS.refWidth; - attributesNS['ref-height'] = attributesNS.refHeight; - attributesNS['x-alignment'] = attributesNS.xAlignment; - attributesNS['y-alignment'] = attributesNS.yAlignment; + var dx = point.x * matrix.a + point.y * matrix.c + 0; + var dy = point.x * matrix.b + point.y * matrix.d + 0; + return { x: dx, y: dy }; + }; -})(joint, _, g, $, joint.util); + V.decomposeMatrix = function(matrix) { + // @see https://gist.github.com/2052247 -// joint.dia.Cell base model. -// -------------------------- + // calculate delta transform point + var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); + var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); -joint.dia.Cell = Backbone.Model.extend({ + // calculate skew + var skewX = ((180 / PI) * atan2(px.y, px.x) - 90); + var skewY = ((180 / PI) * atan2(py.y, py.x)); - // This is the same as Backbone.Model with the only difference that is uses joint.util.merge - // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. - constructor: function(attributes, options) { + return { - var defaults; - var attrs = attributes || {}; - this.cid = joint.util.uniqueId('c'); - this.attributes = {}; - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attrs = this.parse(attrs, options) || {}; - if ((defaults = joint.util.result(this, 'defaults'))) { - // - // Replaced the call to _.defaults with joint.util.merge. - attrs = joint.util.merge({}, defaults, attrs); - // + translateX: matrix.e, + translateY: matrix.f, + scaleX: sqrt(matrix.a * matrix.a + matrix.b * matrix.b), + scaleY: sqrt(matrix.c * matrix.c + matrix.d * matrix.d), + skewX: skewX, + skewY: skewY, + rotation: skewX // rotation is the same as skew x + }; + }; + + // Return the `scale` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToScale = function(matrix) { + + var a,b,c,d; + if (matrix) { + a = V.isUndefined(matrix.a) ? 1 : matrix.a; + d = V.isUndefined(matrix.d) ? 1 : matrix.d; + b = matrix.b; + c = matrix.c; + } else { + a = d = 1; } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); + return { + sx: b ? sqrt(a * a + b * b) : a, + sy: c ? sqrt(c * c + d * d) : d + }; }, - translate: function(dx, dy, opt) { + // Return the `rotate` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToRotate = function(matrix) { - throw new Error('Must define a translate() method.'); - }, + var p = { x: 0, y: 1 }; + if (matrix) { + p = V.deltaTransformPoint(matrix, p); + } - toJSON: function() { + return { + angle: g.normalizeAngle(g.toDeg(atan2(p.y, p.x)) - 90) + }; + }, - var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; - var attrs = this.attributes.attrs; - var finalAttrs = {}; + // Return the `translate` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToTranslate = function(matrix) { - // Loop through all the attributes and - // omit the default attributes as they are implicitly reconstructable by the cell 'type'. - joint.util.forIn(attrs, function(attr, selector) { + return { + tx: (matrix && matrix.e) || 0, + ty: (matrix && matrix.f) || 0 + }; + }, - var defaultAttr = defaultAttrs[selector]; + V.isV = function(object) { - joint.util.forIn(attr, function(value, name) { + return object instanceof V; + }; - // attr is mainly flat though it might have one more level (consider the `style` attribute). - // Check if the `value` is object and if yes, go one level deep. - if (joint.util.isObject(value) && !Array.isArray(value)) { - - joint.util.forIn(value, function(value2, name2) { - - if (!defaultAttr || !defaultAttr[name] || !joint.util.isEqual(defaultAttr[name][name2], value2)) { + // For backwards compatibility: + V.isVElement = V.isV; - finalAttrs[selector] = finalAttrs[selector] || {}; - (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; - } - }); + var svgDocument = V('svg').node; - } else if (!defaultAttr || !joint.util.isEqual(defaultAttr[name], value)) { - // `value` is not an object, default attribute for such a selector does not exist - // or it is different than the attribute value set on the model. + V.createSVGMatrix = function(matrix) { - finalAttrs[selector] = finalAttrs[selector] || {}; - finalAttrs[selector][name] = value; - } - }); - }); + var svgMatrix = svgDocument.createSVGMatrix(); + for (var component in matrix) { + svgMatrix[component] = matrix[component]; + } - var attributes = joint.util.cloneDeep(joint.util.omit(this.attributes, 'attrs')); - //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); - attributes.attrs = finalAttrs; + return svgMatrix; + }; - return attributes; - }, + V.createSVGTransform = function(matrix) { - initialize: function(options) { + if (!V.isUndefined(matrix)) { - if (!options || !options.id) { + if (!(matrix instanceof SVGMatrix)) { + matrix = V.createSVGMatrix(matrix); + } - this.set('id', joint.util.uuid(), { silent: true }); + return svgDocument.createSVGTransformFromMatrix(matrix); } - this._transitionIds = {}; - - // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. - this.processPorts(); - this.on('change:attrs', this.processPorts, this); - }, + return svgDocument.createSVGTransform(); + }; - /** - * @deprecated - */ - processPorts: function() { + V.createSVGPoint = function(x, y) { - // Whenever `attrs` changes, we extract ports from the `attrs` object and store it - // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` - // set to that port, we remove those links as well (to follow the same behaviour as - // with a removed element). + var p = svgDocument.createSVGPoint(); + p.x = x; + p.y = y; + return p; + }; - var previousPorts = this.ports; + V.transformRect = function(r, matrix) { - // Collect ports from the `attrs` object. - var ports = {}; - joint.util.forIn(this.get('attrs'), function(attrs, selector) { + var p = svgDocument.createSVGPoint(); - if (attrs && attrs.port) { + p.x = r.x; + p.y = r.y; + var corner1 = p.matrixTransform(matrix); - // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). - if (attrs.port.id !== undefined) { - ports[attrs.port.id] = attrs.port; - } else { - ports[attrs.port] = { id: attrs.port }; - } - } - }); + p.x = r.x + r.width; + p.y = r.y; + var corner2 = p.matrixTransform(matrix); - // Collect ports that have been removed (compared to the previous ports) - if any. - // Use hash table for quick lookup. - var removedPorts = {}; - joint.util.forIn(previousPorts, function(port, id) { + p.x = r.x + r.width; + p.y = r.y + r.height; + var corner3 = p.matrixTransform(matrix); - if (!ports[id]) removedPorts[id] = true; - }); + p.x = r.x; + p.y = r.y + r.height; + var corner4 = p.matrixTransform(matrix); - // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. - if (this.graph && !joint.util.isEmpty(removedPorts)) { + var minX = min(corner1.x, corner2.x, corner3.x, corner4.x); + var maxX = max(corner1.x, corner2.x, corner3.x, corner4.x); + var minY = min(corner1.y, corner2.y, corner3.y, corner4.y); + var maxY = max(corner1.y, corner2.y, corner3.y, corner4.y); - var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); - inboundLinks.forEach(function(link) { + return new g.Rect(minX, minY, maxX - minX, maxY - minY); + }; - if (removedPorts[link.get('target').port]) link.remove(); - }); + V.transformPoint = function(p, matrix) { - var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); - outboundLinks.forEach(function(link) { + return new g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); + }; - if (removedPorts[link.get('source').port]) link.remove(); - }); - } + V.transformLine = function(l, matrix) { - // Update the `ports` object. - this.ports = ports; - }, + return new g.Line( + V.transformPoint(l.start, matrix), + V.transformPoint(l.end, matrix) + ); + }; - remove: function(opt) { + V.transformPolyline = function(p, matrix) { - opt = opt || {}; + var inPoints = (p instanceof g.Polyline) ? p.points : p; + if (!V.isArray(inPoints)) inPoints = []; + var outPoints = []; + for (var i = 0, n = inPoints.length; i < n; i++) outPoints[i] = V.transformPoint(inPoints[i], matrix); + return new g.Polyline(outPoints); + }, - // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. - var graph = this.graph; - if (graph) { - graph.startBatch('remove'); + // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to + // an object (`{ fill: 'blue', stroke: 'red' }`). + V.styleToObject = function(styleString) { + var ret = {}; + var styles = styleString.split(';'); + for (var i = 0; i < styles.length; i++) { + var style = styles[i]; + var pair = style.split('='); + ret[pair[0].trim()] = pair[1].trim(); } + return ret; + }; - // First, unembed this cell from its parent cell if there is one. - var parentCellId = this.get('parent'); - if (parentCellId) { + // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js + V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { - var parentCell = graph && graph.getCell(parentCellId); - parentCell.unembed(this); - } + var svgArcMax = 2 * PI - 1e-6; + var r0 = innerRadius; + var r1 = outerRadius; + var a0 = startAngle; + var a1 = endAngle; + var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); + var df = da < PI ? '0' : '1'; + var c0 = cos(a0); + var s0 = sin(a0); + var c1 = cos(a1); + var s1 = sin(a1); - joint.util.invoke(this.getEmbeddedCells(), 'remove', opt); + return (da >= svgArcMax) + ? (r0 + ? 'M0,' + r1 + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + + 'M0,' + r0 + + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) + + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 + + 'Z' + : 'M0,' + r1 + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + + 'Z') + : (r0 + ? 'M' + r1 * c0 + ',' + r1 * s0 + + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + + 'L' + r0 * c1 + ',' + r0 * s1 + + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 + + 'Z' + : 'M' + r1 * c0 + ',' + r1 * s0 + + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + + 'L0,0' + + 'Z'); + }; - this.trigger('remove', this, this.collection, opt); + // Merge attributes from object `b` with attributes in object `a`. + // Note that this modifies the object `a`. + // Also important to note that attributes are merged but CSS classes are concatenated. + V.mergeAttrs = function(a, b) { - if (graph) { - graph.stopBatch('remove'); + for (var attr in b) { + + if (attr === 'class') { + // Concatenate classes. + a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; + } else if (attr === 'style') { + // `style` attribute can be an object. + if (V.isObject(a[attr]) && V.isObject(b[attr])) { + // `style` stored in `a` is an object. + a[attr] = V.mergeAttrs(a[attr], b[attr]); + } else if (V.isObject(a[attr])) { + // `style` in `a` is an object but it's a string in `b`. + // Convert the style represented as a string to an object in `b`. + a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); + } else if (V.isObject(b[attr])) { + // `style` in `a` is a string, in `b` it's an object. + a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); + } else { + // Both styles are strings. + a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); + } + } else { + a[attr] = b[attr]; + } } - return this; - }, + return a; + }; - toFront: function(opt) { + V.annotateString = function(t, annotations, opt) { - if (this.graph) { + annotations = annotations || []; + opt = opt || {}; - opt = opt || {}; + var offset = opt.offset || 0; + var compacted = []; + var batch; + var ret = []; + var item; + var prev; - var z = (this.graph.getLastCell().get('z') || 0) + 1; + for (var i = 0; i < t.length; i++) { - this.startBatch('to-front').set('z', z, opt); + item = ret[i] = t[i]; - if (opt.deep) { + for (var j = 0; j < annotations.length; j++) { - var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - cells.forEach(function(cell) { cell.set('z', ++z, opt); }); + var annotation = annotations[j]; + var start = annotation.start + offset; + var end = annotation.end + offset; + if (i >= start && i < end) { + // Annotation applies. + if (V.isObject(item)) { + // There is more than one annotation to be applied => Merge attributes. + item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); + } else { + item = ret[i] = { t: t[i], attrs: annotation.attrs }; + } + if (opt.includeAnnotationIndices) { + (item.annotations || (item.annotations = [])).push(j); + } + } } - this.stopBatch('to-front'); - } - - return this; - }, - - toBack: function(opt) { - - if (this.graph) { + prev = ret[i - 1]; - opt = opt || {}; + if (!prev) { - var z = (this.graph.getFirstCell().get('z') || 0) - 1; + batch = item; - this.startBatch('to-back'); + } else if (V.isObject(item) && V.isObject(prev)) { + // Both previous item and the current one are annotations. If the attributes + // didn't change, merge the text. + if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { + batch.t += item.t; + } else { + compacted.push(batch); + batch = item; + } - if (opt.deep) { + } else if (V.isObject(item)) { + // Previous item was a string, current item is an annotation. + compacted.push(batch); + batch = item; + + } else if (V.isObject(prev)) { + // Previous item was an annotation, current item is a string. + compacted.push(batch); + batch = item; - var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - cells.reverse().forEach(function(cell) { cell.set('z', z--, opt); }); + } else { + // Both previous and current item are strings. + batch = (batch || '') + item; } + } - this.set('z', z, opt).stopBatch('to-back'); + if (batch) { + compacted.push(batch); } - return this; - }, + return compacted; + }; - embed: function(cell, opt) { + V.findAnnotationsAtIndex = function(annotations, index) { - if (this === cell || this.isEmbeddedIn(cell)) { + var found = []; - throw new Error('Recursive embedding not allowed.'); + if (annotations) { - } else { + annotations.forEach(function(annotation) { - this.startBatch('embed'); + if (annotation.start < index && index <= annotation.end) { + found.push(annotation); + } + }); + } - var embeds = joint.util.assign([], this.get('embeds')); + return found; + }; - // We keep all element ids after link ids. - embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); + V.findAnnotationsBetweenIndexes = function(annotations, start, end) { - cell.set('parent', this.id, opt); - this.set('embeds', joint.util.uniq(embeds), opt); + var found = []; - this.stopBatch('embed'); + if (annotations) { + + annotations.forEach(function(annotation) { + + if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { + found.push(annotation); + } + }); } - return this; - }, + return found; + }; - unembed: function(cell, opt) { + // Shift all the text annotations after character `index` by `offset` positions. + V.shiftAnnotations = function(annotations, index, offset) { - this.startBatch('unembed'); + if (annotations) { - cell.unset('parent', opt); - this.set('embeds', joint.util.without(this.get('embeds'), cell.id), opt); + annotations.forEach(function(annotation) { - this.stopBatch('unembed'); + if (annotation.start < index && annotation.end >= index) { + annotation.end += offset; + } else if (annotation.start >= index) { + annotation.start += offset; + annotation.end += offset; + } + }); + } - return this; - }, + return annotations; + }; - // Return an array of ancestor cells. - // The array is ordered from the parent of the cell - // to the most distant ancestor. - getAncestors: function() { + V.convertLineToPathData = function(line) { - var ancestors = []; - var parentId = this.get('parent'); + line = V(line); + var d = [ + 'M', line.attr('x1'), line.attr('y1'), + 'L', line.attr('x2'), line.attr('y2') + ].join(' '); + return d; + }; - if (!this.graph) { - return ancestors; - } + V.convertPolygonToPathData = function(polygon) { - while (parentId !== undefined) { - var parent = this.graph.getCell(parentId); - if (parent !== undefined) { - ancestors.push(parent); - parentId = parent.get('parent'); - } else { - break; - } - } + var points = V.getPointsFromSvgNode(polygon); + if (points.length === 0) return null; - return ancestors; - }, + return V.svgPointsToPath(points) + ' Z'; + }; - getEmbeddedCells: function(opt) { + V.convertPolylineToPathData = function(polyline) { - opt = opt || {}; + var points = V.getPointsFromSvgNode(polyline); + if (points.length === 0) return null; - // Cell models can only be retrieved when this element is part of a collection. - // There is no way this element knows about other cells otherwise. - // This also means that calling e.g. `translate()` on an element with embeds before - // adding it to a graph does not translate its embeds. - if (this.graph) { + return V.svgPointsToPath(points); + }; - var cells; + V.svgPointsToPath = function(points) { - if (opt.deep) { + for (var i = 0, n = points.length; i < n; i++) { + points[i] = points[i].x + ' ' + points[i].y; + } - if (opt.breadthFirst) { + return 'M ' + points.join(' L'); + }; - // breadthFirst algorithm - cells = []; - var queue = this.getEmbeddedCells(); + V.getPointsFromSvgNode = function(node) { - while (queue.length > 0) { + node = V.toNode(node); + var points = []; + var nodePoints = node.points; + if (nodePoints) { + for (var i = 0, n = nodePoints.numberOfItems; i < n; i++) { + points.push(nodePoints.getItem(i)); + } + } - var parent = queue.shift(); - cells.push(parent); - queue.push.apply(queue, parent.getEmbeddedCells()); - } + return points; + }; - } else { + V.KAPPA = 0.551784; - // depthFirst algorithm - cells = this.getEmbeddedCells(); - cells.forEach(function(cell) { - cells.push.apply(cells, cell.getEmbeddedCells(opt)); - }); - } + V.convertCircleToPathData = function(circle) { - } else { + circle = V(circle); + var cx = parseFloat(circle.attr('cx')) || 0; + var cy = parseFloat(circle.attr('cy')) || 0; + var r = parseFloat(circle.attr('r')); + var cd = r * V.KAPPA; // Control distance. - cells = joint.util.toArray(this.get('embeds')).map(this.graph.getCell, this.graph); - } + var d = [ + 'M', cx, cy - r, // Move to the first point. + 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. + 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. + 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. + 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. + 'Z' + ].join(' '); + return d; + }; - return cells; - } - return []; - }, + V.convertEllipseToPathData = function(ellipse) { - isEmbeddedIn: function(cell, opt) { + ellipse = V(ellipse); + var cx = parseFloat(ellipse.attr('cx')) || 0; + var cy = parseFloat(ellipse.attr('cy')) || 0; + var rx = parseFloat(ellipse.attr('rx')); + var ry = parseFloat(ellipse.attr('ry')) || rx; + var cdx = rx * V.KAPPA; // Control distance x. + var cdy = ry * V.KAPPA; // Control distance y. - var cellId = joint.util.isString(cell) ? cell : cell.id; - var parentId = this.get('parent'); + var d = [ + 'M', cx, cy - ry, // Move to the first point. + 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. + 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. + 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. + 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. + 'Z' + ].join(' '); + return d; + }; - opt = joint.util.defaults({ deep: true }, opt); + V.convertRectToPathData = function(rect) { - // See getEmbeddedCells(). - if (this.graph && opt.deep) { + rect = V(rect); - while (parentId) { - if (parentId === cellId) { - return true; - } - parentId = this.graph.getCell(parentId).get('parent'); - } + return V.rectToPath({ + x: parseFloat(rect.attr('x')) || 0, + y: parseFloat(rect.attr('y')) || 0, + width: parseFloat(rect.attr('width')) || 0, + height: parseFloat(rect.attr('height')) || 0, + rx: parseFloat(rect.attr('rx')) || 0, + ry: parseFloat(rect.attr('ry')) || 0 + }); + }; - return false; + // Convert a rectangle to SVG path commands. `r` is an object of the form: + // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, + // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for + // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle + // that has only `rx` and `ry` attributes). + V.rectToPath = function(r) { - } else { + var d; + var x = r.x; + var y = r.y; + var width = r.width; + var height = r.height; + var topRx = min(r.rx || r['top-rx'] || 0, width / 2); + var bottomRx = min(r.rx || r['bottom-rx'] || 0, width / 2); + var topRy = min(r.ry || r['top-ry'] || 0, height / 2); + var bottomRy = min(r.ry || r['bottom-ry'] || 0, height / 2); - // When this cell is not part of a collection check - // at least whether it's a direct child of given cell. - return parentId === cellId; + if (topRx || bottomRx || topRy || bottomRy) { + d = [ + 'M', x, y + topRy, + 'v', height - topRy - bottomRy, + 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, + 'h', width - 2 * bottomRx, + 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, + 'v', -(height - bottomRy - topRy), + 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, + 'h', -(width - 2 * topRx), + 'a', topRx, topRy, 0, 0, 0, -topRx, topRy, + 'Z' + ]; + } else { + d = [ + 'M', x, y, + 'H', x + width, + 'V', y + height, + 'H', x, + 'V', y, + 'Z' + ]; } - }, - // Whether or not the cell is embedded in any other cell. - isEmbedded: function() { + return d.join(' '); + }; - return !!this.get('parent'); - }, + // Take a path data string + // Return a normalized path data string + // If data cannot be parsed, return 'M 0 0' + // Adapted from Rappid normalizePath polyfill + // Highly inspired by Raphael Library (www.raphael.com) + V.normalizePathData = (function() { - // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). - // Shallow cloning simply clones the cell and returns a new cell with different ID. - // Deep cloning clones the cell and all its embedded cells recursively. - clone: function(opt) { + var spaces = '\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029'; + var pathCommand = new RegExp('([a-z])[' + spaces + ',]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?[' + spaces + ']*,?[' + spaces + ']*)+)', 'ig'); + var pathValues = new RegExp('(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)[' + spaces + ']*,?[' + spaces + ']*', 'ig'); - opt = opt || {}; + var math = Math; + var PI = math.PI; + var sin = math.sin; + var cos = math.cos; + var tan = math.tan; + var asin = math.asin; + var sqrt = math.sqrt; + var abs = math.abs; - if (!opt.deep) { - // Shallow cloning. - - var clone = Backbone.Model.prototype.clone.apply(this, arguments); - // We don't want the clone to have the same ID as the original. - clone.set('id', joint.util.uuid()); - // A shallow cloned element does not carry over the original embeds. - clone.unset('embeds'); - // And can not be embedded in any cell - // as the clone is not part of the graph. - clone.unset('parent'); + function q2c(x1, y1, ax, ay, x2, y2) { - return clone; + var _13 = 1 / 3; + var _23 = 2 / 3; + return [(_13 * x1) + (_23 * ax), (_13 * y1) + (_23 * ay), (_13 * x2) + (_23 * ax), (_13 * y2) + (_23 * ay), x2, y2]; + } - } else { - // Deep cloning. + function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { + // for more information of where this math came from visit: + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. - return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); - } - }, + var _120 = (PI * 120) / 180; + var rad = (PI / 180) * (+angle || 0); + var res = []; + var xy; - // A convenient way to set nested properties. - // This method merges the properties you'd like to set with the ones - // stored in the cell and makes sure change events are properly triggered. - // You can either set a nested property with one object - // or use a property path. - // The most simple use case is: - // `cell.prop('name/first', 'John')` or - // `cell.prop({ name: { first: 'John' } })`. - // Nested arrays are supported too: - // `cell.prop('series/0/data/0/degree', 50)` or - // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. - prop: function(props, value, opt) { + var rotate = function(x, y, rad) { - var delim = '/'; - var isString = joint.util.isString(props); + var X = (x * cos(rad)) - (y * sin(rad)); + var Y = (x * sin(rad)) + (y * cos(rad)); + return { x: X, y: Y }; + }; - if (isString || Array.isArray(props)) { - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. + if (!recursive) { + xy = rotate(x1, y1, -rad); + x1 = xy.x; + y1 = xy.y; - if (arguments.length > 1) { + xy = rotate(x2, y2, -rad); + x2 = xy.x; + y2 = xy.y; - var path; - var pathArray; + var x = (x1 - x2) / 2; + var y = (y1 - y2) / 2; + var h = ((x * x) / (rx * rx)) + ((y * y) / (ry * ry)); - if (isString) { - path = props; - pathArray = path.split('/') - } else { - path = props.join(delim); - pathArray = props.slice(); + if (h > 1) { + h = sqrt(h); + rx = h * rx; + ry = h * ry; } - var property = pathArray[0]; - var pathArrayLength = pathArray.length; - - opt = opt || {}; - opt.propertyPath = path; - opt.propertyValue = value; - opt.propertyPathArray = pathArray; + var rx2 = rx * rx; + var ry2 = ry * ry; - if (pathArrayLength === 1) { - // Property is not nested. We can simply use `set()`. - return this.set(property, value, opt); - } + var k = ((large_arc_flag == sweep_flag) ? -1 : 1) * sqrt(abs(((rx2 * ry2) - (rx2 * y * y) - (ry2 * x * x)) / ((rx2 * y * y) + (ry2 * x * x)))); - var update = {}; - // Initialize the nested object. Subobjects are either arrays or objects. - // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. - // Note that this imposes a limitation on object keys one can use with Inspector. - // Pure integer keys will cause issues and are therefore not allowed. - var initializer = update; - var prevProperty = property; + var cx = ((k * rx * y) / ry) + ((x1 + x2) / 2); + var cy = ((k * -ry * x) / rx) + ((y1 + y2) / 2); - for (var i = 1; i < pathArrayLength; i++) { - var pathItem = pathArray[i]; - var isArrayIndex = Number.isFinite(isString ? Number(pathItem) : pathItem); - initializer = initializer[prevProperty] = isArrayIndex ? [] : {}; - prevProperty = pathItem; - } + var f1 = asin(((y1 - cy) / ry).toFixed(9)); + var f2 = asin(((y2 - cy) / ry).toFixed(9)); - // Fill update with the `value` on `path`. - update = joint.util.setByPath(update, pathArray, value, '/'); + f1 = ((x1 < cx) ? (PI - f1) : f1); + f2 = ((x2 < cx) ? (PI - f2) : f2); - var baseAttributes = joint.util.merge({}, this.attributes); - // if rewrite mode enabled, we replace value referenced by path with - // the new one (we don't merge). - opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); + if (f1 < 0) f1 = (PI * 2) + f1; + if (f2 < 0) f2 = (PI * 2) + f2; - // Merge update with the model attributes. - var attributes = joint.util.merge(baseAttributes, update); - // Finally, set the property to the updated attributes. - return this.set(property, attributes[property], opt); + if ((sweep_flag && f1) > f2) f1 = f1 - (PI * 2); + if ((!sweep_flag && f2) > f1) f2 = f2 - (PI * 2); } else { + f1 = recursive[0]; + f2 = recursive[1]; + cx = recursive[2]; + cy = recursive[3]; + } - return joint.util.getByPath(this.attributes, props, delim); + var df = f2 - f1; + + if (abs(df) > _120) { + var f2old = f2; + var x2old = x2; + var y2old = y2; + + f2 = f1 + (_120 * (((sweep_flag && f2) > f1) ? 1 : -1)); + x2 = cx + (rx * cos(f2)); + y2 = cy + (ry * sin(f2)); + + res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); } - } - return this.set(joint.util.merge({}, this.attributes, props), value); - }, + df = f2 - f1; - // A convient way to unset nested properties - removeProp: function(path, opt) { + var c1 = cos(f1); + var s1 = sin(f1); + var c2 = cos(f2); + var s2 = sin(f2); - // Once a property is removed from the `attrs` attribute - // the cellView will recognize a `dirty` flag and rerender itself - // in order to remove the attribute from SVG element. - opt = opt || {}; - opt.dirty = true; + var t = tan(df / 4); - var pathArray = Array.isArray(path) ? path : path.split('/'); + var hx = (4 / 3) * (rx * t); + var hy = (4 / 3) * (ry * t); - if (pathArray.length === 1) { - // A top level property - return this.unset(path, opt); - } + var m1 = [x1, y1]; + var m2 = [x1 + (hx * s1), y1 - (hy * c1)]; + var m3 = [x2 + (hx * s2), y2 - (hy * c2)]; + var m4 = [x2, y2]; - // A nested property - var property = pathArray[0]; - var nestedPath = pathArray.slice(1); - var propertyValue = joint.util.cloneDeep(this.get(property)); + m2[0] = (2 * m1[0]) - m2[0]; + m2[1] = (2 * m1[1]) - m2[1]; - joint.util.unsetByPath(propertyValue, nestedPath, '/'); + if (recursive) { + return [m2, m3, m4].concat(res); - return this.set(property, propertyValue, opt); - }, + } else { + res = [m2, m3, m4].concat(res).join().split(','); - // A convenient way to set nested attributes. - attr: function(attrs, value, opt) { + var newres = []; + var ii = res.length; + for (var i = 0; i < ii; i++) { + newres[i] = (i % 2) ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; + } - var args = Array.from(arguments); - if (args.length === 0) { - return this.get('attrs'); + return newres; + } } - if (Array.isArray(attrs)) { - args[0] = ['attrs'].concat(attrs); - } else if (joint.util.isString(attrs)) { - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. - args[0] = 'attrs/' + attrs; + function parsePathString(pathString) { - } else { + if (!pathString) return null; - args[0] = { 'attrs' : attrs }; - } + var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 }; + var data = []; - return this.prop.apply(this, args); - }, + String(pathString).replace(pathCommand, function(a, b, c) { - // A convenient way to unset nested attributes - removeAttr: function(path, opt) { + var params = []; + var name = b.toLowerCase(); + c.replace(pathValues, function(a, b) { + if (b) params.push(+b); + }); - if (Array.isArray(path)) { + if ((name === 'm') && (params.length > 2)) { + data.push([b].concat(params.splice(0, 2))); + name = 'l'; + b = ((b === 'm') ? 'l' : 'L'); + } - return this.removeProp(['attrs'].concat(path)); + while (params.length >= paramCounts[name]) { + data.push([b].concat(params.splice(0, paramCounts[name]))); + if (!paramCounts[name]) break; + } + }); + + return data; } - return this.removeProp('attrs/' + path, opt); - }, + function pathToAbsolute(pathArray) { - transition: function(path, value, opt, delim) { + if (!Array.isArray(pathArray) || !Array.isArray(pathArray && pathArray[0])) { // rough assumption + pathArray = parsePathString(pathArray); + } - delim = delim || '/'; + // if invalid string, return 'M 0 0' + if (!pathArray || !pathArray.length) return [['M', 0, 0]]; - var defaults = { - duration: 100, - delay: 10, - timingFunction: joint.util.timing.linear, - valueFunction: joint.util.interpolate.number - }; + var res = []; + var x = 0; + var y = 0; + var mx = 0; + var my = 0; + var start = 0; + var pa0; - opt = joint.util.assign(defaults, opt); + var ii = pathArray.length; + for (var i = start; i < ii; i++) { - var firstFrameTime = 0; - var interpolatingFunction; + var r = []; + res.push(r); - var setter = function(runtime) { + var pa = pathArray[i]; + pa0 = pa[0]; - var id, progress, propertyValue; + if (pa0 != pa0.toUpperCase()) { + r[0] = pa0.toUpperCase(); - firstFrameTime = firstFrameTime || runtime; - runtime -= firstFrameTime; - progress = runtime / opt.duration; + var jj; + var j; + switch (r[0]) { + case 'A': + r[1] = pa[1]; + r[2] = pa[2]; + r[3] = pa[3]; + r[4] = pa[4]; + r[5] = pa[5]; + r[6] = +pa[6] + x; + r[7] = +pa[7] + y; + break; - if (progress < 1) { - this._transitionIds[path] = id = joint.util.nextFrame(setter); - } else { - progress = 1; - delete this._transitionIds[path]; - } + case 'V': + r[1] = +pa[1] + y; + break; - propertyValue = interpolatingFunction(opt.timingFunction(progress)); + case 'H': + r[1] = +pa[1] + x; + break; - opt.transitionId = id; + case 'M': + mx = +pa[1] + x; + my = +pa[2] + y; - this.prop(path, propertyValue, opt); + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; - if (!id) this.trigger('transition:end', this, path); + default: + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + } + } else { + var kk = pa.length; + for (var k = 0; k < kk; k++) { + r[k] = pa[k]; + } + } - }.bind(this); + switch (r[0]) { + case 'Z': + x = +mx; + y = +my; + break; - var initiator = function(callback) { + case 'H': + x = r[1]; + break; - this.stopTransitions(path); + case 'V': + y = r[1]; + break; - interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); + case 'M': + mx = r[r.length - 2]; + my = r[r.length - 1]; + x = r[r.length - 2]; + y = r[r.length - 1]; + break; - this._transitionIds[path] = joint.util.nextFrame(callback); + default: + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + } + } - this.trigger('transition:start', this, path); + return res; + } - }.bind(this); + function normalize(path) { - return setTimeout(initiator, opt.delay, setter); - }, + var p = pathToAbsolute(path); + var attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null }; - getTransitions: function() { - return Object.keys(this._transitionIds); - }, + function processPath(path, d, pcom) { - stopTransitions: function(path, delim) { + var nx, ny; - delim = delim || '/'; + if (!path) return ['C', d.x, d.y, d.x, d.y, d.x, d.y]; - var pathArray = path && path.split(delim); + if (!(path[0] in { T: 1, Q: 1 })) { + d.qx = null; + d.qy = null; + } - Object.keys(this._transitionIds).filter(pathArray && function(key) { + switch (path[0]) { + case 'M': + d.X = path[1]; + d.Y = path[2]; + break; - return joint.util.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); + case 'A': + path = ['C'].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1)))); + break; - }).forEach(function(key) { + case 'S': + if (pcom === 'C' || pcom === 'S') { // In 'S' case we have to take into account, if the previous command is C/S. + nx = (d.x * 2) - d.bx; // And reflect the previous + ny = (d.y * 2) - d.by; // command's control point relative to the current point. + } + else { // or some else or nothing + nx = d.x; + ny = d.y; + } + path = ['C', nx, ny].concat(path.slice(1)); + break; - joint.util.cancelFrame(this._transitionIds[key]); + case 'T': + if (pcom === 'Q' || pcom === 'T') { // In 'T' case we have to take into account, if the previous command is Q/T. + d.qx = (d.x * 2) - d.qx; // And make a reflection similar + d.qy = (d.y * 2) - d.qy; // to case 'S'. + } + else { // or something else or nothing + d.qx = d.x; + d.qy = d.y; + } + path = ['C'].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); + break; - delete this._transitionIds[key]; + case 'Q': + d.qx = path[1]; + d.qy = path[2]; + path = ['C'].concat(q2c(d.x, d.y, path[1], path[2], path[3], path[4])); + break; - this.trigger('transition:end', this, key); + case 'H': + path = ['L'].concat(path[1], d.y); + break; - }, this); + case 'V': + path = ['L'].concat(d.x, path[1]); + break; - return this; - }, + // leave 'L' & 'Z' commands as they were: - // A shorcut making it easy to create constructs like the following: - // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. - addTo: function(graph, opt) { + case 'L': + break; - graph.addCell(this, opt); - return this; - }, + case 'Z': + break; + } - // A shortcut for an equivalent call: `paper.findViewByModel(cell)` - // making it easy to create constructs like the following: - // `cell.findView(paper).highlight()` - findView: function(paper) { + return path; + } - return paper.findViewByModel(this); - }, + function fixArc(pp, i) { - isElement: function() { + if (pp[i].length > 7) { - return false; - }, + pp[i].shift(); + var pi = pp[i]; - isLink: function() { + while (pi.length) { + pcoms[i] = 'A'; // if created multiple 'C's, their original seg is saved + pp.splice(i++, 0, ['C'].concat(pi.splice(0, 6))); + } - return false; - }, + pp.splice(i, 1); + ii = p.length; + } + } - startBatch: function(name, opt) { - if (this.graph) { this.graph.startBatch(name, joint.util.assign({}, opt, { cell: this })); } - return this; - }, + var pcoms = []; // path commands of original path p + var pfirst = ''; // temporary holder for original path command + var pcom = ''; // holder for previous path command of original path - stopBatch: function(name, opt) { - if (this.graph) { this.graph.stopBatch(name, joint.util.assign({}, opt, { cell: this })); } - return this; - } + var ii = p.length; + for (var i = 0; i < ii; i++) { + if (p[i]) pfirst = p[i][0]; // save current path command -}, { + if (pfirst !== 'C') { // C is not saved yet, because it may be result of conversion + pcoms[i] = pfirst; // Save current path command + if (i > 0) pcom = pcoms[i - 1]; // Get previous path command pcom + } - getAttributeDefinition: function(attrName) { - - var defNS = this.attributes; - var globalDefNS = joint.dia.attributes; - return (defNS && defNS[attrName]) || globalDefNS[attrName]; - }, + p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath - define: function(type, defaults, protoProps, staticProps) { + if (pcoms[i] !== 'A' && pfirst === 'C') pcoms[i] = 'C'; // 'A' is the only command + // which may produce multiple 'C's + // so we have to make sure that 'C' is also 'C' in original path - protoProps = joint.util.assign({ - defaults: joint.util.defaultsDeep({ type: type }, defaults, this.prototype.defaults) - }, protoProps); + fixArc(p, i); // fixArc adds also the right amount of 'A's to pcoms - var Cell = this.extend(protoProps, staticProps); - joint.util.setByPath(joint.shapes, type, Cell, '.'); - return Cell; - } -}); + var seg = p[i]; + var seglen = seg.length; -// joint.dia.CellView base view and controller. -// -------------------------------------------- + attrs.x = seg[seglen - 2]; + attrs.y = seg[seglen - 1]; -// This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. + attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x; + attrs.by = parseFloat(seg[seglen - 3]) || attrs.y; + } -joint.dia.CellView = joint.mvc.View.extend({ + // make sure normalized path data string starts with an M segment + if (!p[0][0] || p[0][0] !== 'M') { + p.unshift(['M', 0, 0]); + } - tagName: 'g', + return p; + } - svgElement: true, + return function(pathData) { + return normalize(pathData).join(',').split(',').join(' '); + }; + })(); - className: function() { + V.namespace = ns; - var classNames = ['cell']; - var type = this.model.get('type'); + return V; - if (type) { +})(); - type.toLowerCase().split('.').forEach(function(value, index, list) { - classNames.push('type-' + list.slice(0, index + 1).join('-')); - }); - } +// Global namespace. - return classNames.join(' '); - }, +var joint = { - attributes: function() { + version: '2.1.0', - return { 'model-id': this.model.id }; + config: { + // The class name prefix config is for advanced use only. + // Be aware that if you change the prefix, the JointJS CSS will no longer function properly. + classNamePrefix: 'joint-', + defaultTheme: 'default' }, - constructor: function(options) { + // `joint.dia` namespace. + dia: {}, - // Make sure a global unique id is assigned to this view. Store this id also to the properties object. - // The global unique id makes sure that the same view can be rendered on e.g. different machines and - // still be associated to the same object among all those clients. This is necessary for real-time - // collaboration mechanism. - options.id = options.id || joint.util.guid(this); + // `joint.ui` namespace. + ui: {}, - joint.mvc.View.call(this, options); - }, + // `joint.layout` namespace. + layout: {}, - init: function() { + // `joint.shapes` namespace. + shapes: {}, - joint.util.bindAll(this, 'remove', 'update'); + // `joint.format` namespace. + format: {}, - // Store reference to this to the DOM element so that the view is accessible through the DOM tree. - this.$el.data('view', this); + // `joint.connectors` namespace. + connectors: {}, - // Add the cell's type to the view's element as a data attribute. - this.$el.attr('data-type', this.model.get('type')); + // `joint.highlighters` namespace. + highlighters: {}, - this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); - }, + // `joint.routers` namespace. + routers: {}, - onChangeAttrs: function(cell, attrs, opt) { + // `joint.anchors` namespace. + anchors: {}, - if (opt.dirty) { + // `joint.connectionPoints` namespace. + connectionPoints: {}, - // dirty flag could be set when a model attribute was removed and it needs to be cleared - // also from the DOM element. See cell.removeAttr(). - return this.render(); - } + // `joint.connectionStrategies` namespace. + connectionStrategies: {}, - return this.update(cell, attrs, opt); + // `joint.linkTools` namespace. + linkTools: {}, + + // `joint.mvc` namespace. + mvc: { + views: {} }, - // Return `true` if cell link is allowed to perform a certain UI `feature`. - // Example: `can('vertexMove')`, `can('labelMove')`. - can: function(feature) { + setTheme: function(theme, opt) { - var interactive = joint.util.isFunction(this.options.interactive) - ? this.options.interactive(this) - : this.options.interactive; + opt = opt || {}; - return (joint.util.isObject(interactive) && interactive[feature] !== false) || - (joint.util.isBoolean(interactive) && interactive !== false); + joint.util.invoke(joint.mvc.views, 'setTheme', theme, opt); + + // Update the default theme on the view prototype. + joint.mvc.View.prototype.defaultTheme = theme; }, - findBySelector: function(selector, root) { + // `joint.env` namespace. + env: { - var $root = $(root || this.el); - // These are either descendants of `this.$el` of `this.$el` itself. - // `.` is a special selector used to select the wrapping `` element. - return (selector === '.') ? $root : $root.find(selector); - }, + _results: {}, - notify: function(eventName) { + _tests: { - if (this.paper) { + svgforeignobject: function() { + return !!document.createElementNS && + /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); + } + }, - var args = Array.prototype.slice.call(arguments, 1); + addTest: function(name, fn) { - // Trigger the event on both the element itself and also on the paper. - this.trigger.apply(this, [eventName].concat(args)); + return joint.env._tests[name] = fn; + }, - // Paper event handlers receive the view object as the first argument. - this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); - } - }, + test: function(name) { - getStrokeBBox: function(el) { - // Return a bounding box rectangle that takes into account stroke. - // Note that this is a naive and ad-hoc implementation that does not - // works only in certain cases and should be replaced as soon as browsers will - // start supporting the getStrokeBBox() SVG method. - // @TODO any better solution is very welcome! + var fn = joint.env._tests[name]; - var isMagnet = !!el; + if (!fn) { + throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); + } - el = el || this.el; - var bbox = V(el).getBBox({ target: this.paper.viewport }); + var result = joint.env._results[name]; - var strokeWidth; - if (isMagnet) { + if (typeof result !== 'undefined') { + return result; + } - strokeWidth = V(el).attr('stroke-width'); + try { + result = fn(); + } catch (error) { + result = false; + } - } else { + // Cache the test result. + joint.env._results[name] = result; - strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); + return result; } + }, - strokeWidth = parseFloat(strokeWidth) || 0; + util: { - return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); - }, + // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. + hashCode: function(str) { - getBBox: function() { + var hash = 0; + if (str.length == 0) return hash; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + hash = ((hash << 5) - hash) + c; + hash = hash & hash; // Convert to 32bit integer + } + return hash; + }, - return this.vel.getBBox({ target: this.paper.svg }); - }, + getByPath: function(obj, path, delim) { - highlight: function(el, opt) { + var keys = Array.isArray(path) ? path.slice() : path.split(delim || '/'); + var key; - el = !el ? this.el : this.$(el)[0] || this.el; + while (keys.length) { + key = keys.shift(); + if (Object(obj) === obj && key in obj) { + obj = obj[key]; + } else { + return undefined; + } + } + return obj; + }, - // set partial flag if the highlighted element is not the entire view. - opt = opt || {}; - opt.partial = (el !== this.el); + setByPath: function(obj, path, value, delim) { - this.notify('cell:highlight', el, opt); - return this; - }, + var keys = Array.isArray(path) ? path : path.split(delim || '/'); - unhighlight: function(el, opt) { + var diver = obj; + var i = 0; - el = !el ? this.el : this.$(el)[0] || this.el; + for (var len = keys.length; i < len - 1; i++) { + // diver creates an empty object if there is no nested object under such a key. + // This means that one can populate an empty nested object with setByPath(). + diver = diver[keys[i]] || (diver[keys[i]] = {}); + } + diver[keys[len - 1]] = value; - opt = opt || {}; - opt.partial = el != this.el; + return obj; + }, - this.notify('cell:unhighlight', el, opt); - return this; - }, + unsetByPath: function(obj, path, delim) { - // Find the closest element that has the `magnet` attribute set to `true`. If there was not such - // an element found, return the root element of the cell view. - findMagnet: function(el) { + delim = delim || '/'; - var $el = this.$(el); - var $rootEl = this.$el; + var pathArray = Array.isArray(path) ? path.slice() : path.split(delim); - if ($el.length === 0) { - $el = $rootEl; - } + var propertyToRemove = pathArray.pop(); + if (pathArray.length > 0) { - do { + // unsetting a nested attribute + var parent = joint.util.getByPath(obj, pathArray, delim); - var magnet = $el.attr('magnet'); - if ((magnet || $el.is($rootEl)) && magnet !== 'false') { - return $el[0]; - } + if (parent) { + delete parent[propertyToRemove]; + } - $el = $el.parent(); + } else { - } while ($el.length > 0); + // unsetting a primitive attribute + delete obj[propertyToRemove]; + } - // If the overall cell has set `magnet === false`, then return `undefined` to - // announce there is no magnet found for this cell. - // This is especially useful to set on cells that have 'ports'. In this case, - // only the ports have set `magnet === true` and the overall element has `magnet === false`. - return undefined; - }, + return obj; + }, - // Construct a unique selector for the `el` element within this view. - // `prevSelector` is being collected through the recursive call. - // No value for `prevSelector` is expected when using this method. - getSelector: function(el, prevSelector) { - - if (el === this.el) { - return prevSelector; - } - - var selector; + flattenObject: function(obj, delim, stop) { - if (el) { + delim = delim || '/'; + var ret = {}; - var nthChild = V(el).index() + 1; - selector = el.tagName + ':nth-child(' + nthChild + ')'; + for (var key in obj) { - if (prevSelector) { - selector += ' > ' + prevSelector; - } + if (!obj.hasOwnProperty(key)) continue; - selector = this.getSelector(el.parentNode, selector); - } + var shouldGoDeeper = typeof obj[key] === 'object'; + if (shouldGoDeeper && stop && stop(obj[key])) { + shouldGoDeeper = false; + } - return selector; - }, + if (shouldGoDeeper) { - getAttributeDefinition: function(attrName) { + var flatObject = this.flattenObject(obj[key], delim, stop); - return this.model.constructor.getAttributeDefinition(attrName); - }, + for (var flatKey in flatObject) { + if (!flatObject.hasOwnProperty(flatKey)) continue; + ret[key + delim + flatKey] = flatObject[flatKey]; + } - setNodeAttributes: function(node, attrs) { + } else { - if (!joint.util.isEmpty(attrs)) { - if (node instanceof SVGElement) { - V(node).attr(attrs); - } else { - $(node).attr(attrs); + ret[key] = obj[key]; + } } - } - }, - processNodeAttributes: function(node, attrs) { + return ret; + }, - var attrName, attrVal, def, i, n; - var normalAttrs, setAttrs, positionAttrs, offsetAttrs; - var relatives = []; - // divide the attributes between normal and special - for (attrName in attrs) { - if (!attrs.hasOwnProperty(attrName)) continue; - attrVal = attrs[attrName]; - def = this.getAttributeDefinition(attrName); - if (def && (!joint.util.isFunction(def.qualify) || def.qualify.call(this, attrVal, node, attrs))) { - if (joint.util.isString(def.set)) { - normalAttrs || (normalAttrs = {}); - normalAttrs[def.set] = attrVal; - } - if (attrVal !== null) { - relatives.push(attrName, def); - } - } else { - normalAttrs || (normalAttrs = {}); - normalAttrs[joint.util.toKebabCase(attrName)] = attrVal; - } - } + uuid: function() { - // handle the rest of attributes via related method - // from the special attributes namespace. - for (i = 0, n = relatives.length; i < n; i+=2) { - attrName = relatives[i]; - def = relatives[i+1]; - attrVal = attrs[attrName]; - if (joint.util.isFunction(def.set)) { - setAttrs || (setAttrs = {}); - setAttrs[attrName] = attrVal; - } - if (joint.util.isFunction(def.position)) { - positionAttrs || (positionAttrs = {}); - positionAttrs[attrName] = attrVal; - } - if (joint.util.isFunction(def.offset)) { - offsetAttrs || (offsetAttrs = {}); - offsetAttrs[attrName] = attrVal; - } - } + // credit: http://stackoverflow.com/posts/2117523/revisions - return { - raw: attrs, - normal: normalAttrs, - set: setAttrs, - position: positionAttrs, - offset: offsetAttrs - }; - }, + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16|0; + var v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, - updateRelativeAttributes: function(node, attrs, refBBox, opt) { + // Generate global unique id for obj and store it as a property of the object. + guid: function(obj) { - opt || (opt = {}); + this.guid.id = this.guid.id || 1; + obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); + return obj.id; + }, - var attrName, attrVal, def; - var rawAttrs = attrs.raw || {}; - var nodeAttrs = attrs.normal || {}; - var setAttrs = attrs.set; - var positionAttrs = attrs.position; - var offsetAttrs = attrs.offset; + toKebabCase: function(string) { - for (attrName in setAttrs) { - attrVal = setAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // SET - set function should return attributes to be set on the node, - // which will affect the node dimensions based on the reference bounding - // box. e.g. `width`, `height`, `d`, `rx`, `ry`, `points - var setResult = def.set.call(this, attrVal, refBBox.clone(), node, rawAttrs); - if (joint.util.isObject(setResult)) { - joint.util.assign(nodeAttrs, setResult); - } else if (setResult !== undefined) { - nodeAttrs[attrName] = setResult; - } - } + return string.replace(/[A-Z]/g, '-$&').toLowerCase(); + }, - if (node instanceof HTMLElement) { - // TODO: setting the `transform` attribute on HTMLElements - // via `node.style.transform = 'matrix(...)';` would introduce - // a breaking change (e.g. basic.TextBlock). - this.setNodeAttributes(node, nodeAttrs); - return; - } + // Copy all the properties to the first argument from the following arguments. + // All the properties will be overwritten by the properties from the following + // arguments. Inherited properties are ignored. + mixin: _.assign, - // The final translation of the subelement. - var nodeTransform = nodeAttrs.transform; - var nodeMatrix = V.transformStringToMatrix(nodeTransform); - var nodePosition = g.Point(nodeMatrix.e, nodeMatrix.f); - if (nodeTransform) { - nodeAttrs = joint.util.omit(nodeAttrs, 'transform'); - nodeMatrix.e = nodeMatrix.f = 0; - } + // Copy all properties to the first argument from the following + // arguments only in case if they don't exists in the first argument. + // All the function propererties in the first argument will get + // additional property base pointing to the extenders same named + // property function's call method. + supplement: _.defaults, - // Calculate node scale determined by the scalable group - // only if later needed. - var sx, sy, translation; - if (positionAttrs || offsetAttrs) { - var nodeScale = this.getNodeScale(node, opt.scalableNode); - sx = nodeScale.sx; - sy = nodeScale.sy; - } + // Same as `mixin()` but deep version. + deepMixin: _.mixin, - var positioned = false; - for (attrName in positionAttrs) { - attrVal = positionAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // POSITION - position function should return a point from the - // reference bounding box. The default position of the node is x:0, y:0 of - // the reference bounding box or could be further specify by some - // SVG attributes e.g. `x`, `y` - translation = def.position.call(this, attrVal, refBBox.clone(), node, rawAttrs); - if (translation) { - nodePosition.offset(g.Point(translation).scale(sx, sy)); - positioned || (positioned = true); - } - } + // Same as `supplement()` but deep version. + deepSupplement: _.defaultsDeep, - // The node bounding box could depend on the `size` set from the previous loop. - // Here we know, that all the size attributes have been already set. - this.setNodeAttributes(node, nodeAttrs); + normalizeEvent: function(evt) { - var offseted = false; - if (offsetAttrs) { - // Check if the node is visible - var nodeClientRect = node.getBoundingClientRect(); - if (nodeClientRect.width > 0 && nodeClientRect.height > 0) { - var nodeBBox = V.transformRect(node.getBBox(), nodeMatrix).scale(1 / sx, 1 / sy); - for (attrName in offsetAttrs) { - attrVal = offsetAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // OFFSET - offset function should return a point from the element - // bounding box. The default offset point is x:0, y:0 (origin) or could be further - // specify with some SVG attributes e.g. `text-anchor`, `cx`, `cy` - translation = def.offset.call(this, attrVal, nodeBBox, node, rawAttrs); - if (translation) { - nodePosition.offset(g.Point(translation).scale(sx, sy)); - offseted || (offseted = true); + var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; + if (touchEvt) { + for (var property in evt) { + // copy all the properties from the input event that are not + // defined on the touch event (functions included). + if (touchEvt[property] === undefined) { + touchEvt[property] = evt[property]; } } + return touchEvt; } - } - // Do not touch node's transform attribute if there is no transformation applied. - if (nodeTransform !== undefined || positioned || offseted) { - // Round the coordinates to 1 decimal point. - nodePosition.round(1); - nodeMatrix.e = nodePosition.x; - nodeMatrix.f = nodePosition.y; - node.setAttribute('transform', V.matrixToTransformString(nodeMatrix)); - } - }, + return evt; + }, - getNodeScale: function(node, scalableNode) { + nextFrame: (function() { - // Check if the node is a descendant of the scalable group. - var sx, sy; - if (scalableNode && scalableNode.contains(node)) { - var scale = scalableNode.scale(); - sx = 1 / scale.sx; - sy = 1 / scale.sy; - } else { - sx = 1; - sy = 1; - } + var raf; - return { sx: sx, sy: sy }; - }, + if (typeof window !== 'undefined') { - findNodesAttributes: function(attrs, root, selectorCache) { + raf = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame; + } - // TODO: merge attributes in order defined by `index` property + if (!raf) { - var nodesAttrs = {}; + var lastTime = 0; - for (var selector in attrs) { - if (!attrs.hasOwnProperty(selector)) continue; - var $selected = selectorCache[selector] = this.findBySelector(selector, root); + raf = function(callback) { - for (var i = 0, n = $selected.length; i < n; i++) { - var node = $selected[i]; - var nodeId = V.ensureId(node); - var nodeAttrs = attrs[selector]; - var prevNodeAttrs = nodesAttrs[nodeId]; - if (prevNodeAttrs) { - if (!prevNodeAttrs.merged) { - prevNodeAttrs.merged = true; - prevNodeAttrs.attributes = joint.util.cloneDeep(prevNodeAttrs.attributes); - } - joint.util.merge(prevNodeAttrs.attributes, nodeAttrs); - } else { - nodesAttrs[nodeId] = { - attributes: nodeAttrs, - node: node, - merged: false - }; - } + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + + lastTime = currTime + timeToCall; + + return id; + }; } - } - return nodesAttrs; - }, + return function(callback, context) { + return context + ? raf(callback.bind(context)) + : raf(callback); + }; - // Default is to process the `model.attributes.attrs` object and set attributes on subelements based on the selectors, - // unless `attrs` parameter was passed. - updateDOMSubtreeAttributes: function(rootNode, attrs, opt) { + })(), - opt || (opt = {}); - opt.rootBBox || (opt.rootBBox = g.Rect()); + cancelFrame: (function() { - // Cache table for query results and bounding box calculation. - // Note that `selectorCache` needs to be invalidated for all - // `updateAttributes` calls, as the selectors might pointing - // to nodes designated by an attribute or elements dynamically - // created. - var selectorCache = {}; - var bboxCache = {}; - var relativeItems = []; - var item, node, nodeAttrs, nodeData, processedAttrs; + var caf; + var client = typeof window != 'undefined'; - var roAttrs = opt.roAttributes; - var nodesAttrs = this.findNodesAttributes(roAttrs || attrs, rootNode, selectorCache); - // `nodesAttrs` are different from all attributes, when - // rendering only attributes sent to this method. - var nodesAllAttrs = (roAttrs) - ? nodesAllAttrs = this.findNodesAttributes(attrs, rootNode, selectorCache) - : nodesAttrs; + if (client) { - for (var nodeId in nodesAttrs) { - nodeData = nodesAttrs[nodeId]; - nodeAttrs = nodeData.attributes; - node = nodeData.node; - processedAttrs = this.processNodeAttributes(node, nodeAttrs); + caf = window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.webkitCancelRequestAnimationFrame || + window.msCancelAnimationFrame || + window.msCancelRequestAnimationFrame || + window.oCancelAnimationFrame || + window.oCancelRequestAnimationFrame || + window.mozCancelAnimationFrame || + window.mozCancelRequestAnimationFrame; + } - if (!processedAttrs.set && !processedAttrs.position && !processedAttrs.offset) { - // Set all the normal attributes right on the SVG/HTML element. - this.setNodeAttributes(node, processedAttrs.normal); + caf = caf || clearTimeout; - } else { + return client ? caf.bind(window) : caf; - var nodeAllAttrs = nodesAllAttrs[nodeId] && nodesAllAttrs[nodeId].attributes; - var refSelector = (nodeAllAttrs && (nodeAttrs.ref === undefined)) - ? nodeAllAttrs.ref - : nodeAttrs.ref; + })(), - var refNode; - if (refSelector) { - refNode = (selectorCache[refSelector] || this.findBySelector(refSelector, rootNode))[0]; - if (!refNode) { - throw new Error('dia.ElementView: "' + refSelector + '" reference does not exists.'); - } - } else { - refNode = null; - } + // ** Deprecated ** + shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { - item = { - node: node, - refNode: refNode, - processedAttributes: processedAttrs, - allAttributes: nodeAllAttrs - }; + var bbox; + var spot; - // If an element in the list is positioned relative to this one, then - // we want to insert this one before it in the list. - var itemIndex = relativeItems.findIndex(function(item) { - return item.refNode === node; - }); + if (!magnet) { - if (itemIndex > -1) { - relativeItems.splice(itemIndex, 0, item); - } else { - relativeItems.push(item); - } - } - } + // There is no magnet, try to make the best guess what is the + // wrapping SVG element. This is because we want this "smart" + // connection points to work out of the box without the + // programmer to put magnet marks to any of the subelements. + // For example, we want the functoin to work on basic.Path elements + // without any special treatment of such elements. + // The code below guesses the wrapping element based on + // one simple assumption. The wrapping elemnet is the + // first child of the scalable group if such a group exists + // or the first child of the rotatable group if not. + // This makese sense because usually the wrapping element + // is below any other sub element in the shapes. + var scalable = view.$('.scalable')[0]; + var rotatable = view.$('.rotatable')[0]; - for (var i = 0, n = relativeItems.length; i < n; i++) { - item = relativeItems[i]; - node = item.node; - refNode = item.refNode; + if (scalable && scalable.firstChild) { - // Find the reference element bounding box. If no reference was provided, we - // use the optional bounding box. - var refNodeId = refNode ? V.ensureId(refNode) : ''; - var refBBox = bboxCache[refNodeId]; - if (!refBBox) { - // Get the bounding box of the reference element relative to the `rotatable` `` (without rotation) - // or to the root `` element if no rotatable group present if reference node present. - // Uses the bounding box provided. - refBBox = bboxCache[refNodeId] = (refNode) - ? V(refNode).getBBox({ target: (opt.rotatableNode || rootNode) }) - : opt.rootBBox; + magnet = scalable.firstChild; + + } else if (rotatable && rotatable.firstChild) { + + magnet = rotatable.firstChild; + } } - if (roAttrs) { - // if there was a special attribute affecting the position amongst passed-in attributes - // we have to merge it with the rest of the element's attributes as they are necessary - // to update the position relatively (i.e `ref-x` && 'ref-dx') - processedAttrs = this.processNodeAttributes(node, item.allAttributes); - this.mergeProcessedAttributes(processedAttrs, item.processedAttributes); + if (magnet) { + + spot = V(magnet).findIntersection(reference, linkView.paper.viewport); + if (!spot) { + bbox = V(magnet).getBBox({ target: linkView.paper.viewport }); + } } else { - processedAttrs = item.processedAttributes; + + bbox = view.model.getBBox(); + spot = bbox.intersectionWithLineFromCenterToPoint(reference); } + return spot || bbox.center(); + }, - this.updateRelativeAttributes(node, processedAttrs, refBBox, opt); - } - }, + isPercentage: function(val) { - mergeProcessedAttributes: function(processedAttrs, roProcessedAttrs) { + return joint.util.isString(val) && val.slice(-1) === '%'; + }, - processedAttrs.set || (processedAttrs.set = {}); - processedAttrs.position || (processedAttrs.position = {}); - processedAttrs.offset || (processedAttrs.offset = {}); + parseCssNumeric: function(strValue, restrictUnits) { - joint.util.assign(processedAttrs.set, roProcessedAttrs.set); - joint.util.assign(processedAttrs.position, roProcessedAttrs.position); - joint.util.assign(processedAttrs.offset, roProcessedAttrs.offset); + restrictUnits = restrictUnits || []; + var cssNumeric = { value: parseFloat(strValue) }; - // Handle also the special transform property. - var transform = processedAttrs.normal && processedAttrs.normal.transform; - if (transform !== undefined && roProcessedAttrs.normal) { - roProcessedAttrs.normal.transform = transform; - } - processedAttrs.normal = roProcessedAttrs.normal; - }, + if (Number.isNaN(cssNumeric.value)) { + return null; + } - // Interaction. The controller part. - // --------------------------------- + var validUnitsExp = restrictUnits.join('|'); - // Interaction is handled by the paper and delegated to the view in interest. - // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. - // If necessary, real coordinates can be obtained from the `evt` event object. + if (joint.util.isString(strValue)) { + var matches = new RegExp('(\\d+)(' + validUnitsExp + ')$').exec(strValue); + if (!matches) { + return null; + } + if (matches[2]) { + cssNumeric.unit = matches[2]; + } + } + return cssNumeric; + }, - // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, - // i.e. `joint.dia.Element` and `joint.dia.Link`. + breakText: function(text, size, styles, opt) { - pointerdblclick: function(evt, x, y) { + opt = opt || {}; + styles = styles || {}; - this.notify('cell:pointerdblclick', evt, x, y); - }, + var width = size.width; + var height = size.height; - pointerclick: function(evt, x, y) { + var svgDocument = opt.svgDocument || V('svg').node; + var textSpan = V('tspan').node; + var textElement = V('text').attr(styles).append(textSpan).node; + var textNode = document.createTextNode(''); - this.notify('cell:pointerclick', evt, x, y); - }, + // Prevent flickering + textElement.style.opacity = 0; + // Prevent FF from throwing an uncaught exception when `getBBox()` + // called on element that is not in the render tree (is not measurable). + // .getComputedTextLength() returns always 0 in this case. + // Note that the `textElement` resp. `textSpan` can become hidden + // when it's appended to the DOM and a `display: none` CSS stylesheet + // rule gets applied. + textElement.style.display = 'block'; + textSpan.style.display = 'block'; - pointerdown: function(evt, x, y) { + textSpan.appendChild(textNode); + svgDocument.appendChild(textElement); - if (this.model.graph) { - this.model.startBatch('pointer'); - this._graph = this.model.graph; - } + if (!opt.svgDocument) { - this.notify('cell:pointerdown', evt, x, y); - }, + document.body.appendChild(svgDocument); + } - pointermove: function(evt, x, y) { + var separator = opt.separator || ' '; + var eol = opt.eol || '\n'; - this.notify('cell:pointermove', evt, x, y); - }, + var words = text.split(separator); + var full = []; + var lines = []; + var p; + var lineHeight; - pointerup: function(evt, x, y) { + for (var i = 0, l = 0, len = words.length; i < len; i++) { - this.notify('cell:pointerup', evt, x, y); + var word = words[i]; - if (this._graph) { - // we don't want to trigger event on model as model doesn't - // need to be member of collection anymore (remove) - this._graph.stopBatch('pointer', { cell: this.model }); - delete this._graph; - } - }, + if (!word) continue; - mouseover: function(evt) { + if (eol && word.indexOf(eol) >= 0) { + // word cotains end-of-line character + if (word.length > 1) { + // separate word and continue cycle + var eolWords = word.split(eol); + for (var j = 0, jl = eolWords.length - 1; j < jl; j++) { + eolWords.splice(2 * j + 1, 0, eol); + } + Array.prototype.splice.apply(words, [i, 1].concat(eolWords)); + i--; + len += eolWords.length - 1; + } else { + // creates new line + l++; + } + continue; + } - this.notify('cell:mouseover', evt); - }, - mouseout: function(evt) { + textNode.data = lines[l] ? lines[l] + ' ' + word : word; - this.notify('cell:mouseout', evt); - }, + if (textSpan.getComputedTextLength() <= width) { - mouseenter: function(evt) { + // the current line fits + lines[l] = textNode.data; - this.notify('cell:mouseenter', evt); - }, + if (p) { + // We were partitioning. Put rest of the word onto next line + full[l++] = true; - mouseleave: function(evt) { + // cancel partitioning + p = 0; + } - this.notify('cell:mouseleave', evt); - }, + } else { - mousewheel: function(evt, x, y, delta) { + if (!lines[l] || p) { - this.notify('cell:mousewheel', evt, x, y, delta); - }, + var partition = !!p; - contextmenu: function(evt, x, y) { + p = word.length - 1; - this.notify('cell:contextmenu', evt, x, y); - }, + if (partition || !p) { - event: function(evt, eventName, x, y) { + // word has only one character. + if (!p) { - this.notify(eventName, evt, x, y); - }, + if (!lines[l]) { - setInteractivity: function(value) { + // we won't fit this text within our rect + lines = []; - this.options.interactive = value; - } -}); + break; + } -// joint.dia.Element base model. -// ----------------------------- + // partitioning didn't help on the non-empty line + // try again, but this time start with a new line -joint.dia.Element = joint.dia.Cell.extend({ + // cancel partitions created + words.splice(i, 2, word + words[i + 1]); - defaults: { - position: { x: 0, y: 0 }, - size: { width: 1, height: 1 }, - angle: 0 - }, + // adjust word length + len--; - initialize: function() { + full[l++] = true; + i--; - this._initializePorts(); - joint.dia.Cell.prototype.initialize.apply(this, arguments); - }, + continue; + } - /** - * @abstract - */ - _initializePorts: function() { - // implemented in ports.js - }, + // move last letter to the beginning of the next word + words[i] = word.substring(0, p); + words[i + 1] = word.substring(p) + words[i + 1]; - isElement: function() { + } else { - return true; - }, + // We initiate partitioning + // split the long word into two words + words.splice(i, 1, word.substring(0, p), word.substring(p)); - position: function(x, y, opt) { + // adjust words length + len++; - var isSetter = joint.util.isNumber(y); + if (l && !full[l - 1]) { + // if the previous line is not full, try to fit max part of + // the current word there + l--; + } + } - opt = (isSetter ? opt : x) || {}; + i--; - // option `parentRelative` for setting the position relative to the element's parent. - if (opt.parentRelative) { + continue; + } - // Getting the parent's position requires the collection. - // Cell.get('parent') helds cell id only. - if (!this.graph) throw new Error('Element must be part of a graph.'); + l++; + i--; + } - var parent = this.graph.getCell(this.get('parent')); - var parentPosition = parent && !parent.isLink() - ? parent.get('position') - : { x: 0, y: 0 }; - } + // if size.height is defined we have to check whether the height of the entire + // text exceeds the rect height + if (height !== undefined) { - if (isSetter) { + if (lineHeight === undefined) { - if (opt.parentRelative) { - x += parentPosition.x; - y += parentPosition.y; - } + var heightValue; - if (opt.deep) { - var currentPosition = this.get('position'); - this.translate(x - currentPosition.x, y - currentPosition.y, opt); - } else { - this.set('position', { x: x, y: y }, opt); - } + // use the same defaults as in V.prototype.text + if (styles.lineHeight === 'auto') { + heightValue = { value: 1.5, unit: 'em' }; + } else { + heightValue = joint.util.parseCssNumeric(styles.lineHeight, ['em']) || { value: 1, unit: 'em' }; + } - return this; + lineHeight = heightValue.value; + if (heightValue.unit === 'em' ) { + lineHeight *= textElement.getBBox().height; + } + } - } else { // Getter returns a geometry point. + if (lineHeight * lines.length > height) { - var elementPosition = g.point(this.get('position')); + // remove overflowing lines + lines.splice(Math.floor(height / lineHeight)); - return opt.parentRelative - ? elementPosition.difference(parentPosition) - : elementPosition; - } - }, + break; + } + } + } - translate: function(tx, ty, opt) { + if (opt.svgDocument) { - tx = tx || 0; - ty = ty || 0; + // svg document was provided, remove the text element only + svgDocument.removeChild(textElement); - if (tx === 0 && ty === 0) { - // Like nothing has happened. - return this; - } + } else { - opt = opt || {}; - // Pass the initiator of the translation. - opt.translateBy = opt.translateBy || this.id; + // clean svg document + document.body.removeChild(svgDocument); + } - var position = this.get('position') || { x: 0, y: 0 }; + return lines.join(eol); + }, - if (opt.restrictedArea && opt.translateBy === this.id) { + // Sanitize HTML + // Based on https://gist.github.com/ufologist/5a0da51b2b9ef1b861c30254172ac3c9 + // Parses a string into an array of DOM nodes. + // Then outputs it back as a string. + sanitizeHTML: function(html) { - // We are restricting the translation for the element itself only. We get - // the bounding box of the element including all its embeds. - // All embeds have to be translated the exact same way as the element. - var bbox = this.getBBox({ deep: true }); - var ra = opt.restrictedArea; - //- - - - - - - - - - - - -> ra.x + ra.width - // - - - -> position.x | - // -> bbox.x - // ▓▓▓▓▓▓▓ | - // ░░░░░░░▓▓▓▓▓▓▓ - // ░░░░░░░░░ | - // ▓▓▓▓▓▓▓▓░░░░░░░ - // ▓▓▓▓▓▓▓▓ | - // <-dx-> | restricted area right border - // <-width-> | ░ translated element - // <- - bbox.width - -> ▓ embedded element - var dx = position.x - bbox.x; - var dy = position.y - bbox.y; - // Find the maximal/minimal coordinates that the element can be translated - // while complies the restrictions. - var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); - var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); - // recalculate the translation taking the resctrictions into account. - tx = x - position.x; - ty = y - position.y; - } + // Ignores tags that are invalid inside a
tag (e.g. , ) - var translatedPosition = { - x: position.x + tx, - y: position.y + ty - }; + // If documentContext (second parameter) is not specified or given as `null` or `undefined`, a new document is used. + // Inline events will not execute when the HTML is parsed; this includes, for example, sending GET requests for images. - // To find out by how much an element was translated in event 'change:position' handlers. - opt.tx = tx; - opt.ty = ty; + // If keepScripts (last parameter) is `false`, scripts are not executed. + var output = $($.parseHTML('
' + html + '
', null, false)); - if (opt.transition) { + output.find('*').each(function() { // for all nodes + var currentNode = this; - if (!joint.util.isObject(opt.transition)) opt.transition = {}; + $.each(currentNode.attributes, function() { // for all attributes in each node + var currentAttribute = this; - this.transition('position', translatedPosition, joint.util.assign({}, opt.transition, { - valueFunction: joint.util.interpolate.object - })); + var attrName = currentAttribute.name; + var attrValue = currentAttribute.value; - } else { + // Remove attribute names that start with "on" (e.g. onload, onerror...). + // Remove attribute values that start with "javascript:" pseudo protocol (e.g. `href="javascript:alert(1)"`). + if (attrName.indexOf('on') === 0 || attrValue.indexOf('javascript:') === 0) { + $(currentNode).removeAttr(attrName); + } + }); + }); - this.set('position', translatedPosition, opt); - } + return output.html(); + }, - // Recursively call `translate()` on all the embeds cells. - joint.util.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); + // Download `blob` as file with `fileName`. + // Does not work in IE9. + downloadBlob: function(blob, fileName) { - return this; - }, + if (window.navigator.msSaveBlob) { // requires IE 10+ + // pulls up a save dialog + window.navigator.msSaveBlob(blob, fileName); - size: function(width, height, opt) { + } else { // other browsers + // downloads directly in Chrome and Safari - var currentSize = this.get('size'); - // Getter - // () signature - if (width === undefined) { - return { - width: currentSize.width, - height: currentSize.height - }; - } - // Setter - // (size, opt) signature - if (joint.util.isObject(width)) { - opt = height; - height = joint.util.isNumber(width.height) ? width.height : currentSize.height; - width = joint.util.isNumber(width.width) ? width.width : currentSize.width; - } + // presents a save/open dialog in Firefox + // Firefox bug: `from` field in save dialog always shows `from:blob:` + // https://bugzilla.mozilla.org/show_bug.cgi?id=1053327 - return this.resize(width, height, opt); - }, + var url = window.URL.createObjectURL(blob); + var link = document.createElement('a'); - resize: function(width, height, opt) { + link.href = url; + link.download = fileName; + document.body.appendChild(link); - opt = opt || {}; + link.click(); - this.startBatch('resize', opt); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); // mark the url for garbage collection + } + }, - if (opt.direction) { + // Download `dataUri` as file with `fileName`. + // Does not work in IE9. + downloadDataUri: function(dataUri, fileName) { - var currentSize = this.get('size'); - - switch (opt.direction) { - - case 'left': - case 'right': - // Don't change height when resizing horizontally. - height = currentSize.height; - break; + var blob = joint.util.dataUriToBlob(dataUri); + joint.util.downloadBlob(blob, fileName); + }, - case 'top': - case 'bottom': - // Don't change width when resizing vertically. - width = currentSize.width; - break; - } + // Convert an uri-encoded data component (possibly also base64-encoded) to a blob. + dataUriToBlob: function(dataUri) { - // Get the angle and clamp its value between 0 and 360 degrees. - var angle = g.normalizeAngle(this.get('angle') || 0); + // first, make sure there are no newlines in the data uri + dataUri = dataUri.replace(/\s/g, ''); + dataUri = decodeURIComponent(dataUri); - var quadrant = { - 'top-right': 0, - 'right': 0, - 'top-left': 1, - 'top': 1, - 'bottom-left': 2, - 'left': 2, - 'bottom-right': 3, - 'bottom': 3 - }[opt.direction]; + var firstCommaIndex = dataUri.indexOf(','); // split dataUri as `dataTypeString`,`data` - if (opt.absolute) { + var dataTypeString = dataUri.slice(0, firstCommaIndex); // e.g. 'data:image/jpeg;base64' + var mimeString = dataTypeString.split(':')[1].split(';')[0]; // e.g. 'image/jpeg' - // We are taking the element's rotation into account - quadrant += Math.floor((angle + 45) / 90); - quadrant %= 4; + var data = dataUri.slice(firstCommaIndex + 1); + var decodedString; + if (dataTypeString.indexOf('base64') >= 0) { // data may be encoded in base64 + decodedString = atob(data); // decode data + } else { + // convert the decoded string to UTF-8 + decodedString = unescape(encodeURIComponent(data)); + } + // write the bytes of the string to a typed array + var ia = new window.Uint8Array(decodedString.length); + for (var i = 0; i < decodedString.length; i++) { + ia[i] = decodedString.charCodeAt(i); } - // This is a rectangle in size of the unrotated element. - var bbox = this.getBBox(); - - // Pick the corner point on the element, which meant to stay on its place before and - // after the rotation. - var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); - - // Find an image of the previous indent point. This is the position, where is the - // point actually located on the screen. - var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); + return new Blob([ia], { type: mimeString }); // return the typed array as Blob + }, - // Every point on the element rotates around a circle with the centre of rotation - // in the middle of the element while the whole element is being rotated. That means - // that the distance from a point in the corner of the element (supposed its always rect) to - // the center of the element doesn't change during the rotation and therefore it equals - // to a distance on unrotated element. - // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. - var radius = Math.sqrt((width * width) + (height * height)) / 2; + // Read an image at `url` and return it as base64-encoded data uri. + // The mime type of the image is inferred from the `url` file extension. + // If data uri is provided as `url`, it is returned back unchanged. + // `callback` is a method with `err` as first argument and `dataUri` as second argument. + // Works with IE9. + imageToDataUri: function(url, callback) { - // Now we are looking for an angle between x-axis and the line starting at image of fixed point - // and ending at the center of the element. We call this angle `alpha`. + if (!url || url.substr(0, 'data:'.length) === 'data:') { + // No need to convert to data uri if it is already in data uri. - // The image of a fixed point is located in n-th quadrant. For each quadrant passed - // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. - // - // 3 | 2 - // --c-- Quadrant positions around the element's center `c` - // 0 | 1 - // - var alpha = quadrant * Math.PI / 2; + // This not only convenient but desired. For example, + // IE throws a security error if data:image/svg+xml is used to render + // an image to the canvas and an attempt is made to read out data uri. + // Now if our image is already in data uri, there is no need to render it to the canvas + // and so we can bypass this error. - // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis - // going through the center of the element) and line crossing the indent of the fixed point and the center - // of the element. This is the angle we need but on the unrotated element. - alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); + // Keep the async nature of the function. + return setTimeout(function() { + callback(null, url); + }, 0); + } - // Lastly we have to deduct the original angle the element was rotated by and that's it. - alpha -= g.toRad(angle); + // chrome, IE10+ + var modernHandler = function(xhr, callback) { - // With this angle and distance we can easily calculate the centre of the unrotated element. - // Note that fromPolar constructor accepts an angle in radians. - var center = g.point.fromPolar(radius, alpha, imageFixedPoint); + if (xhr.status === 200) { - // The top left corner on the unrotated element has to be half a width on the left - // and half a height to the top from the center. This will be the origin of rectangle - // we were looking for. - var origin = g.point(center).offset(width / -2, height / -2); + var reader = new FileReader(); - // Resize the element (before re-positioning it). - this.set('size', { width: width, height: height }, opt); + reader.onload = function(evt) { + var dataUri = evt.target.result; + callback(null, dataUri); + }; - // Finally, re-position the element. - this.position(origin.x, origin.y, opt); + reader.onerror = function() { + callback(new Error('Failed to load image ' + url)); + }; - } else { + reader.readAsDataURL(xhr.response); + } else { + callback(new Error('Failed to load image ' + url)); + } + }; - // Resize the element. - this.set('size', { width: width, height: height }, opt); - } + var legacyHandler = function(xhr, callback) { - this.stopBatch('resize', opt); + var Uint8ToString = function(u8a) { + var CHUNK_SZ = 0x8000; + var c = []; + for (var i = 0; i < u8a.length; i += CHUNK_SZ) { + c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); + } + return c.join(''); + }; - return this; - }, + if (xhr.status === 200) { - scale: function(sx, sy, origin, opt) { + var bytes = new Uint8Array(xhr.response); - var scaledBBox = this.getBBox().scale(sx, sy, origin); - this.startBatch('scale', opt); - this.position(scaledBBox.x, scaledBBox.y, opt); - this.resize(scaledBBox.width, scaledBBox.height, opt); - this.stopBatch('scale'); - return this; - }, + var suffix = (url.split('.').pop()) || 'png'; + var map = { + 'svg': 'svg+xml' + }; + var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,'; + var b64encoded = meta + btoa(Uint8ToString(bytes)); + callback(null, b64encoded); + } else { + callback(new Error('Failed to load image ' + url)); + } + }; - fitEmbeds: function(opt) { + var xhr = new XMLHttpRequest(); - opt = opt || {}; + xhr.open('GET', url, true); + xhr.addEventListener('error', function() { + callback(new Error('Failed to load image ' + url)); + }); - // Getting the children's size and position requires the collection. - // Cell.get('embdes') helds an array of cell ids only. - if (!this.graph) throw new Error('Element must be part of a graph.'); + xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer'; - var embeddedCells = this.getEmbeddedCells(); + xhr.addEventListener('load', function() { + if (window.FileReader) { + modernHandler(xhr, callback); + } else { + legacyHandler(xhr, callback); + } + }); - if (embeddedCells.length > 0) { + xhr.send(); + }, - this.startBatch('fit-embeds', opt); + getElementBBox: function(el) { - if (opt.deep) { - // Recursively apply fitEmbeds on all embeds first. - joint.util.invoke(embeddedCells, 'fitEmbeds', opt); + var $el = $(el); + if ($el.length === 0) { + throw new Error('Element not found') } - // Compute cell's size and position based on the children bbox - // and given padding. - var bbox = this.graph.getCellsBBox(embeddedCells); - var padding = joint.util.normalizeSides(opt.padding); - - // Apply padding computed above to the bbox. - bbox.moveAndExpand({ - x: -padding.left, - y: -padding.top, - width: padding.right + padding.left, - height: padding.bottom + padding.top - }); - - // Set new element dimensions finally. - this.set({ - position: { x: bbox.x, y: bbox.y }, - size: { width: bbox.width, height: bbox.height } - }, opt); + var element = $el[0]; + var doc = element.ownerDocument; + var clientBBox = element.getBoundingClientRect(); - this.stopBatch('fit-embeds'); - } + var strokeWidthX = 0; + var strokeWidthY = 0; - return this; - }, + // Firefox correction + if (element.ownerSVGElement) { - // Rotate element by `angle` degrees, optionally around `origin` point. - // If `origin` is not provided, it is considered to be the center of the element. - // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not - // the difference from the previous angle. - rotate: function(angle, absolute, origin, opt) { + var vel = V(element); + var bbox = vel.getBBox({ target: vel.svg() }); - if (origin) { + // if FF getBoundingClientRect includes stroke-width, getBBox doesn't. + // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value. + strokeWidthX = (clientBBox.width - bbox.width); + strokeWidthY = (clientBBox.height - bbox.height); + } - var center = this.getBBox().center(); - var size = this.get('size'); - var position = this.get('position'); - center.rotate(origin, this.get('angle') - angle); - var dx = center.x - size.width / 2 - position.x; - var dy = center.y - size.height / 2 - position.y; - this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); - this.position(position.x + dx, position.y + dy, opt); - this.rotate(angle, absolute, null, opt); - this.stopBatch('rotate'); + return { + x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2, + y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2, + width: clientBBox.width - strokeWidthX, + height: clientBBox.height - strokeWidthY + }; + }, - } else { - this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360, opt); - } + // Highly inspired by the jquery.sortElements plugin by Padolsey. + // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. + sortElements: function(elements, comparator) { - return this; - }, + var $elements = $(elements); + var placements = $elements.map(function() { - getBBox: function(opt) { + var sortElement = this; + var parentNode = sortElement.parentNode; + // Since the element itself will change position, we have + // to have some way of storing it's original position in + // the DOM. The easiest way is to have a 'flag' node: + var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); - opt = opt || {}; + return function() { - if (opt.deep && this.graph) { + if (parentNode === this) { + throw new Error('You can\'t sort elements if any one is a descendant of another.'); + } - // Get all the embedded elements using breadth first algorithm, - // that doesn't use recursion. - var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - // Add the model itself. - elements.push(this); + // Insert before flag: + parentNode.insertBefore(this, nextSibling); + // Remove flag: + parentNode.removeChild(nextSibling); + }; + }); - return this.graph.getCellsBBox(elements); - } + return Array.prototype.sort.call($elements, comparator).each(function(i) { + placements[i].call(this); + }); + }, - var position = this.get('position'); - var size = this.get('size'); + // Sets attributes on the given element and its descendants based on the selector. + // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} + setAttributesBySelector: function(element, attrs) { - return g.rect(position.x, position.y, size.width, size.height); - } -}); + var $element = $(element); -// joint.dia.Element base view and controller. -// ------------------------------------------- + joint.util.forIn(attrs, function(attrs, selector) { + var $elements = $element.find(selector).addBack().filter(selector); + // Make a special case for setting classes. + // We do not want to overwrite any existing class. + if (joint.util.has(attrs, 'class')) { + $elements.addClass(attrs['class']); + attrs = joint.util.omit(attrs, 'class'); + } + $elements.attr(attrs); + }); + }, + // Return a new object with all four sides (top, right, bottom, left) in it. + // Value of each side is taken from the given argument (either number or object). + // Default value for a side is 0. + // Examples: + // joint.util.normalizeSides(5) --> { top: 5, right: 5, bottom: 5, left: 5 } + // joint.util.normalizeSides({ horizontal: 5 }) --> { top: 0, right: 5, bottom: 0, left: 5 } + // joint.util.normalizeSides({ left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 } + // joint.util.normalizeSides({ horizontal: 10, left: 5 }) --> { top: 0, right: 10, bottom: 0, left: 5 } + // joint.util.normalizeSides({ horizontal: 0, left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 } + normalizeSides: function(box) { -joint.dia.ElementView = joint.dia.CellView.extend({ + if (Object(box) !== box) { // `box` is not an object + var val = 0; // `val` left as 0 if `box` cannot be understood as finite number + if (isFinite(box)) val = +box; // actually also accepts string numbers (e.g. '100') - /** - * @abstract - */ - _removePorts: function() { - // implemented in ports.js - }, + return { top: val, right: val, bottom: val, left: val }; + } - /** - * - * @abstract - */ - _renderPorts: function() { - // implemented in ports.js - }, + // `box` is an object + var top, right, bottom, left; + top = right = bottom = left = 0; - className: function() { + if (isFinite(box.vertical)) top = bottom = +box.vertical; + if (isFinite(box.horizontal)) right = left = +box.horizontal; - var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); + if (isFinite(box.top)) top = +box.top; // overwrite vertical + if (isFinite(box.right)) right = +box.right; // overwrite horizontal + if (isFinite(box.bottom)) bottom = +box.bottom; // overwrite vertical + if (isFinite(box.left)) left = +box.left; // overwrite horizontal - classNames.push('element'); + return { top: top, right: right, bottom: bottom, left: left }; + }, - return classNames.join(' '); - }, + timing: { - initialize: function() { + linear: function(t) { + return t; + }, - joint.dia.CellView.prototype.initialize.apply(this, arguments); + quad: function(t) { + return t * t; + }, - var model = this.model; + cubic: function(t) { + return t * t * t; + }, - this.listenTo(model, 'change:position', this.translate); - this.listenTo(model, 'change:size', this.resize); - this.listenTo(model, 'change:angle', this.rotate); - this.listenTo(model, 'change:markup', this.render); + inout: function(t) { + if (t <= 0) return 0; + if (t >= 1) return 1; + var t2 = t * t; + var t3 = t2 * t; + return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); + }, - this._initializePorts(); - }, + exponential: function(t) { + return Math.pow(2, 10 * (t - 1)); + }, - /** - * @abstract - */ - _initializePorts: function() { + bounce: function(t) { + for (var a = 0, b = 1; 1; a += b, b /= 2) { + if (t >= (7 - 4 * a) / 11) { + var q = (11 - 6 * a - 11 * t) / 4; + return -q * q + b * b; + } + } + }, - }, + reverse: function(f) { + return function(t) { + return 1 - f(1 - t); + }; + }, - update: function(cell, renderingOnlyAttrs) { + reflect: function(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); + }; + }, - this._removePorts(); + clamp: function(f, n, x) { + n = n || 0; + x = x || 1; + return function(t) { + var r = f(t); + return r < n ? n : r > x ? x : r; + }; + }, - var model = this.model; - var modelAttrs = model.attr(); - this.updateDOMSubtreeAttributes(this.el, modelAttrs, { - rootBBox: g.Rect(model.size()), - scalableNode: this.scalableNode, - rotatableNode: this.rotatableNode, - // Use rendering only attributes if they differs from the model attributes - roAttributes: (renderingOnlyAttrs === modelAttrs) ? null : renderingOnlyAttrs - }); + back: function(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + }, - this._renderPorts(); - }, + elastic: function(x) { + if (!x) x = 1.5; + return function(t) { + return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); + }; + } + }, - // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the - // default markup is not desirable. - renderMarkup: function() { + interpolate: { - var markup = this.model.get('markup') || this.model.markup; + number: function(a, b) { + var d = b - a; + return function(t) { return a + d * t; }; + }, - if (markup) { + object: function(a, b) { + var s = Object.keys(a); + return function(t) { + var i, p; + var r = {}; + for (i = s.length - 1; i != -1; i--) { + p = s[i]; + r[p] = a[p] + (b[p] - a[p]) * t; + } + return r; + }; + }, - var svg = joint.util.template(markup)(); - var nodes = V(svg); + hexColor: function(a, b) { - this.vel.append(nodes); + var ca = parseInt(a.slice(1), 16); + var cb = parseInt(b.slice(1), 16); + var ra = ca & 0x0000ff; + var rd = (cb & 0x0000ff) - ra; + var ga = ca & 0x00ff00; + var gd = (cb & 0x00ff00) - ga; + var ba = ca & 0xff0000; + var bd = (cb & 0xff0000) - ba; - } else { + return function(t) { - throw new Error('properties.markup is missing while the default render() implementation is used.'); - } - }, + var r = (ra + rd * t) & 0x000000ff; + var g = (ga + gd * t) & 0x0000ff00; + var b = (ba + bd * t) & 0x00ff0000; - render: function() { + return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); + }; + }, - this.$el.empty(); + unit: function(a, b) { - this.renderMarkup(); - this.rotatableNode = this.vel.findOne('.rotatable'); - var scalable = this.scalableNode = this.vel.findOne('.scalable'); - if (scalable) { - // Double update is necessary for elements with the scalable group only - // Note the resize() triggers the other `update`. - this.update(); - } - this.resize(); - this.rotate(); - this.translate(); + var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; + var ma = r.exec(a); + var mb = r.exec(b); + var p = mb[1].indexOf('.'); + var f = p > 0 ? mb[1].length - p - 1 : 0; + a = +ma[1]; + var d = +mb[1] - a; + var u = ma[2]; - return this; - }, + return function(t) { + return (a + d * t).toFixed(f) + u; + }; + } + }, - resize: function(cell, changed, opt) { + // SVG filters. + filter: { - var model = this.model; - var size = model.get('size') || { width: 1, height: 1 }; - var angle = model.get('angle') || 0; + // `color` ... outline color + // `width`... outline width + // `opacity` ... outline opacity + // `margin` ... gap between outline and the element + outline: function(args) { - var scalable = this.scalableNode; - if (!scalable) { + var tpl = ''; - if (angle !== 0) { - // update the origin of the rotation - this.rotate(); - } - // update the ref attributes - this.update(); + var margin = Number.isFinite(args.margin) ? args.margin : 2; + var width = Number.isFinite(args.width) ? args.width : 1; - // If there is no scalable elements, than there is nothing to scale. - return; - } + return joint.util.template(tpl)({ + color: args.color || 'blue', + opacity: Number.isFinite(args.opacity) ? args.opacity : 1, + outerRadius: margin + width, + innerRadius: margin + }); + }, - // Getting scalable group's bbox. - // Due to a bug in webkit's native SVG .getBBox implementation, the bbox of groups with path children includes the paths' control points. - // To work around the issue, we need to check whether there are any path elements inside the scalable group. - var recursive = false; - if (scalable.node.getElementsByTagName('path').length > 0) { - // If scalable has at least one descendant that is a path, we need to switch to recursive bbox calculation. - // If there are no path descendants, group bbox calculation works and so we can use the (faster) native function directly. - recursive = true; - } - var scalableBBox = scalable.getBBox({ recursive: recursive }); + // `color` ... color + // `width`... width + // `blur` ... blur + // `opacity` ... opacity + highlight: function(args) { - // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making - // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. - var sx = (size.width / (scalableBBox.width || 1)); - var sy = (size.height / (scalableBBox.height || 1)); - scalable.attr('transform', 'scale(' + sx + ',' + sy + ')'); - - // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` - // Order of transformations is significant but we want to reconstruct the object always in the order: - // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, - // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the - // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation - // around the center of the resized object (which is a different origin then the origin of the previous rotation) - // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. + var tpl = ''; - // Cancel the rotation but now around a different origin, which is the center of the scaled object. - var rotatable = this.rotatableNode; - var rotation = rotatable && rotatable.attr('transform'); - if (rotation && rotation !== 'null') { + return joint.util.template(tpl)({ + color: args.color || 'red', + width: Number.isFinite(args.width) ? args.width : 1, + blur: Number.isFinite(args.blur) ? args.blur : 0, + opacity: Number.isFinite(args.opacity) ? args.opacity : 1 + }); + }, - rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); - var rotatableBBox = scalable.getBBox({ target: this.paper.viewport }); + // `x` ... horizontal blur + // `y` ... vertical blur (optional) + blur: function(args) { - // Store new x, y and perform rotate() again against the new rotation origin. - model.set('position', { x: rotatableBBox.x, y: rotatableBBox.y }, opt); - this.rotate(); - } + var x = Number.isFinite(args.x) ? args.x : 2; - // Update must always be called on non-rotated element. Otherwise, relative positioning - // would work with wrong (rotated) bounding boxes. - this.update(); - }, + return joint.util.template('')({ + stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x + }); + }, - translate: function(model, changes, opt) { + // `dx` ... horizontal shift + // `dy` ... vertical shift + // `blur` ... blur + // `color` ... color + // `opacity` ... opacity + dropShadow: function(args) { - var position = this.model.get('position') || { x: 0, y: 0 }; + var tpl = 'SVGFEDropShadowElement' in window + ? '' + : ''; - this.vel.attr('transform', 'translate(' + position.x + ',' + position.y + ')'); - }, + return joint.util.template(tpl)({ + dx: args.dx || 0, + dy: args.dy || 0, + opacity: Number.isFinite(args.opacity) ? args.opacity : 1, + color: args.color || 'black', + blur: Number.isFinite(args.blur) ? args.blur : 4 + }); + }, - rotate: function() { + // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. + grayscale: function(args) { - var rotatable = this.rotatableNode; - if (!rotatable) { - // If there is no rotatable elements, then there is nothing to rotate. - return; - } + var amount = Number.isFinite(args.amount) ? args.amount : 1; - var angle = this.model.get('angle') || 0; - var size = this.model.get('size') || { width: 1, height: 1 }; + return joint.util.template('')({ + a: 0.2126 + 0.7874 * (1 - amount), + b: 0.7152 - 0.7152 * (1 - amount), + c: 0.0722 - 0.0722 * (1 - amount), + d: 0.2126 - 0.2126 * (1 - amount), + e: 0.7152 + 0.2848 * (1 - amount), + f: 0.0722 - 0.0722 * (1 - amount), + g: 0.2126 - 0.2126 * (1 - amount), + h: 0.0722 + 0.9278 * (1 - amount) + }); + }, - var ox = size.width / 2; - var oy = size.height / 2; + // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. + sepia: function(args) { - if (angle !== 0) { - rotatable.attr('transform', 'rotate(' + angle + ',' + ox + ',' + oy + ')'); - } else { - rotatable.removeAttr('transform'); - } - }, + var amount = Number.isFinite(args.amount) ? args.amount : 1; - getBBox: function(opt) { + return joint.util.template('')({ + a: 0.393 + 0.607 * (1 - amount), + b: 0.769 - 0.769 * (1 - amount), + c: 0.189 - 0.189 * (1 - amount), + d: 0.349 - 0.349 * (1 - amount), + e: 0.686 + 0.314 * (1 - amount), + f: 0.168 - 0.168 * (1 - amount), + g: 0.272 - 0.272 * (1 - amount), + h: 0.534 - 0.534 * (1 - amount), + i: 0.131 + 0.869 * (1 - amount) + }); + }, - if (opt && opt.useModelGeometry) { - var bbox = this.model.getBBox().bbox(this.model.get('angle')); - return this.paper.localToPaperRect(bbox); - } + // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + saturate: function(args) { - return joint.dia.CellView.prototype.getBBox.apply(this, arguments); - }, + var amount = Number.isFinite(args.amount) ? args.amount : 1; - // Embedding mode methods - // ---------------------- + return joint.util.template('')({ + amount: 1 - amount + }); + }, - prepareEmbedding: function(opt) { + // `angle` ... the number of degrees around the color circle the input samples will be adjusted. + hueRotate: function(args) { - opt = opt || {}; + return joint.util.template('')({ + angle: args.angle || 0 + }); + }, - var model = opt.model || this.model; - var paper = opt.paper || this.paper; - var graph = paper.model; + // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. + invert: function(args) { - model.startBatch('to-front', opt); + var amount = Number.isFinite(args.amount) ? args.amount : 1; - // Bring the model to the front with all his embeds. - model.toFront({ deep: true, ui: true }); + return joint.util.template('')({ + amount: amount, + amount2: 1 - amount + }); + }, - // Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see - // the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z. - var maxZ = graph.get('cells').max('z').get('z'); - var connectedLinks = graph.getConnectedLinks(model, { deep: true }); + // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + brightness: function(args) { - // Move to front also all the inbound and outbound links that are connected - // to any of the element descendant. If we bring to front only embedded elements, - // links connected to them would stay in the background. - joint.util.invoke(connectedLinks, 'set', 'z', maxZ + 1, { ui: true }); + return joint.util.template('')({ + amount: Number.isFinite(args.amount) ? args.amount : 1 + }); + }, - model.stopBatch('to-front'); + // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + contrast: function(args) { - // Before we start looking for suitable parent we remove the current one. - var parentId = model.get('parent'); - parentId && graph.getCell(parentId).unembed(model, { ui: true }); - }, + var amount = Number.isFinite(args.amount) ? args.amount : 1; - processEmbedding: function(opt) { + return joint.util.template('')({ + amount: amount, + amount2: .5 - amount / 2 + }); + } + }, - opt = opt || {}; + format: { - var model = opt.model || this.model; - var paper = opt.paper || this.paper; + // Formatting numbers via the Python Format Specification Mini-language. + // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. + // Heavilly inspired by the D3.js library implementation. + number: function(specifier, value, locale) { - var paperOptions = paper.options; - var candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); + locale = locale || { - if (paperOptions.frontParentOnly) { - // pick the element with the highest `z` index - candidates = candidates.slice(-1); - } + currency: ['$', ''], + decimal: '.', + thousands: ',', + grouping: [3] + }; - var newCandidateView = null; - var prevCandidateView = this._candidateEmbedView; + // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. + // [[fill]align][sign][symbol][0][width][,][.precision][type] + var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; - // iterate over all candidates starting from the last one (has the highest z-index). - for (var i = candidates.length - 1; i >= 0; i--) { + var match = re.exec(specifier); + var fill = match[1] || ' '; + var align = match[2] || '>'; + var sign = match[3] || ''; + var symbol = match[4] || ''; + var zfill = match[5]; + var width = +match[6]; + var comma = match[7]; + var precision = match[8]; + var type = match[9]; + var scale = 1; + var prefix = ''; + var suffix = ''; + var integer = false; - var candidate = candidates[i]; + if (precision) precision = +precision.substring(1); - if (prevCandidateView && prevCandidateView.model.id == candidate.id) { + if (zfill || fill === '0' && align === '=') { + zfill = fill = '0'; + align = '='; + if (comma) width -= Math.floor((width - 1) / 4); + } - // candidate remains the same - newCandidateView = prevCandidateView; - break; + switch (type) { + case 'n': + comma = true; type = 'g'; + break; + case '%': + scale = 100; suffix = '%'; type = 'f'; + break; + case 'p': + scale = 100; suffix = '%'; type = 'r'; + break; + case 'b': + case 'o': + case 'x': + case 'X': + if (symbol === '#') prefix = '0' + type.toLowerCase(); + break; + case 'c': + case 'd': + integer = true; precision = 0; + break; + case 's': + scale = -1; type = 'r'; + break; + } - } else { + if (symbol === '$') { + prefix = locale.currency[0]; + suffix = locale.currency[1]; + } - var view = candidate.findView(paper); - if (paperOptions.validateEmbedding.call(paper, this, view)) { + // If no precision is specified for `'r'`, fallback to general notation. + if (type == 'r' && !precision) type = 'g'; - // flip to the new candidate - newCandidateView = view; - break; + // Ensure that the requested precision is in the supported range. + if (precision != null) { + if (type == 'g') precision = Math.max(1, Math.min(21, precision)); + else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); } - } - } - - if (newCandidateView && newCandidateView != prevCandidateView) { - // A new candidate view found. Highlight the new one. - this.clearEmbedding(); - this._candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); - } - if (!newCandidateView && prevCandidateView) { - // No candidate view found. Unhighlight the previous candidate. - this.clearEmbedding(); - } - }, + var zcomma = zfill && comma; - clearEmbedding: function() { + // Return the empty string for floats formatted as ints. + if (integer && (value % 1)) return ''; - var candidateView = this._candidateEmbedView; - if (candidateView) { - // No candidate view found. Unhighlight the previous candidate. - candidateView.unhighlight(null, { embedding: true }); - this._candidateEmbedView = null; - } - }, + // Convert negative to positive, and record the sign prefix. + var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; - finalizeEmbedding: function(opt) { + var fullSuffix = suffix; - opt = opt || {}; + // Apply the scale, computing it from the value's exponent for si format. + // Preserve the existing suffix, if any, such as the currency symbol. + if (scale < 0) { + var unit = this.prefix(value, precision); + value = unit.scale(value); + fullSuffix = unit.symbol + suffix; + } else { + value *= scale; + } - var candidateView = this._candidateEmbedView; - var model = opt.model || this.model; - var paper = opt.paper || this.paper; + // Convert to the desired precision. + value = this.convert(type, value, precision); - if (candidateView) { + // Break the value into the integer part (before) and decimal part (after). + var i = value.lastIndexOf('.'); + var before = i < 0 ? value : value.substring(0, i); + var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); - // We finished embedding. Candidate view is chosen to become the parent of the model. - candidateView.model.embed(model, { ui: true }); - candidateView.unhighlight(null, { embedding: true }); + function formatGroup(value) { - delete this._candidateEmbedView; - } + var i = value.length; + var t = []; + var j = 0; + var g = locale.grouping[0]; + while (i > 0 && g > 0) { + t.push(value.substring(i -= g, i + g)); + g = locale.grouping[j = (j + 1) % locale.grouping.length]; + } + return t.reverse().join(locale.thousands); + } - joint.util.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); - }, + // If the fill character is not `'0'`, grouping is applied before padding. + if (!zfill && comma && locale.grouping) { - // Interaction. The controller part. - // --------------------------------- + before = formatGroup(before); + } - pointerdown: function(evt, x, y) { + var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); + var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; - var paper = this.paper; + // If the fill character is `'0'`, grouping is applied after padding. + if (zcomma) before = formatGroup(padding + before); - if ( - evt.target.getAttribute('magnet') && - this.can('addLinkFromMagnet') && - paper.options.validateMagnet.call(paper, this, evt.target) - ) { + // Apply prefix. + negative += prefix; - this.model.startBatch('add-link'); + // Rejoin integer and decimal parts. + value = before + after; - var link = paper.getDefaultLink(this, evt.target); + return (align === '<' ? negative + value + padding + : align === '>' ? padding + negative + value + : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) + : negative + (zcomma ? value : padding + value)) + fullSuffix; + }, - link.set({ - source: { - id: this.model.id, - selector: this.getSelector(evt.target), - port: evt.target.getAttribute('port') - }, - target: { x: x, y: y } - }); + // Formatting string via the Python Format string. + // See https://docs.python.org/2/library/string.html#format-string-syntax) + string: function(formatString, value) { - paper.model.addCell(link); + var fieldDelimiterIndex; + var fieldDelimiter = '{'; + var endPlaceholder = false; + var formattedStringArray = []; - var linkView = this._linkView = paper.findViewByModel(link); + while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { - linkView.pointerdown(evt, x, y); - linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); + var pieceFormatedString, formatSpec, fieldName; - } else { + pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); - this._dx = x; - this._dy = y; + if (endPlaceholder) { + formatSpec = pieceFormatedString.split(':'); + fieldName = formatSpec.shift().split('.'); + pieceFormatedString = value; - this.restrictedArea = paper.getRestrictedArea(this); + for (var i = 0; i < fieldName.length; i++) + pieceFormatedString = pieceFormatedString[fieldName[i]]; - joint.dia.CellView.prototype.pointerdown.apply(this, arguments); - this.notify('element:pointerdown', evt, x, y); - } - }, + if (formatSpec.length) + pieceFormatedString = this.number(formatSpec, pieceFormatedString); + } - pointermove: function(evt, x, y) { + formattedStringArray.push(pieceFormatedString); - if (this._linkView) { + formatString = formatString.slice(fieldDelimiterIndex + 1); + fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; + } + formattedStringArray.push(formatString); - // let the linkview deal with this event - this._linkView.pointermove(evt, x, y); + return formattedStringArray.join(''); + }, - } else { + convert: function(type, value, precision) { - var grid = this.paper.options.gridSize; + switch (type) { + case 'b': return value.toString(2); + case 'c': return String.fromCharCode(value); + case 'o': return value.toString(8); + case 'x': return value.toString(16); + case 'X': return value.toString(16).toUpperCase(); + case 'g': return value.toPrecision(precision); + case 'e': return value.toExponential(precision); + case 'f': return value.toFixed(precision); + case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); + default: return value + ''; + } + }, - if (this.can('elementMove')) { + round: function(value, precision) { - var position = this.model.get('position'); + return precision + ? Math.round(value * (precision = Math.pow(10, precision))) / precision + : Math.round(value); + }, - // Make sure the new element's position always snaps to the current grid after - // translate as the previous one could be calculated with a different grid size. - var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - this._dx, grid); - var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - this._dy, grid); + precision: function(value, precision) { - this.model.translate(tx, ty, { restrictedArea: this.restrictedArea, ui: true }); + return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); + }, - if (this.paper.options.embeddingMode) { + prefix: function(value, precision) { - if (!this._inProcessOfEmbedding) { - // Prepare the element for embedding only if the pointer moves. - // We don't want to do unnecessary action with the element - // if an user only clicks/dblclicks on it. - this.prepareEmbedding(); - this._inProcessOfEmbedding = true; - } + var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) { + var k = Math.pow(10, Math.abs(8 - i) * 3); + return { + scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, + symbol: d + }; + }); - this.processEmbedding(); + var i = 0; + if (value) { + if (value < 0) value *= -1; + if (precision) value = this.round(value, this.precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); } + return prefixes[8 + i / 3]; } + }, - this._dx = g.snapToGrid(x, grid); - this._dy = g.snapToGrid(y, grid); + /* + Pre-compile the HTML to be used as a template. + */ + template: function(html) { - joint.dia.CellView.prototype.pointermove.apply(this, arguments); - this.notify('element:pointermove', evt, x, y); - } - }, + /* + Must support the variation in templating syntax found here: + https://lodash.com/docs#template + */ + var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; - pointerup: function(evt, x, y) { + return function(data) { - if (this._linkView) { + data = data || {}; - // Let the linkview deal with this event. - this._linkView.pointerup(evt, x, y); - this._linkView = null; - this.model.stopBatch('add-link'); + return html.replace(regex, function(match) { - } else { + var args = Array.from(arguments); + var attr = args.slice(1, 4).find(function(_attr) { + return !!_attr; + }); - if (this._inProcessOfEmbedding) { - this.finalizeEmbedding(); - this._inProcessOfEmbedding = false; - } + var attrArray = attr.split('.'); + var value = data[attrArray.shift()]; - this.notify('element:pointerup', evt, x, y); - joint.dia.CellView.prototype.pointerup.apply(this, arguments); - } - }, + while (value !== undefined && attrArray.length) { + value = value[attrArray.shift()]; + } - mouseenter: function(evt) { + return value !== undefined ? value : ''; + }); + }; + }, - joint.dia.CellView.prototype.mouseenter.apply(this, arguments); - this.notify('element:mouseenter', evt); - }, + /** + * @param {Element=} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default. + */ + toggleFullScreen: function(el) { - mouseleave: function(evt) { + var topDocument = window.top.document; + el = el || topDocument.body; - joint.dia.CellView.prototype.mouseleave.apply(this, arguments); - this.notify('element:mouseleave', evt); - } -}); + function prefixedResult(el, prop) { + var prefixes = ['webkit', 'moz', 'ms', 'o', '']; + for (var i = 0; i < prefixes.length; i++) { + var prefix = prefixes[i]; + var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); + if (el[propName] !== undefined) { + return joint.util.isFunction(el[propName]) ? el[propName]() : el[propName]; + } + } + } -// joint.dia.Link base model. -// -------------------------- + if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) { + prefixedResult(topDocument, 'ExitFullscreen') || // Spec. + prefixedResult(topDocument, 'CancelFullScreen'); // Firefox + } else { + prefixedResult(el, 'RequestFullscreen') || // Spec. + prefixedResult(el, 'RequestFullScreen'); // Firefox + } + }, -joint.dia.Link = joint.dia.Cell.extend({ + addClassNamePrefix: function(className) { - // The default markup for links. - markup: [ - '', - '', - '', - '', - '', - '', - '', - '' - ].join(''), + if (!className) return className; - labelMarkup: [ - '', - '', - '', - '' - ].join(''), + return className.toString().split(' ').map(function(_className) { - toolMarkup: [ - '', - '', - '', - '', - 'Remove link.', - '', - '', - '', - '', - 'Link options.', - '', - '' - ].join(''), + if (_className.substr(0, joint.config.classNamePrefix.length) !== joint.config.classNamePrefix) { + _className = joint.config.classNamePrefix + _className; + } - // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). - // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for - // dragging vertices (changin their position). The latter is used for removing vertices. - vertexMarkup: [ - '', - '', - '', - '', - 'Remove vertex.', - '', - '' - ].join(''), + return _className; - arrowheadMarkup: [ - '', - '', - '' - ].join(''), + }).join(' '); + }, - defaults: { + removeClassNamePrefix: function(className) { - type: 'link', - source: {}, - target: {} - }, - - isLink: function() { - - return true; - }, - - disconnect: function() { - - return this.set({ source: g.point(0, 0), target: g.point(0, 0) }); - }, - - // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. - label: function(idx, value, opt) { - - idx = idx || 0; + if (!className) return className; - // Is it a getter? - if (arguments.length <= 1) { - return this.prop(['labels', idx]); - } + return className.toString().split(' ').map(function(_className) { - return this.prop(['labels', idx], value, opt); - }, + if (_className.substr(0, joint.config.classNamePrefix.length) === joint.config.classNamePrefix) { + _className = _className.substr(joint.config.classNamePrefix.length); + } - translate: function(tx, ty, opt) { + return _className; - // enrich the option object - opt = opt || {}; - opt.translateBy = opt.translateBy || this.id; - opt.tx = tx; - opt.ty = ty; + }).join(' '); + }, - return this.applyToPoints(function(p) { - return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; - }, opt); - }, + wrapWith: function(object, methods, wrapper) { - scale: function(sx, sy, origin, opt) { + if (joint.util.isString(wrapper)) { - return this.applyToPoints(function(p) { - return g.point(p).scale(sx, sy, origin).toJSON(); - }, opt); - }, + if (!joint.util.wrappers[wrapper]) { + throw new Error('Unknown wrapper: "' + wrapper + '"'); + } - applyToPoints: function(fn, opt) { + wrapper = joint.util.wrappers[wrapper]; + } - if (!joint.util.isFunction(fn)) { - throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); - } + if (!joint.util.isFunction(wrapper)) { + throw new Error('Wrapper must be a function.'); + } - var attrs = {}; + this.toArray(methods).forEach(function(method) { + object[method] = wrapper(object[method]); + }); + }, - var source = this.get('source'); - if (!source.id) { - attrs.source = fn(source); - } + wrappers: { - var target = this.get('target'); - if (!target.id) { - attrs.target = fn(target); - } + /* + Prepares a function with the following usage: - var vertices = this.get('vertices'); - if (vertices && vertices.length > 0) { - attrs.vertices = vertices.map(fn); - } + fn([cell, cell, cell], opt); + fn([cell, cell, cell]); + fn(cell, cell, cell, opt); + fn(cell, cell, cell); + fn(cell); + */ + cells: function(fn) { - return this.set(attrs, opt); - }, + return function() { - reparent: function(opt) { + var args = Array.from(arguments); + var n = args.length; + var cells = n > 0 && args[0] || []; + var opt = n > 1 && args[n - 1] || {}; - var newParent; + if (!Array.isArray(cells)) { - if (this.graph) { + if (opt instanceof joint.dia.Cell) { + cells = args; + } else if (cells instanceof joint.dia.Cell) { + if (args.length > 1) { + args.pop(); + } + cells = args; + } + } - var source = this.graph.getCell(this.get('source').id); - var target = this.graph.getCell(this.get('target').id); - var prevParent = this.graph.getCell(this.get('parent')); + if (opt instanceof joint.dia.Cell) { + opt = {}; + } - if (source && target) { - newParent = this.graph.getCommonAncestor(source, target); + return fn.call(this, cells, opt); + }; } + }, - if (prevParent && (!newParent || newParent.id !== prevParent.id)) { - // Unembed the link if source and target has no common ancestor - // or common ancestor changed - prevParent.unembed(this, opt); + parseDOMJSON: function(json, namespace) { + + var selectors = {}; + var svgNamespace = V.namespace.xmlns; + var ns = namespace || svgNamespace; + var fragment = document.createDocumentFragment(); + var queue = [json, fragment, ns]; + while (queue.length > 0) { + ns = queue.pop(); + var parentNode = queue.pop(); + var siblingsDef = queue.pop(); + for (var i = 0, n = siblingsDef.length; i < n; i++) { + var nodeDef = siblingsDef[i]; + // TagName + if (!nodeDef.hasOwnProperty('tagName')) throw new Error('json-dom-parser: missing tagName'); + var tagName = nodeDef.tagName; + // Namespace URI + if (nodeDef.hasOwnProperty('namespaceURI')) ns = nodeDef.namespaceURI; + var node = document.createElementNS(ns, tagName); + var svg = (ns === svgNamespace); + var wrapper = (svg) ? V : $; + // Attributes + var attributes = nodeDef.attributes; + if (attributes) wrapper(node).attr(attributes); + // Style + var style = nodeDef.style; + if (style) $(node).css(style); + // ClassName + if (nodeDef.hasOwnProperty('className')) { + var className = nodeDef.className; + if (svg) { + node.className.baseVal = className; + } else { + node.className = className; + } + } + // Selector + if (nodeDef.hasOwnProperty('selector')) { + var nodeSelector = nodeDef.selector; + if (selectors[nodeSelector]) throw new Error('json-dom-parser: selector must be unique'); + selectors[nodeSelector] = node; + wrapper(node).attr('joint-selector', nodeSelector); + } + parentNode.appendChild(node); + // Children + var childrenDef = nodeDef.children; + if (Array.isArray(childrenDef)) queue.push(childrenDef, node, ns); + } } - - if (newParent) { - newParent.embed(this, opt); + return { + fragment: fragment, + selectors: selectors } - } + }, - return newParent; - }, + // lodash 3 vs 4 incompatible + sortedIndex: _.sortedIndexBy || _.sortedIndex, + uniq: _.uniqBy || _.uniq, + uniqueId: _.uniqueId, + sortBy: _.sortBy, + isFunction: _.isFunction, + result: _.result, + union: _.union, + invoke: _.invokeMap || _.invoke, + difference: _.difference, + intersection: _.intersection, + omit: _.omit, + pick: _.pick, + has: _.has, + bindAll: _.bindAll, + assign: _.assign, + defaults: _.defaults, + defaultsDeep: _.defaultsDeep, + isPlainObject: _.isPlainObject, + isEmpty: _.isEmpty, + isEqual: _.isEqual, + noop: function() {}, + cloneDeep: _.cloneDeep, + toArray: _.toArray, + flattenDeep: _.flattenDeep, + camelCase: _.camelCase, + groupBy: _.groupBy, + forIn: _.forIn, + without: _.without, + debounce: _.debounce, + clone: _.clone, - hasLoop: function(opt) { + isBoolean: function(value) { + var toString = Object.prototype.toString; + return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]'); + }, - opt = opt || {}; + isObject: function(value) { + return !!value && (typeof value === 'object' || typeof value === 'function'); + }, - var sourceId = this.get('source').id; - var targetId = this.get('target').id; + isNumber: function(value) { + var toString = Object.prototype.toString; + return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]'); + }, - if (!sourceId || !targetId) { - // Link "pinned" to the paper does not have a loop. - return false; - } + isString: function(value) { + var toString = Object.prototype.toString; + return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]'); + }, - var loop = sourceId === targetId; + merge: function() { + if (_.mergeWith) { + var args = Array.from(arguments); + var last = args[args.length - 1]; - // Note that there in the deep mode a link can have a loop, - // even if it connects only a parent and its embed. - // A loop "target equals source" is valid in both shallow and deep mode. - if (!loop && opt.deep && this.graph) { + var customizer = this.isFunction(last) ? last : this.noop; + args.push(function(a,b) { + var customResult = customizer(a, b); + if (customResult !== undefined) { + return customResult; + } - var sourceElement = this.graph.getCell(sourceId); - var targetElement = this.graph.getCell(targetId); + if (Array.isArray(a) && !Array.isArray(b)) { + return b; + } + }); - loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); + return _.mergeWith.apply(this, args) + } + return _.merge.apply(this, arguments); } + } +}; - return loop; - }, - - getSourceElement: function() { - var source = this.get('source'); +joint.mvc.View = Backbone.View.extend({ - return (source && source.id && this.graph && this.graph.getCell(source.id)) || null; - }, + options: {}, + theme: null, + themeClassNamePrefix: joint.util.addClassNamePrefix('theme-'), + requireSetThemeOverride: false, + defaultTheme: joint.config.defaultTheme, + children: null, + childNodes: null, - getTargetElement: function() { + constructor: function(options) { - var target = this.get('target'); + this.requireSetThemeOverride = options && !!options.theme; + this.options = joint.util.assign({}, this.options, options); - return (target && target.id && this.graph && this.graph.getCell(target.id)) || null; + Backbone.View.call(this, options); }, - // Returns the common ancestor for the source element, - // target element and the link itself. - getRelationshipAncestor: function() { + initialize: function(options) { - var connectionAncestor; + joint.util.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); - if (this.graph) { + joint.mvc.views[this.cid] = this; - var cells = [ - this, - this.getSourceElement(), // null if source is a point - this.getTargetElement() // null if target is a point - ].filter(function(item) { - return !!item; - }); + this.setTheme(this.options.theme || this.defaultTheme); + this.init(); + }, - connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); + renderChildren: function(children) { + children || (children = this.children); + if (children) { + var namespace = V.namespace[this.svgElement ? 'xmlns' : 'xhtml']; + var doc = joint.util.parseDOMJSON(children, namespace); + this.vel.empty().append(doc.fragment); + this.childNodes = doc.selectors; } - - return connectionAncestor || null; + return this; }, - // Is source, target and the link itself embedded in a given cell? - isRelationshipEmbeddedIn: function(cell) { + // Override the Backbone `_ensureElement()` method in order to create an + // svg element (e.g., ``) node that wraps all the nodes of the Cell view. + // Expose class name setter as a separate method. + _ensureElement: function() { + if (!this.el) { + var tagName = joint.util.result(this, 'tagName'); + var attrs = joint.util.assign({}, joint.util.result(this, 'attributes')); + if (this.id) attrs.id = joint.util.result(this, 'id'); + this.setElement(this._createElement(tagName)); + this._setAttributes(attrs); + } else { + this.setElement(joint.util.result(this, 'el')); + } + this._ensureElClassName(); + }, - var cellId = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : cell.id; - var ancestor = this.getRelationshipAncestor(); - - return !!ancestor && (ancestor.id === cellId || ancestor.isEmbeddedIn(cellId)); - } -}, - { - endsEqual: function(a, b) { + _setAttributes: function(attrs) { + if (this.svgElement) { + this.vel.attr(attrs); + } else { + this.$el.attr(attrs); + } + }, - var portsEqual = a.port === b.port || !a.port && !b.port; - return a.id === b.id && portsEqual; + _createElement: function(tagName) { + if (this.svgElement) { + return document.createElementNS(V.namespace.xmlns, tagName); + } else { + return document.createElement(tagName); } - }); + }, + // Utilize an alternative DOM manipulation API by + // adding an element reference wrapped in Vectorizer. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + if (this.svgElement) this.vel = V(this.el); + }, -// joint.dia.Link base view and controller. -// ---------------------------------------- + _ensureElClassName: function() { + var className = joint.util.result(this, 'className'); + var prefixedClassName = joint.util.addClassNamePrefix(className); + // Note: className removal here kept for backwards compatibility only + if (this.svgElement) { + this.vel.removeClass(className).addClass(prefixedClassName); + } else { + this.$el.removeClass(className).addClass(prefixedClassName); + } + }, -joint.dia.LinkView = joint.dia.CellView.extend({ + init: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, - className: function() { + onRender: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, - var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); + setTheme: function(theme, opt) { - classNames.push('link'); + opt = opt || {}; - return classNames.join(' '); - }, + // Theme is already set, override is required, and override has not been set. + // Don't set the theme. + if (this.theme && this.requireSetThemeOverride && !opt.override) { + return this; + } - options: { + this.removeThemeClassName(); + this.addThemeClassName(theme); + this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); + this.theme = theme; - shortLinkLength: 100, - doubleLinkTools: false, - longLinkLength: 160, - linkToolsOffset: 40, - doubleLinkToolsOffset: 60, - sampleInterval: 50 + return this; }, - _z: null, + addThemeClassName: function(theme) { - initialize: function(options) { + theme = theme || this.theme; - joint.dia.CellView.prototype.initialize.apply(this, arguments); + var className = this.themeClassNamePrefix + theme; - // create methods in prototype, so they can be accessed from any instance and - // don't need to be create over and over - if (typeof this.constructor.prototype.watchSource !== 'function') { - this.constructor.prototype.watchSource = this.createWatcher('source'); - this.constructor.prototype.watchTarget = this.createWatcher('target'); + if (this.svgElement) { + this.vel.addClass(className); + } else { + this.$el.addClass(className); } - // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to - // `` nodes wrapped by Vectorizer. This allows for quick access to the - // nodes in `updateLabelPosition()` in order to update the label positions. - this._labelCache = {}; - - // keeps markers bboxes and positions again for quicker access - this._markerCache = {}; - - // bind events - this.startListening(); + return this; }, - startListening: function() { - - var model = this.model; + removeThemeClassName: function(theme) { - this.listenTo(model, 'change:markup', this.render); - this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); - this.listenTo(model, 'change:toolMarkup', this.onToolsChange); - this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); - this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); - this.listenTo(model, 'change:source', this.onSourceChange); - this.listenTo(model, 'change:target', this.onTargetChange); - }, + theme = theme || this.theme; - onSourceChange: function(cell, source, opt) { + var className = this.themeClassNamePrefix + theme; - // Start watching the new source model. - this.watchSource(cell, source); - // This handler is called when the source attribute is changed. - // This can happen either when someone reconnects the link (or moves arrowhead), - // or when an embedded link is translated by its ancestor. - // 1. Always do update. - // 2. Do update only if the opposite end ('target') is also a point. - if (!opt.translateBy || !this.model.get('target').id) { - opt.updateConnectionOnly = true; - this.update(this.model, null, opt); + if (this.svgElement) { + this.vel.removeClass(className); + } else { + this.$el.removeClass(className); } - }, - onTargetChange: function(cell, target, opt) { + return this; + }, - // Start watching the new target model. - this.watchTarget(cell, target); - // See `onSourceChange` method. - if (!opt.translateBy) { - opt.updateConnectionOnly = true; - this.update(this.model, null, opt); - } + onSetTheme: function(oldTheme, newTheme) { + // Intentionally empty. + // This method is meant to be overriden. }, - onVerticesChange: function(cell, changed, opt) { + remove: function() { - this.renderVertexMarkers(); + this.onRemove(); + this.undelegateDocumentEvents(); - // If the vertices have been changed by a translation we do update only if the link was - // the only link that was translated. If the link was translated via another element which the link - // is embedded in, this element will be translated as well and that triggers an update. - // Note that all embeds in a model are sorted - first comes links, then elements. - if (!opt.translateBy || opt.translateBy === this.model.id) { - // Vertices were changed (not as a reaction on translate) - // or link.translate() was called or - opt.updateConnectionOnly = true; - this.update(cell, null, opt); - } - }, + joint.mvc.views[this.cid] = null; - onToolsChange: function() { + Backbone.View.prototype.remove.apply(this, arguments); - this.renderTools().updateToolsPosition(); + return this; }, - onLabelsChange: function(link, labels, opt) { - - var requireRender = true; + onRemove: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, - var previousLabels = this.model.previous('labels'); + getEventNamespace: function() { + // Returns a per-session unique namespace + return '.joint-event-ns-' + this.cid; + }, - if (previousLabels) { - // Here is an optimalization for cases when we know, that change does - // not require rerendering of all labels. - if (('propertyPathArray' in opt) && ('propertyValue' in opt)) { - // The label is setting by `prop()` method - var pathArray = opt.propertyPathArray || []; - var pathLength = pathArray.length; - if (pathLength > 1) { - // We are changing a single label here e.g. 'labels/0/position' - var labelExists = !!previousLabels[pathArray[1]]; - if (labelExists) { - if (pathLength === 2) { - // We are changing the entire label. Need to check if the - // markup is also being changed. - requireRender = ('markup' in Object(opt.propertyValue)); - } else if (pathArray[2] !== 'markup') { - // We are changing a label property but not the markup - requireRender = false; - } - } - } - } + delegateElementEvents: function(element, events, data) { + if (!events) return this; + data || (data = {}); + var eventNS = this.getEventNamespace(); + for (var eventName in events) { + var method = events[eventName]; + if (typeof method !== 'function') method = this[method]; + if (!method) continue; + $(element).on(eventName + eventNS, data, method.bind(this)); } + return this; + }, - if (requireRender) { - this.renderLabels(); - } else { - this.updateLabels(); - } + undelegateElementEvents: function(element) { + $(element).off(this.getEventNamespace()); + return this; + }, - this.updateLabelPositions(); + delegateDocumentEvents: function(events, data) { + events || (events = joint.util.result(this, 'documentEvents')); + return this.delegateElementEvents(document, events, data); }, - // Rendering - //---------- + undelegateDocumentEvents: function() { + return this.undelegateElementEvents(document); + }, - render: function() { + eventData: function(evt, data) { + if (!evt) throw new Error('eventData(): event object required.'); + var currentData = evt.data; + var key = '__' + this.cid + '__'; + if (data === undefined) { + if (!currentData) return {}; + return currentData[key] || {}; + } + currentData || (currentData = evt.data = {}); + currentData[key] || (currentData[key] = {}); + joint.util.assign(currentData[key], data); + return this; + } - this.$el.empty(); +}, { - // A special markup can be given in the `properties.markup` property. This might be handy - // if e.g. arrowhead markers should be `` elements or any other element than ``s. - // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors - // of elements with special meaning though. Therefore, those classes should be preserved in any - // special markup passed in `properties.markup`. - var model = this.model; - var markup = model.get('markup') || model.markup; - var children = V(markup); + extend: function() { - // custom markup may contain only one children - if (!Array.isArray(children)) children = [children]; + var args = Array.from(arguments); - // Cache all children elements for quicker access. - this._V = {}; // vectorized markup; - children.forEach(function(child) { + // Deep clone the prototype and static properties objects. + // This prevents unexpected behavior where some properties are overwritten outside of this function. + var protoProps = args[0] && joint.util.assign({}, args[0]) || {}; + var staticProps = args[1] && joint.util.assign({}, args[1]) || {}; - var className = child.attr('class'); + // Need the real render method so that we can wrap it and call it later. + var renderFn = protoProps.render || (this.prototype && this.prototype.render) || null; - if (className) { - // Strip the joint class name prefix, if there is one. - className = joint.util.removeClassNamePrefix(className); - this._V[$.camelCase(className)] = child; + /* + Wrap the real render method so that: + .. `onRender` is always called. + .. `this` is always returned. + */ + protoProps.render = function() { + + if (renderFn) { + // Call the original render method. + renderFn.apply(this, arguments); } - }, this); + // Should always call onRender() method. + this.onRender(); - // Only the connection path is mandatory - if (!this._V.connection) throw new Error('link: no connection path in the markup'); + // Should always return itself. + return this; + }; - // partial rendering - this.renderTools(); - this.renderVertexMarkers(); - this.renderArrowheadMarkers(); + return Backbone.View.extend.call(this, protoProps, staticProps); + } +}); - this.vel.append(children); - // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox - // returns zero values) - this.renderLabels(); - // start watching the ends of the link for changes - this.watchSource(model, model.get('source')) - .watchTarget(model, model.get('target')) - .update(); +joint.dia.GraphCells = Backbone.Collection.extend({ - return this; - }, + cellNamespace: joint.shapes, - renderLabels: function() { + initialize: function(models, opt) { - var vLabels = this._V.labels; - if (!vLabels) { - return this; + // Set the optional namespace where all model classes are defined. + if (opt.cellNamespace) { + this.cellNamespace = opt.cellNamespace; } - vLabels.empty(); - - var model = this.model; - var labels = model.get('labels') || []; - var labelCache = this._labelCache = {}; - var labelsCount = labels.length; - if (labelsCount === 0) { - return this; - } + this.graph = opt.graph; + }, - var labelTemplate = joint.util.template(model.get('labelMarkup') || model.labelMarkup); - // This is a prepared instance of a vectorized SVGDOM node for the label element resulting from - // compilation of the labelTemplate. The purpose is that all labels will just `clone()` this - // node to create a duplicate. - var labelNodeInstance = V(labelTemplate()); + model: function(attrs, opt) { - for (var i = 0; i < labelsCount; i++) { + var collection = opt.collection; + var namespace = collection.cellNamespace; - var label = labels[i]; - var labelMarkup = label.markup; - // Cache label nodes so that the `updateLabels()` can just update the label node positions. - var vLabelNode = labelCache[i] = (labelMarkup) - ? V('g').append(V(labelMarkup)) - : labelNodeInstance.clone(); + // Find the model class in the namespace or use the default one. + var ModelClass = (attrs.type === 'link') + ? joint.dia.Link + : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; - vLabelNode - .addClass('label') - .attr('label-idx', i) - .appendTo(vLabels); + var cell = new ModelClass(attrs, opt); + // Add a reference to the graph. It is necessary to do this here because this is the earliest place + // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. + if (!opt.dry) { + cell.graph = collection.graph; } - this.updateLabels(); - - return this; + return cell; }, - updateLabels: function() { + // `comparator` makes it easy to sort cells based on their `z` index. + comparator: function(model) { - if (!this._V.labels) { - return this; - } + return model.get('z') || 0; + } +}); - var labels = this.model.get('labels') || []; - var canLabelMove = this.can('labelMove'); - for (var i = 0, n = labels.length; i < n; i++) { +joint.dia.Graph = Backbone.Model.extend({ - var vLabel = this._labelCache[i]; - var label = labels[i]; + _batches: {}, - vLabel.attr('cursor', (canLabelMove ? 'move' : 'default')); + initialize: function(attrs, opt) { - var labelAttrs = label.attrs; - if (!label.markup) { - // Default attributes to maintain backwards compatibility - labelAttrs = joint.util.merge({ - text: { - textAnchor: 'middle', - fontSize: 14, - fill: '#000000', - pointerEvents: 'none', - yAlignment: 'middle' - }, - rect: { - ref: 'text', - fill: '#ffffff', - rx: 3, - ry: 3, - refWidth: 1, - refHeight: 1, - refX: 0, - refY: 0 - } - }, labelAttrs); - } + opt = opt || {}; - this.updateDOMSubtreeAttributes(vLabel.node, labelAttrs, { - rootBBox: g.Rect(label.size) - }); - } + // Passing `cellModel` function in the options object to graph allows for + // setting models based on attribute objects. This is especially handy + // when processing JSON graphs that are in a different than JointJS format. + var cells = new joint.dia.GraphCells([], { + model: opt.cellModel, + cellNamespace: opt.cellNamespace, + graph: this + }); + Backbone.Model.prototype.set.call(this, 'cells', cells); - return this; - }, + // Make all the events fired in the `cells` collection available. + // to the outside world. + cells.on('all', this.trigger, this); - renderTools: function() { + // Backbone automatically doesn't trigger re-sort if models attributes are changed later when + // they're already in the collection. Therefore, we're triggering sort manually here. + this.on('change:z', this._sortOnChangeZ, this); - if (!this._V.linkTools) return this; + // `joint.dia.Graph` keeps an internal data structure (an adjacency list) + // for fast graph queries. All changes that affect the structure of the graph + // must be reflected in the `al` object. This object provides fast answers to + // questions such as "what are the neighbours of this node" or "what + // are the sibling links of this link". - // Tools are a group of clickable elements that manipulate the whole link. - // A good example of this is the remove tool that removes the whole link. - // Tools appear after hovering the link close to the `source` element/point of the link - // but are offset a bit so that they don't cover the `marker-arrowhead`. + // Outgoing edges per node. Note that we use a hash-table for the list + // of outgoing edges for a faster lookup. + // [node ID] -> Object [edge] -> true + this._out = {}; + // Ingoing edges per node. + // [node ID] -> Object [edge] -> true + this._in = {}; + // `_nodes` is useful for quick lookup of all the elements in the graph, without + // having to go through the whole cells array. + // [node ID] -> true + this._nodes = {}; + // `_edges` is useful for quick lookup of all the links in the graph, without + // having to go through the whole cells array. + // [edge ID] -> true + this._edges = {}; - var $tools = $(this._V.linkTools.node).empty(); - var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); - var tool = V(toolTemplate()); + cells.on('add', this._restructureOnAdd, this); + cells.on('remove', this._restructureOnRemove, this); + cells.on('reset', this._restructureOnReset, this); + cells.on('change:source', this._restructureOnChangeSource, this); + cells.on('change:target', this._restructureOnChangeTarget, this); + cells.on('remove', this._removeCell, this); + }, - $tools.append(tool.node); + _sortOnChangeZ: function() { - // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. - this._toolCache = tool; + this.get('cells').sort(); + }, - // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the - // link as well but only if the link is longer than `longLinkLength`. - if (this.options.doubleLinkTools) { + _restructureOnAdd: function(cell) { - var tool2; - if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { - toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); - tool2 = V(toolTemplate()); - } else { - tool2 = tool.clone(); + if (cell.isLink()) { + this._edges[cell.id] = true; + var source = cell.get('source'); + var target = cell.get('target'); + if (source.id) { + (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; } - - $tools.append(tool2.node); - this._tool2Cache = tool2; + if (target.id) { + (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; + } + } else { + this._nodes[cell.id] = true; } - - return this; }, - renderVertexMarkers: function() { - - if (!this._V.markerVertices) return this; + _restructureOnRemove: function(cell) { - var $markerVertices = $(this._V.markerVertices.node).empty(); + if (cell.isLink()) { + delete this._edges[cell.id]; + var source = cell.get('source'); + var target = cell.get('target'); + if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { + delete this._out[source.id][cell.id]; + } + if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { + delete this._in[target.id][cell.id]; + } + } else { + delete this._nodes[cell.id]; + } + }, - // A special markup can be given in the `properties.vertexMarkup` property. This might be handy - // if default styling (elements) are not desired. This makes it possible to use any - // SVG elements for .marker-vertex and .marker-vertex-remove tools. - var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); + _restructureOnReset: function(cells) { - joint.util.toArray(this.model.get('vertices')).forEach(function(vertex, idx) { + // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. + cells = cells.models; - $markerVertices.append(V(markupTemplate(joint.util.assign({ idx: idx }, vertex))).node); - }); + this._out = {}; + this._in = {}; + this._nodes = {}; + this._edges = {}; - return this; + cells.forEach(this._restructureOnAdd, this); }, - renderArrowheadMarkers: function() { + _restructureOnChangeSource: function(link) { - // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. - if (!this._V.markerArrowheads) return this; + var prevSource = link.previous('source'); + if (prevSource.id && this._out[prevSource.id]) { + delete this._out[prevSource.id][link.id]; + } + var source = link.get('source'); + if (source.id) { + (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; + } + }, - var $markerArrowheads = $(this._V.markerArrowheads.node); + _restructureOnChangeTarget: function(link) { - $markerArrowheads.empty(); + var prevTarget = link.previous('target'); + if (prevTarget.id && this._in[prevTarget.id]) { + delete this._in[prevTarget.id][link.id]; + } + var target = link.get('target'); + if (target.id) { + (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; + } + }, - // A special markup can be given in the `properties.vertexMarkup` property. This might be handy - // if default styling (elements) are not desired. This makes it possible to use any - // SVG elements for .marker-vertex and .marker-vertex-remove tools. - var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); + // Return all outbound edges for the node. Return value is an object + // of the form: [edge] -> true + getOutboundEdges: function(node) { - this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); - this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); + return (this._out && this._out[node]) || {}; + }, - $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); + // Return all inbound edges for the node. Return value is an object + // of the form: [edge] -> true + getInboundEdges: function(node) { - return this; + return (this._in && this._in[node]) || {}; }, - // Updating - //--------- - - // Default is to process the `attrs` object and set attributes on subelements based on the selectors. - update: function(model, attributes, opt) { + toJSON: function() { - opt = opt || {}; + // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. + // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. + var json = Backbone.Model.prototype.toJSON.apply(this, arguments); + json.cells = this.get('cells').toJSON(); + return json; + }, - if (!opt.updateConnectionOnly) { - // update SVG attributes defined by 'attrs/'. - this.updateDOMSubtreeAttributes(this.el, this.model.attr()); - } + fromJSON: function(json, opt) { - // update the link path, label position etc. - this.updateConnection(opt); - this.updateLabelPositions(); - this.updateToolsPosition(); - this.updateArrowheadMarkers(); + if (!json.cells) { - // Local perpendicular flag (as opposed to one defined on paper). - // Could be enabled inside a connector/router. It's valid only - // during the update execution. - this.options.perpendicular = null; - // Mark that postponed update has been already executed. - this.updatePostponed = false; + throw new Error('Graph JSON must contain cells array.'); + } - return this; + return this.set(json, opt); }, - updateConnection: function(opt) { + set: function(key, val, opt) { - opt = opt || {}; + var attrs; - var model = this.model; - var route; + // Handle both `key`, value and {key: value} style arguments. + if (typeof key === 'object') { + attrs = key; + opt = val; + } else { + (attrs = {})[key] = val; + } - if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { - // The link is being translated by an ancestor that will - // shift source point, target point and all vertices - // by an equal distance. - var tx = opt.tx || 0; - var ty = opt.ty || 0; - - route = this.route = joint.util.toArray(this.route).map(function(point) { - // translate point by point by delta translation - return g.point(point).offset(tx, ty); - }); - - // translate source and target connection and marker points. - this._translateConnectionPoints(tx, ty); - - } else { - // Necessary path finding - route = this.route = this.findRoute(model.get('vertices') || [], opt); - // finds all the connection points taking new vertices into account - this._findConnectionPoints(route); + // Make sure that `cells` attribute is handled separately via resetCells(). + if (attrs.hasOwnProperty('cells')) { + this.resetCells(attrs.cells, opt); + attrs = joint.util.omit(attrs, 'cells'); } - var pathData = this.getPathData(route); - - // The markup needs to contain a `.connection` - this._V.connection.attr('d', pathData); - this._V.connectionWrap && this._V.connectionWrap.attr('d', pathData); - - this._translateAndAutoOrientArrows(this._V.markerSource, this._V.markerTarget); + // The rest of the attributes are applied via original set method. + return Backbone.Model.prototype.set.call(this, attrs, opt); }, - _findConnectionPoints: function(vertices) { + clear: function(opt) { - // cache source and target points - var sourcePoint, targetPoint, sourceMarkerPoint, targetMarkerPoint; - var verticesArr = joint.util.toArray(vertices); + opt = joint.util.assign({}, opt, { clear: true }); - var firstVertex = verticesArr[0]; + var collection = this.get('cells'); - sourcePoint = this.getConnectionPoint( - 'source', this.model.get('source'), firstVertex || this.model.get('target') - ).round(); + if (collection.length === 0) return this; - var lastVertex = verticesArr[verticesArr.length - 1]; + this.startBatch('clear', opt); - targetPoint = this.getConnectionPoint( - 'target', this.model.get('target'), lastVertex || sourcePoint - ).round(); + // The elements come after the links. + var cells = collection.sortBy(function(cell) { + return cell.isLink() ? 1 : 2; + }); - // Move the source point by the width of the marker taking into account - // its scale around x-axis. Note that scale is the only transform that - // makes sense to be set in `.marker-source` attributes object - // as all other transforms (translate/rotate) will be replaced - // by the `translateAndAutoOrient()` function. - var cache = this._markerCache; + do { - if (this._V.markerSource) { + // Remove all the cells one by one. + // Note that all the links are removed first, so it's + // safe to remove the elements without removing the connected + // links first. + cells.shift().remove(opt); - cache.sourceBBox = cache.sourceBBox || this._V.markerSource.getBBox(); + } while (cells.length > 0); - sourceMarkerPoint = g.point(sourcePoint).move( - firstVertex || targetPoint, - cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 - ).round(); - } + this.stopBatch('clear'); - if (this._V.markerTarget) { + return this; + }, - cache.targetBBox = cache.targetBBox || this._V.markerTarget.getBBox(); + _prepareCell: function(cell, opt) { - targetMarkerPoint = g.point(targetPoint).move( - lastVertex || sourcePoint, - cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 - ).round(); + var attrs; + if (cell instanceof Backbone.Model) { + attrs = cell.attributes; + if (!cell.graph && (!opt || !opt.dry)) { + // An element can not be member of more than one graph. + // A cell stops being the member of the graph after it's explicitely removed. + cell.graph = this; + } + } else { + // In case we're dealing with a plain JS object, we have to set the reference + // to the `graph` right after the actual model is created. This happens in the `model()` function + // of `joint.dia.GraphCells`. + attrs = cell; } - // if there was no markup for the marker, use the connection point. - cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); - cache.targetPoint = targetMarkerPoint || targetPoint.clone(); + if (!joint.util.isString(attrs.type)) { + throw new TypeError('dia.Graph: cell type must be a string.'); + } - // make connection points public - this.sourcePoint = sourcePoint; - this.targetPoint = targetPoint; + return cell; }, - _translateConnectionPoints: function(tx, ty) { - - var cache = this._markerCache; + minZIndex: function() { - cache.sourcePoint.offset(tx, ty); - cache.targetPoint.offset(tx, ty); - this.sourcePoint.offset(tx, ty); - this.targetPoint.offset(tx, ty); + var firstCell = this.get('cells').first(); + return firstCell ? (firstCell.get('z') || 0) : 0; }, - updateLabelPositions: function() { - - if (!this._V.labels) return this; + maxZIndex: function() { - // This method assumes all the label nodes are stored in the `this._labelCache` hash table - // by their indexes in the `this.get('labels')` array. This is done in the `renderLabels()` method. + var lastCell = this.get('cells').last(); + return lastCell ? (lastCell.get('z') || 0) : 0; + }, - var labels = this.model.get('labels') || []; - if (!labels.length) return this; + addCell: function(cell, opt) { - var samples; - var connectionElement = this._V.connection.node; - var connectionLength = connectionElement.getTotalLength(); + if (Array.isArray(cell)) { - // Firefox returns connectionLength=NaN in odd cases (for bezier curves). - // In that case we won't update labels at all. - if (Number.isNaN(connectionLength)) { - return this; + return this.addCells(cell, opt); } - for (var idx = 0, n = labels.length; idx < n; idx++) { - - var label = labels[idx]; - var position = label.position; - var isPositionObject = joint.util.isObject(position); - var labelCoordinates; - - var distance = isPositionObject ? position.distance : position; - var offset = isPositionObject ? position.offset : { x: 0, y: 0 }; + if (cell instanceof Backbone.Model) { - if (Number.isFinite(distance)) { - distance = (distance > connectionLength) ? connectionLength : distance; // sanity check - distance = (distance < 0) ? connectionLength + distance : distance; - distance = (distance > 1) ? distance : connectionLength * distance; - } else { - distance = connectionLength / 2; + if (!cell.has('z')) { + cell.set('z', this.maxZIndex() + 1); } - labelCoordinates = connectionElement.getPointAtLength(distance); - - if (joint.util.isObject(offset)) { + } else if (cell.z === undefined) { - // Just offset the label by the x,y provided in the offset object. - labelCoordinates = g.point(labelCoordinates).offset(offset); + cell.z = this.maxZIndex() + 1; + } - } else if (Number.isFinite(offset)) { + this.get('cells').add(this._prepareCell(cell, opt), opt || {}); - if (!samples) { - samples = this._samples || this._V.connection.sample(this.options.sampleInterval); - } + return this; + }, - // Offset the label by the amount provided in `offset` to an either - // side of the link. + addCells: function(cells, opt) { - // 1. Find the closest sample & its left and right neighbours. - var minSqDistance = Infinity; - var closestSampleIndex, sample, sqDistance; - for (var i = 0, m = samples.length; i < m; i++) { - sample = samples[i]; - sqDistance = g.line(sample, labelCoordinates).squaredLength(); - if (sqDistance < minSqDistance) { - minSqDistance = sqDistance; - closestSampleIndex = i; - } - } - var prevSample = samples[closestSampleIndex - 1]; - var nextSample = samples[closestSampleIndex + 1]; + if (cells.length) { - // 2. Offset the label on the perpendicular line between - // the current label coordinate ("at `distance`") and - // the next sample. - var angle = 0; - if (nextSample) { - angle = g.point(labelCoordinates).theta(nextSample); - } else if (prevSample) { - angle = g.point(prevSample).theta(labelCoordinates); - } - labelCoordinates = g.point(labelCoordinates).offset(offset).rotate(labelCoordinates, angle - 90); - } + cells = joint.util.flattenDeep(cells); + opt.position = cells.length; - this._labelCache[idx].attr('transform', 'translate(' + labelCoordinates.x + ', ' + labelCoordinates.y + ')'); + this.startBatch('add'); + cells.forEach(function(cell) { + opt.position--; + this.addCell(cell, opt); + }, this); + this.stopBatch('add'); } return this; }, + // When adding a lot of cells, it is much more efficient to + // reset the entire cells collection in one go. + // Useful for bulk operations and optimizations. + resetCells: function(cells, opt) { - updateToolsPosition: function() { - - if (!this._V.linkTools) return this; + var preparedCells = joint.util.toArray(cells).map(function(cell) { + return this._prepareCell(cell, opt); + }, this); + this.get('cells').reset(preparedCells, opt); - // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. - // Note that the offset is hardcoded here. The offset should be always - // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking - // this up all the time would be slow. + return this; + }, - var scale = ''; - var offset = this.options.linkToolsOffset; - var connectionLength = this.getConnectionLength(); + removeCells: function(cells, opt) { - // Firefox returns connectionLength=NaN in odd cases (for bezier curves). - // In that case we won't update tools position at all. - if (!Number.isNaN(connectionLength)) { + if (cells.length) { - // If the link is too short, make the tools half the size and the offset twice as low. - if (connectionLength < this.options.shortLinkLength) { - scale = 'scale(.5)'; - offset /= 2; - } + this.startBatch('remove'); + joint.util.invoke(cells, 'remove', opt); + this.stopBatch('remove'); + } - var toolPosition = this.getPointAtLength(offset); + return this; + }, - this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + _removeCell: function(cell, collection, options) { - if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { + options = options || {}; - var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; + if (!options.clear) { + // Applications might provide a `disconnectLinks` option set to `true` in order to + // disconnect links when a cell is removed rather then removing them. The default + // is to remove all the associated links. + if (options.disconnectLinks) { - toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); - this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); - this._tool2Cache.attr('visibility', 'visible'); + this.disconnectLinks(cell, options); - } else if (this.options.doubleLinkTools) { + } else { - this._tool2Cache.attr('visibility', 'hidden'); + this.removeLinks(cell, options); } } + // Silently remove the cell from the cells collection. Silently, because + // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is + // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events + // would be triggered on the graph model. + this.get('cells').remove(cell, { silent: true }); - return this; + if (cell.graph === this) { + // Remove the element graph reference only if the cell is the member of this graph. + cell.graph = null; + } }, + // Get a cell by `id`. + getCell: function(id) { - updateArrowheadMarkers: function() { - - if (!this._V.markerArrowheads) return this; - - // getting bbox of an element with `display="none"` in IE9 ends up with access violation - if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; - - var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; - this._V.sourceArrowhead.scale(sx); - this._V.targetArrowhead.scale(sx); + return this.get('cells').get(id); + }, - this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); + getCells: function() { - return this; + return this.get('cells').toArray(); }, - // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, - // it stops listening on the previous one and starts listening to the new one. - createWatcher: function(endType) { + getElements: function() { + return Object.keys(this._nodes).map(this.getCell, this); + }, - // create handler for specific end type (source|target). - var onModelChange = function(endModel, opt) { - this.onEndModelChange(endType, endModel, opt); - }; + getLinks: function() { + return Object.keys(this._edges).map(this.getCell, this); + }, - function watchEndModel(link, end) { + getFirstCell: function() { - end = end || {}; + return this.get('cells').first(); + }, - var endModel = null; - var previousEnd = link.previous(endType) || {}; + getLastCell: function() { - if (previousEnd.id) { - this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); - } + return this.get('cells').last(); + }, - if (end.id) { - // If the observed model changes, it caches a new bbox and do the link update. - endModel = this.paper.getModelById(end.id); - this.listenTo(endModel, 'change', onModelChange); - } + // Get all inbound and outbound links connected to the cell `model`. + getConnectedLinks: function(model, opt) { - onModelChange.call(this, endModel, { cacheOnly: true }); + opt = opt || {}; - return this; + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; } - return watchEndModel; - }, - - onEndModelChange: function(endType, endModel, opt) { + // The final array of connected link models. + var links = []; + // Connected edges. This hash table ([edge] -> true) serves only + // for a quick lookup to check if we already added a link. + var edges = {}; - var doUpdate = !opt.cacheOnly; - var model = this.model; - var end = model.get(endType) || {}; - - if (endModel) { - - var selector = this.constructor.makeSelector(end); - var oppositeEndType = endType == 'source' ? 'target' : 'source'; - var oppositeEnd = model.get(oppositeEndType) || {}; - var oppositeSelector = oppositeEnd.id && this.constructor.makeSelector(oppositeEnd); + if (outbound) { + joint.util.forIn(this.getOutboundEdges(model.id), function(exists, edge) { + if (!edges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + if (inbound) { + joint.util.forIn(this.getInboundEdges(model.id), function(exists, edge) { + // Skip links that were already added. Those must be self-loop links + // because they are both inbound and outbond edges of the same element. + if (!edges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } - // Caching end models bounding boxes. - // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached - // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed - // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). - if (opt.handleBy === this.cid && selector == oppositeSelector) { + // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells + // and are not descendents themselves. + if (opt.deep) { - // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the - // second time now. There is no need to calculate bbox and find magnet element again. - // It was calculated already for opposite link end. - this[endType + 'BBox'] = this[oppositeEndType + 'BBox']; - this[endType + 'View'] = this[oppositeEndType + 'View']; - this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; + var embeddedCells = model.getEmbeddedCells({ deep: true }); + // In the first round, we collect all the embedded edges so that we can exclude + // them from the final result. + var embeddedEdges = {}; + embeddedCells.forEach(function(cell) { + if (cell.isLink()) { + embeddedEdges[cell.id] = true; + } + }); + embeddedCells.forEach(function(cell) { + if (cell.isLink()) return; + if (outbound) { + joint.util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { + if (!edges[edge] && !embeddedEdges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + if (inbound) { + joint.util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { + if (!edges[edge] && !embeddedEdges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + }, this); + } - } else if (opt.translateBy) { - // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. - // If `opt.translateBy` is an ID of the element that was originally translated. This allows us - // to just offset the cached bounding box by the translation instead of calculating the bounding - // box from scratch on every translate. + return links; + }, - var bbox = this[endType + 'BBox']; - bbox.x += opt.tx; - bbox.y += opt.ty; + getNeighbors: function(model, opt) { - } else { - // The slowest path, source/target could have been rotated or resized or any attribute - // that affects the bounding box of the view might have been changed. + opt = opt || {}; - var view = this.paper.findViewByModel(end.id); - var magnetElement = view.el.querySelector(selector); + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } - this[endType + 'BBox'] = view.getStrokeBBox(magnetElement); - this[endType + 'View'] = view; - this[endType + 'Magnet'] = magnetElement; - } + var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { - if (opt.handleBy === this.cid && opt.translateBy && - model.isEmbeddedIn(endModel) && - !joint.util.isEmpty(model.get('vertices'))) { - // Loop link whose element was translated and that has vertices (that need to be translated with - // the parent in which my element is embedded). - // If the link is embedded, has a loop and vertices and the end model - // has been translated, do not update yet. There are vertices still to be updated (change:vertices - // event will come in the next turn). - doUpdate = false; - } + var source = link.get('source'); + var target = link.get('target'); + var loop = link.hasLoop(opt); - if (!this.updatePostponed && oppositeEnd.id) { - // The update was not postponed (that can happen e.g. on the first change event) and the opposite - // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if - // we're reacting on change:source event, the oppositeEnd is the target model). + // Discard if it is a point, or if the neighbor was already added. + if (inbound && joint.util.has(source, 'id') && !res[source.id]) { - var oppositeEndModel = this.paper.getModelById(oppositeEnd.id); + var sourceElement = this.getCell(source.id); - // Passing `handleBy` flag via event option. - // Note that if we are listening to the same model for event 'change' twice. - // The same event will be handled by this method also twice. - if (end.id === oppositeEnd.id) { - // We're dealing with a loop link. Tell the handlers in the next turn that they should update - // the link instead of me. (We know for sure there will be a next turn because - // loop links react on at least two events: change on the source model followed by a change on - // the target model). - opt.handleBy = this.cid; + if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { + res[source.id] = sourceElement; } + } - if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { + // Discard if it is a point, or if the neighbor was already added. + if (outbound && joint.util.has(target, 'id') && !res[target.id]) { - // Here are two options: - // - Source and target are connected to the same model (not necessarily the same port). - // - Both end models are translated by the same ancestor. We know that opposite end - // model will be translated in the next turn as well. - // In both situations there will be more changes on the model that trigger an - // update. So there is no need to update the linkView yet. - this.updatePostponed = true; - doUpdate = false; + var targetElement = this.getCell(target.id); + + if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { + res[target.id] = targetElement; } } - } else { - - // the link end is a point ~ rect 1x1 - this[endType + 'BBox'] = g.rect(end.x || 0, end.y || 0, 1, 1); - this[endType + 'View'] = this[endType + 'Magnet'] = null; - } + return res; + }.bind(this), {}); - if (doUpdate) { - opt.updateConnectionOnly = true; - this.update(model, null, opt); - } + return joint.util.toArray(neighbors); }, - _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { - - // Make the markers "point" to their sticky points being auto-oriented towards - // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. - var route = joint.util.toArray(this.route); - if (sourceArrow) { - sourceArrow.translateAndAutoOrient( - this.sourcePoint, - route[0] || this.targetPoint, - this.paper.viewport - ); - } + getCommonAncestor: function(/* cells */) { - if (targetArrow) { - targetArrow.translateAndAutoOrient( - this.targetPoint, - route[route.length - 1] || this.sourcePoint, - this.paper.viewport - ); - } - }, + var cellsAncestors = Array.from(arguments).map(function(cell) { - removeVertex: function(idx) { + var ancestors = []; + var parentId = cell.get('parent'); - var vertices = joint.util.assign([], this.model.get('vertices')); + while (parentId) { - if (vertices && vertices.length) { + ancestors.push(parentId); + parentId = this.getCell(parentId).get('parent'); + } - vertices.splice(idx, 1); - this.model.set('vertices', vertices, { ui: true }); - } + return ancestors; - return this; - }, + }, this); - // This method ads a new vertex to the `vertices` array of `.connection`. This method - // uses a heuristic to find the index at which the new `vertex` should be placed at assuming - // the new vertex is somewhere on the path. - addVertex: function(vertex) { + cellsAncestors = cellsAncestors.sort(function(a, b) { + return a.length - b.length; + }); - // As it is very hard to find a correct index of the newly created vertex, - // a little heuristics is taking place here. - // The heuristics checks if length of the newly created - // path is lot more than length of the old path. If this is the case, - // new vertex was probably put into a wrong index. - // Try to put it into another index and repeat the heuristics again. + var commonAncestor = joint.util.toArray(cellsAncestors.shift()).find(function(ancestor) { + return cellsAncestors.every(function(cellAncestors) { + return cellAncestors.includes(ancestor); + }); + }); - var vertices = (this.model.get('vertices') || []).slice(); - // Store the original vertices for a later revert if needed. - var originalVertices = vertices.slice(); + return this.getCell(commonAncestor); + }, - // A `` element used to compute the length of the path during heuristics. - var path = this._V.connection.node.cloneNode(false); + // Find the whole branch starting at `element`. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. + getSuccessors: function(element, opt) { - // Length of the original path. - var originalPathLength = path.getTotalLength(); - // Current path length. - var pathLength; - // Tolerance determines the highest possible difference between the length - // of the old and new path. The number has been chosen heuristically. - var pathLengthTolerance = 20; - // Total number of vertices including source and target points. - var idx = vertices.length + 1; + opt = opt || {}; + var res = []; + // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. + this.search(element, function(el) { + if (el !== element) { + res.push(el); + } + }, joint.util.assign({}, opt, { outbound: true })); + return res; + }, - // Loop through all possible indexes and check if the difference between - // path lengths changes significantly. If not, the found index is - // most probably the right one. - while (idx--) { + // Clone `cells` returning an object that maps the original cell ID to the clone. The number + // of clones is exactly the same as the `cells.length`. + // This function simply clones all the `cells`. However, it also reconstructs + // all the `source/target` and `parent/embed` references within the `cells`. + // This is the main difference from the `cell.clone()` method. The + // `cell.clone()` method works on one single cell only. + // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` + // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. + // the source and target of the link `L2` is changed to point to `A2` and `B2`. + cloneCells: function(cells) { - vertices.splice(idx, 0, vertex); - V(path).attr('d', this.getPathData(this.findRoute(vertices))); + cells = joint.util.uniq(cells); - pathLength = path.getTotalLength(); + // A map of the form [original cell ID] -> [clone] helping + // us to reconstruct references for source/target and parent/embeds. + // This is also the returned value. + var cloneMap = joint.util.toArray(cells).reduce(function(map, cell) { + map[cell.id] = cell.clone(); + return map; + }, {}); - // Check if the path lengths changed significantly. - if (pathLength - originalPathLength > pathLengthTolerance) { + joint.util.toArray(cells).forEach(function(cell) { - // Revert vertices to the original array. The path length has changed too much - // so that the index was not found yet. - vertices = originalVertices.slice(); + var clone = cloneMap[cell.id]; + // assert(clone exists) - } else { + if (clone.isLink()) { + var source = clone.get('source'); + var target = clone.get('target'); + if (source.id && cloneMap[source.id]) { + // Source points to an element and the element is among the clones. + // => Update the source of the cloned link. + clone.prop('source/id', cloneMap[source.id].id); + } + if (target.id && cloneMap[target.id]) { + // Target points to an element and the element is among the clones. + // => Update the target of the cloned link. + clone.prop('target/id', cloneMap[target.id].id); + } + } - break; + // Find the parent of the original cell + var parent = cell.get('parent'); + if (parent && cloneMap[parent]) { + clone.set('parent', cloneMap[parent].id); } - } - if (idx === -1) { - // If no suitable index was found for such a vertex, make the vertex the first one. - idx = 0; - vertices.splice(idx, 0, vertex); - } + // Find the embeds of the original cell + var embeds = joint.util.toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) { + // Embedded cells that are not being cloned can not be carried + // over with other embedded cells. + if (cloneMap[embed]) { + newEmbeds.push(cloneMap[embed].id); + } + return newEmbeds; + }, []); - this.model.set('vertices', vertices, { ui: true }); + if (!joint.util.isEmpty(embeds)) { + clone.set('embeds', embeds); + } + }); - return idx; + return cloneMap; }, - // Send a token (an SVG element, usually a circle) along the connection path. - // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)` - // `opt.duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. - // `opt.directon` is optional and it determines whether the token goes from source to target or other way round (`reverse`) - // `callback` is optional and is a function to be called once the token reaches the target. - sendToken: function(token, opt, callback) { - - function onAnimationEnd(vToken, callback) { - return function() { - vToken.remove(); - if (typeof callback === 'function') { - callback(); - } - }; - } + // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). + // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. + // Return a map of the form: [original cell ID] -> [clone]. + cloneSubgraph: function(cells, opt) { - var duration, isReversed; - if (joint.util.isObject(opt)) { - duration = opt.duration; - isReversed = (opt.direction === 'reverse'); - } else { - // Backwards compatibility - duration = opt; - isReversed = false; - } + var subgraph = this.getSubgraph(cells, opt); + return this.cloneCells(subgraph); + }, - duration = duration || 1000; + // Return `cells` and all the connected links that connect cells in the `cells` array. + // If `opt.deep` is `true`, return all the cells including all their embedded cells + // and all the links that connect any of the returned cells. + // For example, for a single shallow element, the result is that very same element. + // For two elements connected with a link: `A --- L ---> B`, the result for + // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. + getSubgraph: function(cells, opt) { - var animationAttributes = { - dur: duration + 'ms', - repeatCount: 1, - calcMode: 'linear', - fill: 'freeze' - }; + opt = opt || {}; - if (isReversed) { - animationAttributes.keyPoints = '1;0'; - animationAttributes.keyTimes = '0;1'; - } + var subgraph = []; + // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. + var cellMap = {}; + var elements = []; + var links = []; - var vToken = V(token); - var vPath = this._V.connection; + joint.util.toArray(cells).forEach(function(cell) { + if (!cellMap[cell.id]) { + subgraph.push(cell); + cellMap[cell.id] = cell; + if (cell.isLink()) { + links.push(cell); + } else { + elements.push(cell); + } + } - vToken - .appendTo(this.paper.viewport) - .animateAlongPath(animationAttributes, vPath); + if (opt.deep) { + var embeds = cell.getEmbeddedCells({ deep: true }); + embeds.forEach(function(embed) { + if (!cellMap[embed.id]) { + subgraph.push(embed); + cellMap[embed.id] = embed; + if (embed.isLink()) { + links.push(embed); + } else { + elements.push(embed); + } + } + }); + } + }); - setTimeout(onAnimationEnd(vToken, callback), duration); - }, + links.forEach(function(link) { + // For links, return their source & target (if they are elements - not points). + var source = link.get('source'); + var target = link.get('target'); + if (source.id && !cellMap[source.id]) { + var sourceElement = this.getCell(source.id); + subgraph.push(sourceElement); + cellMap[sourceElement.id] = sourceElement; + elements.push(sourceElement); + } + if (target.id && !cellMap[target.id]) { + var targetElement = this.getCell(target.id); + subgraph.push(this.getCell(target.id)); + cellMap[targetElement.id] = targetElement; + elements.push(targetElement); + } + }, this); - findRoute: function(oldVertices) { + elements.forEach(function(element) { + // For elements, include their connected links if their source/target is in the subgraph; + var links = this.getConnectedLinks(element, opt); + links.forEach(function(link) { + var source = link.get('source'); + var target = link.get('target'); + if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { + subgraph.push(link); + cellMap[link.id] = link; + } + }); + }, this); - var namespace = joint.routers; - var router = this.model.get('router'); - var defaultRouter = this.paper.options.defaultRouter; + return subgraph; + }, - if (!router) { + // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. + getPredecessors: function(element, opt) { - if (this.model.get('manhattan')) { - // backwards compability - router = { name: 'orthogonal' }; - } else if (defaultRouter) { - router = defaultRouter; - } else { - return oldVertices; + opt = opt || {}; + var res = []; + // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. + this.search(element, function(el) { + if (el !== element) { + res.push(el); } - } + }, joint.util.assign({}, opt, { inbound: true })); + return res; + }, - var args = router.args || {}; - var routerFn = joint.util.isFunction(router) ? router : namespace[router.name]; + // Perform search on the graph. + // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. + // By setting `opt.inbound` to `true`, you can reverse the direction of the search. + // If `opt.deep` is `true`, take into account embedded elements too. + // `iteratee` is a function of the form `function(element) {}`. + // If `iteratee` explicitely returns `false`, the searching stops. + search: function(element, iteratee, opt) { - if (!joint.util.isFunction(routerFn)) { - throw new Error('unknown router: "' + router.name + '"'); + opt = opt || {}; + if (opt.breadthFirst) { + this.bfs(element, iteratee, opt); + } else { + this.dfs(element, iteratee, opt); } - - var newVertices = routerFn.call(this, oldVertices || [], args, this); - - return newVertices; }, - // Return the `d` attribute value of the `` element representing the link - // between `source` and `target`. - getPathData: function(vertices) { + // Breadth-first search. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). + // `iteratee` is a function of the form `function(element, distance) {}`. + // where `element` is the currently visited element and `distance` is the distance of that element + // from the root `element` passed the `bfs()`, i.e. the element we started the search from. + // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels + // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. + // If `iteratee` explicitely returns `false`, the searching stops. + bfs: function(element, iteratee, opt) { - var namespace = joint.connectors; - var connector = this.model.get('connector'); - var defaultConnector = this.paper.options.defaultConnector; + opt = opt || {}; + var visited = {}; + var distance = {}; + var queue = []; - if (!connector) { + queue.push(element); + distance[element.id] = 0; - // backwards compability - if (this.model.get('smooth')) { - connector = { name: 'smooth' }; - } else { - connector = defaultConnector || {}; + while (queue.length > 0) { + var next = queue.shift(); + if (!visited[next.id]) { + visited[next.id] = true; + if (iteratee(next, distance[next.id]) === false) return; + this.getNeighbors(next, opt).forEach(function(neighbor) { + distance[neighbor.id] = distance[next.id] + 1; + queue.push(neighbor); + }); } } - - var connectorFn = joint.util.isFunction(connector) ? connector : namespace[connector.name]; - var args = connector.args || {}; - - if (!joint.util.isFunction(connectorFn)) { - throw new Error('unknown connector: "' + connector.name + '"'); - } - - var pathData = connectorFn.call( - this, - this._markerCache.sourcePoint, // Note that the value is translated by the size - this._markerCache.targetPoint, // of the marker. (We'r not using this.sourcePoint) - vertices || (this.model.get('vertices') || {}), - args, // options - this - ); - - return pathData; }, - // Find a point that is the start of the connection. - // If `selectorOrPoint` is a point, then we're done and that point is the start of the connection. - // If the `selectorOrPoint` is an element however, we need to know a reference point (or element) - // that the link leads to in order to determine the start of the connection on the original element. - getConnectionPoint: function(end, selectorOrPoint, referenceSelectorOrPoint) { + // Depth-first search. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). + // `iteratee` is a function of the form `function(element, distance) {}`. + // If `iteratee` explicitely returns `false`, the search stops. + dfs: function(element, iteratee, opt, _visited, _distance) { - var spot; + opt = opt || {}; + var visited = _visited || {}; + var distance = _distance || 0; + if (iteratee(element, distance) === false) return; + visited[element.id] = true; - // If the `selectorOrPoint` (or `referenceSelectorOrPoint`) is `undefined`, the `source`/`target` of the link model is `undefined`. - // We want to allow this however so that one can create links such as `var link = new joint.dia.Link` and - // set the `source`/`target` later. - joint.util.isEmpty(selectorOrPoint) && (selectorOrPoint = { x: 0, y: 0 }); - joint.util.isEmpty(referenceSelectorOrPoint) && (referenceSelectorOrPoint = { x: 0, y: 0 }); + this.getNeighbors(element, opt).forEach(function(neighbor) { + if (!visited[neighbor.id]) { + this.dfs(neighbor, iteratee, opt, visited, distance + 1); + } + }, this); + }, - if (!selectorOrPoint.id) { + // Get all the roots of the graph. Time complexity: O(|V|). + getSources: function() { - // If the source is a point, we don't need a reference point to find the sticky point of connection. - spot = g.Point(selectorOrPoint); + var sources = []; + joint.util.forIn(this._nodes, function(exists, node) { + if (!this._in[node] || joint.util.isEmpty(this._in[node])) { + sources.push(this.getCell(node)); + } + }.bind(this)); + return sources; + }, - } else { + // Get all the leafs of the graph. Time complexity: O(|V|). + getSinks: function() { - // If the source is an element, we need to find a point on the element boundary that is closest - // to the reference point (or reference element). - // Get the bounding box of the spot relative to the paper viewport. This is necessary - // in order to follow paper viewport transformations (scale/rotate). - // `_sourceBbox` (`_targetBbox`) comes from `_sourceBboxUpdate` (`_sourceBboxUpdate`) - // method, it exists since first render and are automatically updated - var spotBBox = g.Rect(end === 'source' ? this.sourceBBox : this.targetBBox); + var sinks = []; + joint.util.forIn(this._nodes, function(exists, node) { + if (!this._out[node] || joint.util.isEmpty(this._out[node])) { + sinks.push(this.getCell(node)); + } + }.bind(this)); + return sinks; + }, - var reference; + // Return `true` if `element` is a root. Time complexity: O(1). + isSource: function(element) { - if (!referenceSelectorOrPoint.id) { + return !this._in[element.id] || joint.util.isEmpty(this._in[element.id]); + }, - // Reference was passed as a point, therefore, we're ready to find the sticky point of connection on the source element. - reference = g.Point(referenceSelectorOrPoint); + // Return `true` if `element` is a leaf. Time complexity: O(1). + isSink: function(element) { - } else { + return !this._out[element.id] || joint.util.isEmpty(this._out[element.id]); + }, - // Reference was passed as an element, therefore we need to find a point on the reference - // element boundary closest to the source element. - // Get the bounding box of the spot relative to the paper viewport. This is necessary - // in order to follow paper viewport transformations (scale/rotate). - var referenceBBox = g.Rect(end === 'source' ? this.targetBBox : this.sourceBBox); + // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. + isSuccessor: function(elementA, elementB) { - reference = referenceBBox.intersectionWithLineFromCenterToPoint(spotBBox.center()); - reference = reference || referenceBBox.center(); + var isSuccessor = false; + this.search(elementA, function(element) { + if (element === elementB && element !== elementA) { + isSuccessor = true; + return false; } + }, { outbound: true }); + return isSuccessor; + }, - var paperOptions = this.paper.options; - // If `perpendicularLinks` flag is set on the paper and there are vertices - // on the link, then try to find a connection point that makes the link perpendicular - // even though the link won't point to the center of the targeted object. - if (paperOptions.perpendicularLinks || this.options.perpendicular) { - - var nearestSide; - var spotOrigin = spotBBox.origin(); - var spotCorner = spotBBox.corner(); - - if (spotOrigin.y <= reference.y && reference.y <= spotCorner.y) { + // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. + isPredecessor: function(elementA, elementB) { - nearestSide = spotBBox.sideNearestToPoint(reference); - switch (nearestSide) { - case 'left': - spot = g.Point(spotOrigin.x, reference.y); - break; - case 'right': - spot = g.Point(spotCorner.x, reference.y); - break; - default: - spot = spotBBox.center(); - break; - } - - } else if (spotOrigin.x <= reference.x && reference.x <= spotCorner.x) { + var isPredecessor = false; + this.search(elementA, function(element) { + if (element === elementB && element !== elementA) { + isPredecessor = true; + return false; + } + }, { inbound: true }); + return isPredecessor; + }, - nearestSide = spotBBox.sideNearestToPoint(reference); - switch (nearestSide) { - case 'top': - spot = g.Point(reference.x, spotOrigin.y); - break; - case 'bottom': - spot = g.Point(reference.x, spotCorner.y); - break; - default: - spot = spotBBox.center(); - break; - } + // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. + // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` + // for more details. + // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. + // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. + isNeighbor: function(elementA, elementB, opt) { - } else { + opt = opt || {}; - // If there is no intersection horizontally or vertically with the object bounding box, - // then we fall back to the regular situation finding straight line (not perpendicular) - // between the object and the reference point. - spot = spotBBox.intersectionWithLineFromCenterToPoint(reference); - spot = spot || spotBBox.center(); - } + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } - } else if (paperOptions.linkConnectionPoint) { + var isNeighbor = false; - var view = (end === 'target') ? this.targetView : this.sourceView; - var magnet = (end === 'target') ? this.targetMagnet : this.sourceMagnet; + this.getConnectedLinks(elementA, opt).forEach(function(link) { - spot = paperOptions.linkConnectionPoint(this, view, magnet, reference, end); + var source = link.get('source'); + var target = link.get('target'); - } else { + // Discard if it is a point. + if (inbound && joint.util.has(source, 'id') && source.id === elementB.id) { + isNeighbor = true; + return false; + } - spot = spotBBox.intersectionWithLineFromCenterToPoint(reference); - spot = spot || spotBBox.center(); + // Discard if it is a point, or if the neighbor was already added. + if (outbound && joint.util.has(target, 'id') && target.id === elementB.id) { + isNeighbor = true; + return false; } - } + }); - return spot; + return isNeighbor; }, - // Public API - // ---------- + // Disconnect links connected to the cell `model`. + disconnectLinks: function(model, opt) { - getConnectionLength: function() { + this.getConnectedLinks(model).forEach(function(link) { - return this._V.connection.node.getTotalLength(); + link.set(link.get('source').id === model.id ? 'source' : 'target', { x: 0, y: 0 }, opt); + }); }, - getPointAtLength: function(length) { + // Remove links connected to the cell `model` completely. + removeLinks: function(model, opt) { - return this._V.connection.node.getPointAtLength(length); + joint.util.invoke(this.getConnectedLinks(model), 'remove', opt); }, - // Interaction. The controller part. - // --------------------------------- - - _beforeArrowheadMove: function() { - - this._z = this.model.get('z'); - this.model.toFront(); - - // Let the pointer propagate throught the link view elements so that - // the `evt.target` is another element under the pointer, not the link itself. - this.el.style.pointerEvents = 'none'; + // Find all elements at given point + findModelsFromPoint: function(p) { - if (this.paper.options.markAvailable) { - this._markAvailableMagnets(); - } + return this.getElements().filter(function(el) { + return el.getBBox().containsPoint(p); + }); }, - _afterArrowheadMove: function() { + // Find all elements in given area + findModelsInArea: function(rect, opt) { - if (this._z !== null) { - this.model.set('z', this._z, { ui: true }); - this._z = null; - } + rect = g.rect(rect); + opt = joint.util.defaults(opt || {}, { strict: false }); - // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation. - // Value `auto` doesn't work in IE9. We force to use `visiblePainted` instead. - // See `https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events`. - this.el.style.pointerEvents = 'visiblePainted'; + var method = opt.strict ? 'containsRect' : 'intersect'; - if (this.paper.options.markAvailable) { - this._unmarkAvailableMagnets(); - } + return this.getElements().filter(function(el) { + return rect[method](el.getBBox()); + }); }, - _createValidateConnectionArgs: function(arrowhead) { - // It makes sure the arguments for validateConnection have the following form: - // (source view, source magnet, target view, target magnet and link view) - var args = []; - - args[4] = arrowhead; - args[5] = this; - - var oppositeArrowhead; - var i = 0; - var j = 0; - - if (arrowhead === 'source') { - i = 2; - oppositeArrowhead = 'target'; - } else { - j = 2; - oppositeArrowhead = 'source'; - } - - var end = this.model.get(oppositeArrowhead); + // Find all elements under the given element. + findModelsUnderElement: function(element, opt) { - if (end.id) { - args[i] = this.paper.findViewByModel(end.id); - args[i + 1] = end.selector && args[i].el.querySelector(end.selector); - } + opt = joint.util.defaults(opt || {}, { searchBy: 'bbox' }); - function validateConnectionArgs(cellView, magnet) { - args[j] = cellView; - args[j + 1] = cellView.el === magnet ? undefined : magnet; - return args; - } + var bbox = element.getBBox(); + var elements = (opt.searchBy === 'bbox') + ? this.findModelsInArea(bbox) + : this.findModelsFromPoint(bbox[opt.searchBy]()); - return validateConnectionArgs; + // don't account element itself or any of its descendents + return elements.filter(function(el) { + return element.id !== el.id && !el.isEmbeddedIn(element); + }); }, - _markAvailableMagnets: function() { - function isMagnetAvailable(view, magnet) { - var paper = view.paper; - var validate = paper.options.validateConnection; - return validate.apply(paper, this._validateConnectionArgs(view, magnet)); - } + // Return bounding box of all elements. + getBBox: function(cells, opt) { - var paper = this.paper; - var elements = paper.model.getElements(); - this._marked = {}; + return this.getCellsBBox(cells || this.getElements(), opt); + }, - for (var i = 0, n = elements.length; i < n; i++) { - var view = elements[i].findView(paper); + // Return the bounding box of all cells in array provided. + // Links are being ignored. + getCellsBBox: function(cells, opt) { - if (!view) { - continue; + return joint.util.toArray(cells).reduce(function(memo, cell) { + if (cell.isLink()) return memo; + if (memo) { + return memo.union(cell.getBBox(opt)); + } else { + return cell.getBBox(opt); } + }, null); + }, - var magnets = Array.prototype.slice.call(view.el.querySelectorAll('[magnet]')); - if (view.el.getAttribute('magnet') !== 'false') { - // Element wrapping group is also a magnet - magnets.push(view.el); - } + translate: function(dx, dy, opt) { - var availableMagnets = magnets.filter(isMagnetAvailable.bind(this, view)); + // Don't translate cells that are embedded in any other cell. + var cells = this.getCells().filter(function(cell) { + return !cell.isEmbedded(); + }); - if (availableMagnets.length > 0) { - // highlight all available magnets - for (var j = 0, m = availableMagnets.length; j < m; j++) { - view.highlight(availableMagnets[j], { magnetAvailability: true }); - } - // highlight the entire view - view.highlight(null, { elementAvailability: true }); + joint.util.invoke(cells, 'translate', dx, dy, opt); - this._marked[view.model.id] = availableMagnets; - } - } + return this; }, - _unmarkAvailableMagnets: function() { + resize: function(width, height, opt) { - var markedKeys = Object.keys(this._marked); - var id; - var markedMagnets; + return this.resizeCells(width, height, this.getCells(), opt); + }, - for (var i = 0, n = markedKeys.length; i < n; i++) { - id = markedKeys[i]; - markedMagnets = this._marked[id]; + resizeCells: function(width, height, cells, opt) { - var view = this.paper.findViewByModel(id); - if (view) { - for (var j = 0, m = markedMagnets.length; j < m; j++) { - view.unhighlight(markedMagnets[j], { magnetAvailability: true }); - } - view.unhighlight(null, { elementAvailability: true }); - } + // `getBBox` method returns `null` if no elements provided. + // i.e. cells can be an array of links + var bbox = this.getCellsBBox(cells); + if (bbox) { + var sx = Math.max(width / bbox.width, 0); + var sy = Math.max(height / bbox.height, 0); + joint.util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); } - this._marked = null; + return this; }, - startArrowheadMove: function(end, opt) { + startBatch: function(name, data) { - opt = joint.util.defaults(opt || {}, { whenNotAllowed: 'revert' }); - // Allow to delegate events from an another view to this linkView in order to trigger arrowhead - // move without need to click on the actual arrowhead dom element. - this._action = 'arrowhead-move'; - this._whenNotAllowed = opt.whenNotAllowed; - this._arrowhead = end; - this._initialMagnet = this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null); - this._initialEnd = joint.util.assign({}, this.model.get(end)) || { x: 0, y: 0 }; - this._validateConnectionArgs = this._createValidateConnectionArgs(this._arrowhead); - this._beforeArrowheadMove(); - }, + data = data || {}; + this._batches[name] = (this._batches[name] || 0) + 1; - pointerdown: function(evt, x, y) { + return this.trigger('batch:start', joint.util.assign({}, data, { batchName: name })); + }, - joint.dia.CellView.prototype.pointerdown.apply(this, arguments); - this.notify('link:pointerdown', evt, x, y); + stopBatch: function(name, data) { - this._dx = x; - this._dy = y; + data = data || {}; + this._batches[name] = (this._batches[name] || 0) - 1; - // if are simulating pointerdown on a link during a magnet click, skip link interactions - if (evt.target.getAttribute('magnet') != null) return; + return this.trigger('batch:stop', joint.util.assign({}, data, { batchName: name })); + }, - var className = joint.util.removeClassNamePrefix(evt.target.getAttribute('class')); - var parentClassName = joint.util.removeClassNamePrefix(evt.target.parentNode.getAttribute('class')); - var labelNode; - if (parentClassName === 'label') { - className = parentClassName; - labelNode = evt.target.parentNode; - } else { - labelNode = evt.target; + hasActiveBatch: function(name) { + if (arguments.length === 0) { + return joint.util.toArray(this._batches).some(function(batches) { + return batches > 0; + }); + } + if (Array.isArray(name)) { + return name.some(function(name) { + return !!this._batches[name]; + }, this); } + return !!this._batches[name]; + } - switch (className) { +}, { - case 'marker-vertex': - if (this.can('vertexMove')) { - this._action = 'vertex-move'; - this._vertexIdx = evt.target.getAttribute('idx'); - } - break; + validations: { - case 'marker-vertex-remove': - case 'marker-vertex-remove-area': - if (this.can('vertexRemove')) { - this.removeVertex(evt.target.getAttribute('idx')); - } - break; + multiLinks: function(graph, link) { - case 'marker-arrowhead': - if (this.can('arrowheadMove')) { - this.startArrowheadMove(evt.target.getAttribute('end')); - } - break; + // Do not allow multiple links to have the same source and target. + var source = link.get('source'); + var target = link.get('target'); - case 'label': - if (this.can('labelMove')) { - this._action = 'label-move'; - this._labelIdx = parseInt(V(labelNode).attr('label-idx'), 10); - // Precalculate samples so that we don't have to do that - // over and over again while dragging the label. - this._samples = this._V.connection.sample(1); - this._linkLength = this._V.connection.node.getTotalLength(); - } - break; + if (source.id && target.id) { - default: + var sourceModel = link.getSourceElement(); + if (sourceModel) { - if (this.can('vertexAdd')) { + var connectedLinks = graph.getConnectedLinks(sourceModel, { outbound: true }); + var sameLinks = connectedLinks.filter(function(_link) { - // Store the index at which the new vertex has just been placed. - // We'll be update the very same vertex position in `pointermove()`. - this._vertexIdx = this.addVertex({ x: x, y: y }); - this._action = 'vertex-move'; - } - } - }, + var _source = _link.get('source'); + var _target = _link.get('target'); - pointermove: function(evt, x, y) { - - switch (this._action) { - - case 'vertex-move': - - var vertices = joint.util.assign([], this.model.get('vertices')); - vertices[this._vertexIdx] = { x: x, y: y }; - this.model.set('vertices', vertices, { ui: true }); - break; + return _source && _source.id === source.id && + (!_source.port || (_source.port === source.port)) && + _target && _target.id === target.id && + (!_target.port || (_target.port === target.port)); - case 'label-move': + }); - var dragPoint = { x: x, y: y }; - var samples = this._samples; - var minSqDistance = Infinity; - var closestSample; - var closestSampleIndex; - var p; - var sqDistance; - for (var i = 0, n = samples.length; i < n; i++) { - p = samples[i]; - sqDistance = g.line(p, dragPoint).squaredLength(); - if (sqDistance < minSqDistance) { - minSqDistance = sqDistance; - closestSample = p; - closestSampleIndex = i; + if (sameLinks.length > 1) { + return false; } } - var prevSample = samples[closestSampleIndex - 1]; - var nextSample = samples[closestSampleIndex + 1]; - var offset = 0; - if (prevSample && nextSample) { - offset = g.line(prevSample, nextSample).pointOffset(dragPoint); - } else if (prevSample) { - offset = g.line(prevSample, closestSample).pointOffset(dragPoint); - } else if (nextSample) { - offset = g.line(closestSample, nextSample).pointOffset(dragPoint); - } - - this.model.label(this._labelIdx, { - position: { - distance: closestSample.distance / this._linkLength, - offset: offset - } - }); - break; + } - case 'arrowhead-move': + return true; + }, - if (this.paper.options.snapLinks) { + linkPinning: function(graph, link) { + return link.source().id && link.target().id; + } + } - // checking view in close area of the pointer +}); - var r = this.paper.options.snapLinks.radius || 50; - var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); +joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); - if (this._closestView) { - this._closestView.unhighlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - } - this._closestView = this._closestEnd = null; +(function(joint, V, g, $, util) { - var distance; - var minDistance = Number.MAX_VALUE; - var pointer = g.point(x, y); + function setWrapper(attrName, dimension) { + return function(value, refBBox) { + var isValuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (isValuePercentage) { + value /= 100; + } - viewsInArea.forEach(function(view) { + var attrs = {}; + if (isFinite(value)) { + var attrValue = (isValuePercentage || value >= 0 && value <= 1) + ? value * refBBox[dimension] + : Math.max(value + refBBox[dimension], 0); + attrs[attrName] = attrValue; + } - // skip connecting to the element in case '.': { magnet: false } attribute present - if (view.el.getAttribute('magnet') !== 'false') { + return attrs; + }; + } - // find distance from the center of the model to pointer coordinates - distance = view.model.getBBox().center().distance(pointer); + function positionWrapper(axis, dimension, origin) { + return function(value, refBBox) { + var valuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (valuePercentage) { + value /= 100; + } - // the connection is looked up in a circle area by `distance < r` - if (distance < r && distance < minDistance) { + var delta; + if (isFinite(value)) { + var refOrigin = refBBox[origin](); + if (valuePercentage || value > 0 && value < 1) { + delta = refOrigin[axis] + refBBox[dimension] * value; + } else { + delta = refOrigin[axis] + value; + } + } - if (this.paper.options.validateConnection.apply( - this.paper, this._validateConnectionArgs(view, null) - )) { - minDistance = distance; - this._closestView = view; - this._closestEnd = { id: view.model.id }; - } - } - } + var point = g.Point(); + point[axis] = delta || 0; + return point; + }; + } - view.$('[magnet]').each(function(index, magnet) { + function offsetWrapper(axis, dimension, corner) { + return function(value, nodeBBox) { + var delta; + if (value === 'middle') { + delta = nodeBBox[dimension] / 2; + } else if (value === corner) { + delta = nodeBBox[dimension]; + } else if (isFinite(value)) { + // TODO: or not to do a breaking change? + delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value; + } else if (util.isPercentage(value)) { + delta = nodeBBox[dimension] * parseFloat(value) / 100; + } else { + delta = 0; + } - var bbox = V(magnet).getBBox({ target: this.paper.viewport }); + var point = g.Point(); + point[axis] = -(nodeBBox[axis] + delta); + return point; + }; + } - distance = pointer.distance({ - x: bbox.x + bbox.width / 2, - y: bbox.y + bbox.height / 2 - }); + function shapeWrapper(shapeConstructor, opt) { + var cacheName = 'joint-shape'; + var resetOffset = opt && opt.resetOffset; + return function(value, refBBox, node) { + var $node = $(node); + var cache = $node.data(cacheName); + if (!cache || cache.value !== value) { + // only recalculate if value has changed + var cachedShape = shapeConstructor(value); + cache = { + value: value, + shape: cachedShape, + shapeBBox: cachedShape.bbox() + }; + $node.data(cacheName, cache); + } - if (distance < r && distance < minDistance) { + var shape = cache.shape.clone(); + var shapeBBox = cache.shapeBBox.clone(); + var shapeOrigin = shapeBBox.origin(); + var refOrigin = refBBox.origin(); - if (this.paper.options.validateConnection.apply( - this.paper, this._validateConnectionArgs(view, magnet) - )) { - minDistance = distance; - this._closestView = view; - this._closestEnd = { - id: view.model.id, - selector: view.getSelector(magnet), - port: magnet.getAttribute('port') - }; - } - } + shapeBBox.x = refOrigin.x; + shapeBBox.y = refOrigin.y; - }.bind(this)); + var fitScale = refBBox.maxRectScaleToFit(shapeBBox, refOrigin); + // `maxRectScaleToFit` can give Infinity if width or height is 0 + var sx = (shapeBBox.width === 0 || refBBox.width === 0) ? 1 : fitScale.sx; + var sy = (shapeBBox.height === 0 || refBBox.height === 0) ? 1 : fitScale.sy; - }, this); + shape.scale(sx, sy, shapeOrigin); + if (resetOffset) { + shape.translate(-shapeOrigin.x, -shapeOrigin.y); + } - if (this._closestView) { - this._closestView.highlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - } + return shape; + }; + } - this.model.set(this._arrowhead, this._closestEnd || { x: x, y: y }, { ui: true }); + // `d` attribute for SVGPaths + function dWrapper(opt) { + function pathConstructor(value) { + return new g.Path(V.normalizePathData(value)); + } + var shape = shapeWrapper(pathConstructor, opt); + return function(value, refBBox, node) { + var path = shape(value, refBBox, node); + return { + d: path.serialize() + }; + }; + } - } else { + // `points` attribute for SVGPolylines and SVGPolygons + function pointsWrapper(opt) { + var shape = shapeWrapper(g.Polyline, opt); + return function(value, refBBox, node) { + var polyline = shape(value, refBBox, node); + return { + points: polyline.serialize() + }; + }; + } - // checking views right under the pointer + function atConnectionWrapper(method, opt) { + var zeroVector = new g.Point(1, 0); + return function(value) { + var p, angle; + var tangent = this[method](value); + if (tangent) { + angle = (opt.rotate) ? tangent.vector().vectorAngle(zeroVector) : 0; + p = tangent.start; + } else { + p = this.path.start; + angle = 0; + } + if (angle === 0) return { transform: 'translate(' + p.x + ',' + p.y + ')' }; + return { transform: 'translate(' + p.x + ',' + p.y + ') rotate(' + angle + ')' }; + } + } - // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. - // It holds the element when a touchstart triggered. - var target = (evt.type === 'mousemove') - ? evt.target - : document.elementFromPoint(evt.clientX, evt.clientY); + function isTextInUse(lineHeight, node, attrs) { + return (attrs.text !== undefined); + } - if (this._eventTarget !== target) { - // Unhighlight the previous view under pointer if there was one. - if (this._magnetUnderPointer) { - this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { - connecting: true - }); - } + function isLinkView() { + return this instanceof joint.dia.LinkView; + } - this._viewUnderPointer = this.paper.findView(target); - if (this._viewUnderPointer) { - // If we found a view that is under the pointer, we need to find the closest - // magnet based on the real target element of the event. - this._magnetUnderPointer = this._viewUnderPointer.findMagnet(target); - - if (this._magnetUnderPointer && this.paper.options.validateConnection.apply( - this.paper, - this._validateConnectionArgs(this._viewUnderPointer, this._magnetUnderPointer) - )) { - // If there was no magnet found, do not highlight anything and assume there - // is no view under pointer we're interested in reconnecting to. - // This can only happen if the overall element has the attribute `'.': { magnet: false }`. - if (this._magnetUnderPointer) { - this._viewUnderPointer.highlight(this._magnetUnderPointer, { - connecting: true - }); - } - } else { - // This type of connection is not valid. Disregard this magnet. - this._magnetUnderPointer = null; - } - } else { - // Make sure we'll unset previous magnet. - this._magnetUnderPointer = null; - } - } + function contextMarker(context) { + var marker = {}; + // Stroke + // The context 'fill' is disregared here. The usual case is to use the marker with a connection + // (for which 'fill' attribute is set to 'none'). + var stroke = context.stroke; + if (typeof stroke === 'string') { + marker['stroke'] = stroke; + marker['fill'] = stroke; + } + // Opacity + // Again the context 'fill-opacity' is ignored. + var strokeOpacity = context.strokeOpacity; + if (strokeOpacity === undefined) strokeOpacity = context['stroke-opacity']; + if (strokeOpacity === undefined) strokeOpacity = context.opacity + if (strokeOpacity !== undefined) { + marker['stroke-opacity'] = strokeOpacity; + marker['fill-opacity'] = strokeOpacity; + } + return marker; + } - this._eventTarget = target; + var attributesNS = joint.dia.attributes = { - this.model.set(this._arrowhead, { x: x, y: y }, { ui: true }); - } - break; - } + xlinkHref: { + set: 'xlink:href' + }, - this._dx = x; - this._dy = y; + xlinkShow: { + set: 'xlink:show' + }, - joint.dia.CellView.prototype.pointermove.apply(this, arguments); - this.notify('link:pointermove', evt, x, y); - }, + xlinkRole: { + set: 'xlink:role' + }, - pointerup: function(evt, x, y) { + xlinkType: { + set: 'xlink:type' + }, - if (this._action === 'label-move') { + xlinkArcrole: { + set: 'xlink:arcrole' + }, - this._samples = null; + xlinkTitle: { + set: 'xlink:title' + }, - } else if (this._action === 'arrowhead-move') { + xlinkActuate: { + set: 'xlink:actuate' + }, - var model = this.model; - var paper = this.paper; - var paperOptions = paper.options; - var arrowhead = this._arrowhead; - var initialEnd = this._initialEnd; - var magnetUnderPointer; - - if (paperOptions.snapLinks) { - - // Finish off link snapping. - // Everything except view unhighlighting was already done on pointermove. - if (this._closestView) { - this._closestView.unhighlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); + xmlSpace: { + set: 'xml:space' + }, - magnetUnderPointer = this._closestView.findMagnet(this._closestEnd.selector); - } + xmlBase: { + set: 'xml:base' + }, + + xmlLang: { + set: 'xml:lang' + }, - this._closestView = this._closestEnd = null; + preserveAspectRatio: { + set: 'preserveAspectRatio' + }, - } else { + requiredExtension: { + set: 'requiredExtension' + }, - var viewUnderPointer = this._viewUnderPointer; - magnetUnderPointer = this._magnetUnderPointer; + requiredFeatures: { + set: 'requiredFeatures' + }, - this._viewUnderPointer = null; - this._magnetUnderPointer = null; + systemLanguage: { + set: 'systemLanguage' + }, - if (magnetUnderPointer) { + externalResourcesRequired: { + set: 'externalResourceRequired' + }, - viewUnderPointer.unhighlight(magnetUnderPointer, { connecting: true }); - // Find a unique `selector` of the element under pointer that is a magnet. If the - // `this._magnetUnderPointer` is the root element of the `this._viewUnderPointer` itself, - // the returned `selector` will be `undefined`. That means we can directly pass it to the - // `source`/`target` attribute of the link model below. - var selector = viewUnderPointer.getSelector(magnetUnderPointer); - var port = magnetUnderPointer.getAttribute('port'); - var arrowheadValue = { id: viewUnderPointer.model.id }; - if (port != null) arrowheadValue.port = port; - if (selector != null) arrowheadValue.selector = selector; - model.set(arrowhead, arrowheadValue, { ui: true }); - } + filter: { + qualify: util.isPlainObject, + set: function(filter) { + return 'url(#' + this.paper.defineFilter(filter) + ')'; } + }, - // If the changed link is not allowed, revert to its previous state. - if (!paper.linkAllowed(this)) { + fill: { + qualify: util.isPlainObject, + set: function(fill) { + return 'url(#' + this.paper.defineGradient(fill) + ')'; + } + }, - switch (this._whenNotAllowed) { + stroke: { + qualify: util.isPlainObject, + set: function(stroke) { + return 'url(#' + this.paper.defineGradient(stroke) + ')'; + } + }, - case 'remove': - model.remove({ ui: true }); - break; + sourceMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), marker); + return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, - case 'revert': - default: - model.set(arrowhead, initialEnd, { ui: true }); - break; - } + targetMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), { 'transform': 'rotate(180)' }, marker); + return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, - } else { + vertexMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), marker); + return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, - // Reparent the link if embedding is enabled - if (paperOptions.embeddingMode && model.reparent()) { - // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). - this._z = null; + text: { + qualify: function(text, node, attrs) { + return !attrs.textWrap || !util.isPlainObject(attrs.textWrap); + }, + set: function(text, refBBox, node, attrs) { + var $node = $(node); + var cacheName = 'joint-text'; + var cache = $node.data(cacheName); + var textAttrs = joint.util.pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'textVerticalAnchor', 'eol'); + var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize']; + var textHash = JSON.stringify([text, textAttrs]); + // Update the text only if there was a change in the string + // or any of its attributes. + if (cache === undefined || cache !== textHash) { + // Chrome bug: + // Tspans positions defined as `em` are not updated + // when container `font-size` change. + if (fontSize) node.setAttribute('font-size', fontSize); + // Text Along Path Selector + var textPath = textAttrs.textPath; + if (util.isObject(textPath)) { + var pathSelector = textPath.selector; + if (typeof pathSelector === 'string') { + var pathNode = this.findBySelector(pathSelector)[0]; + if (pathNode instanceof SVGPathElement) { + textAttrs.textPath = util.assign({ 'xlink:href': '#' + pathNode.id }, textPath); + } + } + } + V(node).text('' + text, textAttrs); + $node.data(cacheName, textHash); } + } + }, - var currentEnd = model.prop(arrowhead); - var endChanged = currentEnd && !joint.dia.Link.endsEqual(initialEnd, currentEnd); - if (endChanged) { - - if (initialEnd.id) { - this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), this._initialMagnet, arrowhead); - } - if (currentEnd.id) { - this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), magnetUnderPointer, arrowhead); - } + textWrap: { + qualify: util.isPlainObject, + set: function(value, refBBox, node, attrs) { + // option `width` + var width = value.width || 0; + if (util.isPercentage(width)) { + refBBox.width *= parseFloat(width) / 100; + } else if (width <= 0) { + refBBox.width += width; + } else { + refBBox.width = width; + } + // option `height` + var height = value.height || 0; + if (util.isPercentage(height)) { + refBBox.height *= parseFloat(height) / 100; + } else if (height <= 0) { + refBBox.height += height; + } else { + refBBox.height = height; } + // option `text` + var text = value.text; + if (text === undefined) text = attr.text; + if (text !== undefined) { + var wrappedText = joint.util.breakText('' + text, refBBox, { + 'font-weight': attrs['font-weight'] || attrs.fontWeight, + 'font-size': attrs['font-size'] || attrs.fontSize, + 'font-family': attrs['font-family'] || attrs.fontFamily, + 'lineHeight': attrs.lineHeight + }, { + // Provide an existing SVG Document here + // instead of creating a temporary one over again. + svgDocument: this.paper.svg + }); + } + joint.dia.attributes.text.set.call(this, wrappedText, refBBox, node, attrs); } + }, - this._afterArrowheadMove(); - } - - this._action = null; - this._whenNotAllowed = null; - this._initialMagnet = null; - this._initialEnd = null; - this._validateConnectionArgs = null; - this._eventTarget = null; + title: { + qualify: function(title, node) { + // HTMLElement title is specified via an attribute (i.e. not an element) + return node instanceof SVGElement; + }, + set: function(title, refBBox, node) { + var $node = $(node); + var cacheName = 'joint-title'; + var cache = $node.data(cacheName); + if (cache === undefined || cache !== title) { + $node.data(cacheName, title); + // Generally element should be the first child element of its parent. + var firstChild = node.firstChild; + if (firstChild && firstChild.tagName.toUpperCase() === 'TITLE') { + // Update an existing title + firstChild.textContent = title; + } else { + // Create a new title + var titleNode = document.createElementNS(node.namespaceURI, 'title'); + titleNode.textContent = title; + node.insertBefore(titleNode, firstChild); + } + } + } + }, - this.notify('link:pointerup', evt, x, y); - joint.dia.CellView.prototype.pointerup.apply(this, arguments); - }, + lineHeight: { + qualify: isTextInUse + }, - mouseenter: function(evt) { + textVerticalAnchor: { + qualify: isTextInUse + }, - joint.dia.CellView.prototype.mouseenter.apply(this, arguments); - this.notify('link:mouseenter', evt); - }, + textPath: { + qualify: isTextInUse + }, - mouseleave: function(evt) { + annotations: { + qualify: isTextInUse + }, - joint.dia.CellView.prototype.mouseleave.apply(this, arguments); - this.notify('link:mouseleave', evt); - }, + // `port` attribute contains the `id` of the port that the underlying magnet represents. + port: { + set: function(port) { + return (port === null || port.id === undefined) ? port : port.id; + } + }, - event: function(evt, eventName, x, y) { + // `style` attribute is special in the sense that it sets the CSS style of the subelement. + style: { + qualify: util.isPlainObject, + set: function(styles, refBBox, node) { + $(node).css(styles); + } + }, - // Backwards compatibility - var linkTool = V(evt.target).findParentByClass('link-tool', this.el); - if (linkTool) { - // No further action to be executed - evt.stopPropagation(); - // Allow `interactive.useLinkTools=false` - if (this.can('useLinkTools')) { - if (eventName === 'remove') { - // Built-in remove event - this.model.remove({ ui: true }); - } else { - // link:options and other custom events inside the link tools - this.notify(eventName, evt, x, y); - } + html: { + set: function(html, refBBox, node) { + $(node).html(html + ''); } + }, - } else { + ref: { + // We do not set `ref` attribute directly on an element. + // The attribute itself does not qualify for relative positioning. + }, - joint.dia.CellView.prototype.event.apply(this, arguments); - } - } + // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width + // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box + // otherwise, `refX` is the left coordinate of the bounding box -}, { + refX: { + position: positionWrapper('x', 'width', 'origin') + }, - makeSelector: function(end) { + refY: { + position: positionWrapper('y', 'height', 'origin') + }, - var selector = '[model-id="' + end.id + '"]'; - // `port` has a higher precendence over `selector`. This is because the selector to the magnet - // might change while the name of the port can stay the same. - if (end.port) { - selector += ' [port="' + end.port + '"]'; - } else if (end.selector) { - selector += ' ' + end.selector; - } + // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom + // coordinate of the reference element. - return selector; - } + refDx: { + position: positionWrapper('x', 'width', 'corner') + }, -}); + refDy: { + position: positionWrapper('y', 'height', 'corner') + }, + // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to + // the reference element size + // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width + // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 -joint.dia.Paper = joint.mvc.View.extend({ + refWidth: { + set: setWrapper('width', 'width') + }, - className: 'paper', + refHeight: { + set: setWrapper('height', 'height') + }, - options: { + refRx: { + set: setWrapper('rx', 'width') + }, - width: 800, - height: 600, - origin: { x: 0, y: 0 }, // x,y coordinates in top-left corner - gridSize: 1, + refRy: { + set: setWrapper('ry', 'height') + }, - // Whether or not to draw the grid lines on the paper's DOM element. - // e.g drawGrid: true, drawGrid: { color: 'red', thickness: 2 } - drawGrid: false, + refRInscribed: { + set: (function(attrName) { + var widthFn = setWrapper(attrName, 'width'); + var heightFn = setWrapper(attrName, 'height'); + return function(value, refBBox) { + var fn = (refBBox.height > refBBox.width) ? widthFn : heightFn; + return fn(value, refBBox); + } + })('r') + }, - // Whether or not to draw the background on the paper's DOM element. - // e.g. background: { color: 'lightblue', image: '/paper-background.png', repeat: 'flip-xy' } - background: false, + refRCircumscribed: { + set: function(value, refBBox) { + var isValuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (isValuePercentage) { + value /= 100; + } - perpendicularLinks: false, - elementView: joint.dia.ElementView, - linkView: joint.dia.LinkView, - snapLinks: false, // false, true, { radius: value } + var diagonalLength = Math.sqrt((refBBox.height * refBBox.height) + (refBBox.width * refBBox.width)); - // When set to FALSE, an element may not have more than 1 link with the same source and target element. - multiLinks: true, + var rValue; + if (isFinite(value)) { + if (isValuePercentage || value >= 0 && value <= 1) rValue = value * diagonalLength; + else rValue = Math.max(value + diagonalLength, 0); + } - // For adding custom guard logic. - guard: function(evt, view) { + return { r: rValue }; + } + }, - // FALSE means the event isn't guarded. - return false; + refCx: { + set: setWrapper('cx', 'width') }, - highlighting: { - 'default': { - name: 'stroke', - options: { - padding: 3 - } - }, - magnetAvailability: { - name: 'addClass', - options: { - className: 'available-magnet' - } - }, - elementAvailability: { - name: 'addClass', - options: { - className: 'available-cell' - } - } + refCy: { + set: setWrapper('cy', 'height') }, - // Prevent the default context menu from being displayed. - preventContextMenu: true, - // Prevent the default action for blank:pointer<action>. - preventDefaultBlankAction: true, - // Restrict the translation of elements by given bounding box. - // Option accepts a boolean: - // true - the translation is restricted to the paper area - // false - no restrictions - // A method: - // restrictTranslate: function(elementView) { - // var parentId = elementView.model.get('parent'); - // return parentId && this.model.getCell(parentId).getBBox(); - // }, - // Or a bounding box: - // restrictTranslate: { x: 10, y: 10, width: 790, height: 590 } - restrictTranslate: false, - // Marks all available magnets with 'available-magnet' class name and all available cells with - // 'available-cell' class name. Marks them when dragging a link is started and unmark - // when the dragging is stopped. - markAvailable: false, + // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. + // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. - // Defines what link model is added to the graph after an user clicks on an active magnet. - // Value could be the Backbone.model or a function returning the Backbone.model - // defaultLink: function(elementView, magnet) { return condition ? new customLink1() : new customLink2() } - defaultLink: new joint.dia.Link, + xAlignment: { + offset: offsetWrapper('x', 'width', 'right') + }, - // A connector that is used by links with no connector defined on the model. - // e.g. { name: 'rounded', args: { radius: 5 }} or a function - defaultConnector: { name: 'normal' }, + // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. + // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. - // A router that is used by links with no router defined on the model. - // e.g. { name: 'oneSide', args: { padding: 10 }} or a function - defaultRouter: { name: 'normal' }, + yAlignment: { + offset: offsetWrapper('y', 'height', 'bottom') + }, - /* CONNECTING */ + resetOffset: { + offset: function(val, nodeBBox) { + return (val) + ? { x: -nodeBBox.x, y: -nodeBBox.y } + : { x: 0, y: 0 }; + } - // Check whether to add a new link to the graph when user clicks on an a magnet. - validateMagnet: function(cellView, magnet) { - return magnet.getAttribute('magnet') !== 'passive'; }, - // Check whether to allow or disallow the link connection while an arrowhead end (source/target) - // being changed. - validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { - return (end === 'target' ? cellViewT : cellViewS) instanceof joint.dia.ElementView; + refDResetOffset: { + set: dWrapper({ resetOffset: true }) }, - /* EMBEDDING */ + refDKeepOffset: { + set: dWrapper({ resetOffset: false }) + }, - // Enables embedding. Reparents the dragged element with elements under it and makes sure that - // all links and elements are visible taken the level of embedding into account. - embeddingMode: false, + refPointsResetOffset: { + set: pointsWrapper({ resetOffset: true }) + }, - // Check whether to allow or disallow the element embedding while an element being translated. - validateEmbedding: function(childView, parentView) { - // by default all elements can be in relation child-parent - return true; + refPointsKeepOffset: { + set: pointsWrapper({ resetOffset: false }) }, - // Determines the way how a cell finds a suitable parent when it's dragged over the paper. - // The cell with the highest z-index (visually on the top) will be choosen. - findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft' + // LinkView Attributes - // If enabled only the element on the very front is taken into account for the embedding. - // If disabled the elements under the dragged view are tested one by one - // (from front to back) until a valid parent found. - frontParentOnly: true, + connection: { + qualify: isLinkView, + set: function() { + return { d: this.getSerializedConnection() }; + } + }, - // Interactive flags. See online docs for the complete list of interactive flags. - interactive: { - labelMove: false + atConnectionLengthKeepGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtLength', { rotate: true }) }, - // When set to true the links can be pinned to the paper. - // i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 }; - linkPinning: true, + atConnectionLengthIgnoreGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtLength', { rotate: false }) + }, - // Allowed number of mousemove events after which the pointerclick event will be still triggered. - clickThreshold: 0, + atConnectionRatioKeepGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtRatio', { rotate: true }) + }, - // Number of required mousemove events before the first pointermove event will be triggered. - moveThreshold: 0, + atConnectionRatioIgnoreGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtRatio', { rotate: false }) + } + }; - // The namespace, where all the cell views are defined. - cellViewNamespace: joint.shapes, + // Aliases + attributesNS.refR = attributesNS.refRInscribed; + attributesNS.refD = attributesNS.refDResetOffset; + attributesNS.refPoints = attributesNS.refPointsResetOffset; + attributesNS.atConnectionLength = attributesNS.atConnectionLengthKeepGradient; + attributesNS.atConnectionRatio = attributesNS.atConnectionRatioKeepGradient; - // The namespace, where all the cell views are defined. - highlighterNamespace: joint.highlighters - }, + // This allows to combine both absolute and relative positioning + // refX: 50%, refX2: 20 + attributesNS.refX2 = attributesNS.refX; + attributesNS.refY2 = attributesNS.refY; - events: { + // Aliases for backwards compatibility + attributesNS['ref-x'] = attributesNS.refX; + attributesNS['ref-y'] = attributesNS.refY; + attributesNS['ref-dy'] = attributesNS.refDy; + attributesNS['ref-dx'] = attributesNS.refDx; + attributesNS['ref-width'] = attributesNS.refWidth; + attributesNS['ref-height'] = attributesNS.refHeight; + attributesNS['x-alignment'] = attributesNS.xAlignment; + attributesNS['y-alignment'] = attributesNS.yAlignment; - 'mousedown': 'pointerdown', - 'dblclick': 'mousedblclick', - 'click': 'mouseclick', - 'touchstart': 'pointerdown', - 'touchend': 'mouseclick', - 'touchmove': 'pointermove', - 'mousemove': 'pointermove', - 'mouseover .joint-cell': 'cellMouseover', - 'mouseout .joint-cell': 'cellMouseout', - 'contextmenu': 'contextmenu', - 'mousewheel': 'mousewheel', - 'DOMMouseScroll': 'mousewheel', - 'mouseenter .joint-cell': 'cellMouseenter', - 'mouseleave .joint-cell': 'cellMouseleave', - 'mousedown .joint-cell [event]': 'cellEvent', - 'touchstart .joint-cell [event]': 'cellEvent' - }, +})(joint, V, g, $, joint.util); - _highlights: {}, +(function(joint, util) { - init: function() { + var ToolView = joint.mvc.View.extend({ + name: null, + tagName: 'g', + className: 'tool', + svgElement: true, + _visible: true, - joint.util.bindAll(this, 'pointerup'); + init: function() { + var name = this.name; + if (name) this.vel.attr('data-tool-name', name); + }, - var model = this.model = this.options.model || new joint.dia.Graph; + configure: function(view, toolsView) { + this.relatedView = view; + this.paper = view.paper; + this.parentView = toolsView; + this.simulateRelatedView(this.el); + return this; + }, - this.setGrid(this.options.drawGrid); - this.cloneOptions(); - this.render(); - this.setDimensions(); + simulateRelatedView: function(el) { + if (el) el.setAttribute('model-id', this.relatedView.model.id); + }, - this.listenTo(model, 'add', this.onCellAdded) - .listenTo(model, 'remove', this.removeView) - .listenTo(model, 'reset', this.resetViews) - .listenTo(model, 'sort', this._onSort) - .listenTo(model, 'batch:stop', this._onBatchStop); + getName: function() { + return this.name; + }, - this.on('cell:highlight', this.onCellHighlight) - .on('cell:unhighlight', this.onCellUnhighlight) - .on('scale translate', this.update); + show: function() { + this.el.style.display = ''; + this._visible = true; + }, - // Hold the value when mouse has been moved: when mouse moved, no click event will be triggered. - this._mousemoved = 0; - // Hash of all cell views. - this._views = {}; - // Reference to the paper owner document - this.$document = $(this.el.ownerDocument); - }, + hide: function() { + this.el.style.display = 'none'; + this._visible = false; + }, - cloneOptions: function() { + isVisible: function() { + return !!this._visible; + }, - var options = this.options; + focus: function() { + var opacity = this.options.focusOpacity; + if (isFinite(opacity)) this.el.style.opacity = opacity; + this.parentView.focusTool(this); + }, - // This is a fix for the case where two papers share the same options. - // Changing origin.x for one paper would change the value of origin.x for the other. - // This prevents that behavior. - options.origin = joint.util.assign({}, options.origin); - options.defaultConnector = joint.util.assign({}, options.defaultConnector); - // Return the default highlighting options into the user specified options. - options.highlighting = joint.util.defaultsDeep( - {}, - options.highlighting, - this.constructor.prototype.options.highlighting - ); - }, + blur: function() { + this.el.style.opacity = ''; + this.parentView.blurTool(this); + }, - bindDocumentEvents: function() { - var eventNS = this.getEventNamespace(); - this.$document.on('mouseup' + eventNS + ' touchend' + eventNS, this.pointerup); - }, + update: function() { + // to be overriden + } + }); - unbindDocumentEvents: function() { - this.$document.off(this.getEventNamespace()); - }, + var ToolsView = joint.mvc.View.extend({ + tagName: 'g', + className: 'tools', + svgElement: true, + tools: null, + options: { + tools: null, + relatedView: null, + name: null, + component: false + }, - render: function() { + configure: function(options) { + options = util.assign(this.options, options); + var tools = options.tools; + if (!Array.isArray(tools)) return this; + var relatedView = options.relatedView; + if (!(relatedView instanceof joint.dia.CellView)) return this; + var views = this.tools = []; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (!(tool instanceof ToolView)) continue; + tool.configure(relatedView, this); + tool.render(); + this.vel.append(tool.el); + views.push(tool); + } + return this; + }, - this.$el.empty(); + getName: function() { + return this.options.name; + }, - this.svg = V('svg').attr({ width: '100%', height: '100%' }).node; - this.viewport = V('g').addClass(joint.util.addClassNamePrefix('viewport')).node; - this.defs = V('defs').node; + update: function(opt) { - // Append `<defs>` element to the SVG document. This is useful for filters and gradients. - V(this.svg).append([this.viewport, this.defs]); + opt || (opt = {}); + var tools = this.tools; + if (!tools) return; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (opt.tool !== tool.cid && tool.isVisible()) { + tool.update(); + } + } + return this; + }, - this.$background = $('<div/>').addClass(joint.util.addClassNamePrefix('paper-background')); - if (this.options.background) { - this.drawBackground(this.options.background); - } + focusTool: function(focusedTool) { - this.$grid = $('<div/>').addClass(joint.util.addClassNamePrefix('paper-grid')); - if (this.options.drawGrid) { - this.drawGrid(); - } + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (focusedTool === tool) { + tool.show(); + } else { + tool.hide(); + } + } + return this; + }, - this.$el.append(this.$background, this.$grid, this.svg); + blurTool: function(blurredTool) { + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (tool !== blurredTool && !tool.isVisible()) { + tool.show(); + tool.update(); + } + } + return this; + }, - return this; - }, + hide: function() { + return this.focusTool(null); + }, - update: function() { + show: function() { + return this.blurTool(null); + }, - if (this.options.drawGrid) { - this.drawGrid(); - } + onRemove: function() { - if (this._background) { - this.updateBackgroundImage(this._background); + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + tools[i].remove(); + } + this.tools = null; + }, + + mount: function() { + var options = this.options; + var relatedView = options.relatedView; + if (relatedView) { + var container = (options.component) ? relatedView.el : relatedView.paper.tools; + container.appendChild(this.el); + } + return this; } - return this; - }, + }); - // For storing the current transformation matrix (CTM) of the paper's viewport. - _viewportMatrix: null, - // For verifying whether the CTM is up-to-date. The viewport transform attribute - // could have been manipulated directly. - _viewportTransformString: null, + joint.dia.ToolsView = ToolsView; + joint.dia.ToolView = ToolView; - matrix: function(ctm) { +})(joint, joint.util); - var viewport = this.viewport; - // Getter: - if (ctm === undefined) { +// joint.dia.Cell base model. +// -------------------------- - var transformString = viewport.getAttribute('transform'); +joint.dia.Cell = Backbone.Model.extend({ - if ((this._viewportTransformString || null) === transformString) { - // It's ok to return the cached matrix. The transform attribute has not changed since - // the matrix was stored. - ctm = this._viewportMatrix; - } else { - // The viewport transform attribute has changed. Measure the matrix and cache again. - ctm = viewport.getCTM(); - this._viewportMatrix = ctm; - this._viewportTransformString = transformString; - } + // This is the same as Backbone.Model with the only difference that is uses joint.util.merge + // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. + constructor: function(attributes, options) { - // Clone the cached current transformation matrix. - // If no matrix previously stored the identity matrix is returned. - return V.createSVGMatrix(ctm); + var defaults; + var attrs = attributes || {}; + this.cid = joint.util.uniqueId('c'); + this.attributes = {}; + if (options && options.collection) this.collection = options.collection; + if (options && options.parse) attrs = this.parse(attrs, options) || {}; + if ((defaults = joint.util.result(this, 'defaults'))) { + //<custom code> + // Replaced the call to _.defaults with joint.util.merge. + attrs = joint.util.merge({}, defaults, attrs); + //</custom code> } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }, - // Setter: - ctm = V.createSVGMatrix(ctm); - V(viewport).transform(ctm, { absolute: true }); - this._viewportMatrix = ctm; - this._viewportTransformString = viewport.getAttribute('transform'); + translate: function(dx, dy, opt) { - return this; + throw new Error('Must define a translate() method.'); }, - clientMatrix: function() { + toJSON: function() { - return V.createSVGMatrix(this.viewport.getScreenCTM()); - }, + var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; + var attrs = this.attributes.attrs; + var finalAttrs = {}; - _onSort: function() { - if (!this.model.hasActiveBatch('add')) { - this.sortViews(); - } - }, + // Loop through all the attributes and + // omit the default attributes as they are implicitly reconstructable by the cell 'type'. + joint.util.forIn(attrs, function(attr, selector) { - _onBatchStop: function(data) { - var name = data && data.batchName; - if (name === 'add' && !this.model.hasActiveBatch('add')) { - this.sortViews(); - } - }, + var defaultAttr = defaultAttrs[selector]; - onRemove: function() { + joint.util.forIn(attr, function(value, name) { - //clean up all DOM elements/views to prevent memory leaks - this.removeViews(); - this.unbindDocumentEvents(); - }, + // attr is mainly flat though it might have one more level (consider the `style` attribute). + // Check if the `value` is object and if yes, go one level deep. + if (joint.util.isObject(value) && !Array.isArray(value)) { - setDimensions: function(width, height) { + joint.util.forIn(value, function(value2, name2) { - width = this.options.width = width || this.options.width; - height = this.options.height = height || this.options.height; + if (!defaultAttr || !defaultAttr[name] || !joint.util.isEqual(defaultAttr[name][name2], value2)) { - this.$el.css({ - width: Math.round(width), - height: Math.round(height) - }); + finalAttrs[selector] = finalAttrs[selector] || {}; + (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; + } + }); - this.trigger('resize', width, height); - }, + } else if (!defaultAttr || !joint.util.isEqual(defaultAttr[name], value)) { + // `value` is not an object, default attribute for such a selector does not exist + // or it is different than the attribute value set on the model. - setOrigin: function(ox, oy) { + finalAttrs[selector] = finalAttrs[selector] || {}; + finalAttrs[selector][name] = value; + } + }); + }); - return this.translate(ox || 0, oy || 0, { absolute: true }); - }, + var attributes = joint.util.cloneDeep(joint.util.omit(this.attributes, 'attrs')); + //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); + attributes.attrs = finalAttrs; - // Expand/shrink the paper to fit the content. Snap the width/height to the grid - // defined in `gridWidth`, `gridHeight`. `padding` adds to the resulting width/height of the paper. - // When options { fitNegative: true } it also translates the viewport in order to make all - // the content visible. - fitToContent: function(gridWidth, gridHeight, padding, opt) { // alternatively function(opt) + return attributes; + }, - if (joint.util.isObject(gridWidth)) { - // first parameter is an option object - opt = gridWidth; - gridWidth = opt.gridWidth || 1; - gridHeight = opt.gridHeight || 1; - padding = opt.padding || 0; + initialize: function(options) { - } else { + if (!options || !options.id) { - opt = opt || {}; - gridWidth = gridWidth || 1; - gridHeight = gridHeight || 1; - padding = padding || 0; + this.set('id', joint.util.uuid(), { silent: true }); } - padding = joint.util.normalizeSides(padding); - - // Calculate the paper size to accomodate all the graph's elements. - var bbox = V(this.viewport).getBBox(); + this._transitionIds = {}; - var currentScale = this.scale(); - var currentTranslate = this.translate(); - - bbox.x *= currentScale.sx; - bbox.y *= currentScale.sy; - bbox.width *= currentScale.sx; - bbox.height *= currentScale.sy; + // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. + this.processPorts(); + this.on('change:attrs', this.processPorts, this); + }, - var calcWidth = Math.max(Math.ceil((bbox.width + bbox.x) / gridWidth), 1) * gridWidth; - var calcHeight = Math.max(Math.ceil((bbox.height + bbox.y) / gridHeight), 1) * gridHeight; + /** + * @deprecated + */ + processPorts: function() { - var tx = 0; - var ty = 0; + // Whenever `attrs` changes, we extract ports from the `attrs` object and store it + // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` + // set to that port, we remove those links as well (to follow the same behaviour as + // with a removed element). - if ((opt.allowNewOrigin == 'negative' && bbox.x < 0) || (opt.allowNewOrigin == 'positive' && bbox.x >= 0) || opt.allowNewOrigin == 'any') { - tx = Math.ceil(-bbox.x / gridWidth) * gridWidth; - tx += padding.left; - calcWidth += tx; - } + var previousPorts = this.ports; - if ((opt.allowNewOrigin == 'negative' && bbox.y < 0) || (opt.allowNewOrigin == 'positive' && bbox.y >= 0) || opt.allowNewOrigin == 'any') { - ty = Math.ceil(-bbox.y / gridHeight) * gridHeight; - ty += padding.top; - calcHeight += ty; - } + // Collect ports from the `attrs` object. + var ports = {}; + joint.util.forIn(this.get('attrs'), function(attrs, selector) { - calcWidth += padding.right; - calcHeight += padding.bottom; + if (attrs && attrs.port) { - // Make sure the resulting width and height are greater than minimum. - calcWidth = Math.max(calcWidth, opt.minWidth || 0); - calcHeight = Math.max(calcHeight, opt.minHeight || 0); + // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). + if (attrs.port.id !== undefined) { + ports[attrs.port.id] = attrs.port; + } else { + ports[attrs.port] = { id: attrs.port }; + } + } + }); - // Make sure the resulting width and height are lesser than maximum. - calcWidth = Math.min(calcWidth, opt.maxWidth || Number.MAX_VALUE); - calcHeight = Math.min(calcHeight, opt.maxHeight || Number.MAX_VALUE); + // Collect ports that have been removed (compared to the previous ports) - if any. + // Use hash table for quick lookup. + var removedPorts = {}; + joint.util.forIn(previousPorts, function(port, id) { - var dimensionChange = calcWidth != this.options.width || calcHeight != this.options.height; - var originChange = tx != currentTranslate.tx || ty != currentTranslate.ty; + if (!ports[id]) removedPorts[id] = true; + }); - // Change the dimensions only if there is a size discrepency or an origin change - if (originChange) { - this.translate(tx, ty); - } - if (dimensionChange) { - this.setDimensions(calcWidth, calcHeight); - } - }, + // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. + if (this.graph && !joint.util.isEmpty(removedPorts)) { - scaleContentToFit: function(opt) { + var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); + inboundLinks.forEach(function(link) { - var contentBBox = this.getContentBBox(); + if (removedPorts[link.get('target').port]) link.remove(); + }); - if (!contentBBox.width || !contentBBox.height) return; + var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); + outboundLinks.forEach(function(link) { - opt = opt || {}; + if (removedPorts[link.get('source').port]) link.remove(); + }); + } - joint.util.defaults(opt, { - padding: 0, - preserveAspectRatio: true, - scaleGrid: null, - minScale: 0, - maxScale: Number.MAX_VALUE - //minScaleX - //minScaleY - //maxScaleX - //maxScaleY - //fittingBBox - }); + // Update the `ports` object. + this.ports = ports; + }, - var padding = opt.padding; + remove: function(opt) { - var minScaleX = opt.minScaleX || opt.minScale; - var maxScaleX = opt.maxScaleX || opt.maxScale; - var minScaleY = opt.minScaleY || opt.minScale; - var maxScaleY = opt.maxScaleY || opt.maxScale; + opt = opt || {}; - var fittingBBox; - if (opt.fittingBBox) { - fittingBBox = opt.fittingBBox; - } else { - var currentTranslate = this.translate(); - fittingBBox = { - x: currentTranslate.tx, - y: currentTranslate.ty, - width: this.options.width, - height: this.options.height - }; + // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. + var graph = this.graph; + if (graph) { + graph.startBatch('remove'); } - fittingBBox = g.rect(fittingBBox).moveAndExpand({ - x: padding, - y: padding, - width: -2 * padding, - height: -2 * padding - }); + // First, unembed this cell from its parent cell if there is one. + var parentCell = this.getParentCell(); + if (parentCell) parentCell.unembed(this); - var currentScale = this.scale(); + joint.util.invoke(this.getEmbeddedCells(), 'remove', opt); - var newSx = fittingBBox.width / contentBBox.width * currentScale.sx; - var newSy = fittingBBox.height / contentBBox.height * currentScale.sy; + this.trigger('remove', this, this.collection, opt); - if (opt.preserveAspectRatio) { - newSx = newSy = Math.min(newSx, newSy); + if (graph) { + graph.stopBatch('remove'); } - // snap scale to a grid - if (opt.scaleGrid) { + return this; + }, - var gridSize = opt.scaleGrid; + toFront: function(opt) { - newSx = gridSize * Math.floor(newSx / gridSize); - newSy = gridSize * Math.floor(newSy / gridSize); - } + var graph = this.graph; + if (graph) { - // scale min/max boundaries - newSx = Math.min(maxScaleX, Math.max(minScaleX, newSx)); - newSy = Math.min(maxScaleY, Math.max(minScaleY, newSy)); + opt = opt || {}; - this.scale(newSx, newSy); + var z = graph.maxZIndex(); - var contentTranslation = this.getContentBBox(); + var cells; - var newOx = fittingBBox.x - contentTranslation.x; - var newOy = fittingBBox.y - contentTranslation.y; + if (opt.deep) { + cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + cells.unshift(this); + } else { + cells = [this]; + } - this.translate(newOx, newOy); - }, + z = z - cells.length + 1; - getContentBBox: function() { + var collection = graph.get('cells'); + var shouldUpdate = (collection.indexOf(this) !== (collection.length - cells.length)); + if (!shouldUpdate) { + shouldUpdate = cells.some(function(cell, index) { + return cell.get('z') !== z + index; + }); + } - var crect = this.viewport.getBoundingClientRect(); + if (shouldUpdate) { + this.startBatch('to-front'); - // Using Screen CTM was the only way to get the real viewport bounding box working in both - // Google Chrome and Firefox. - var clientCTM = this.clientMatrix(); + z = z + cells.length; - // for non-default origin we need to take the viewport translation into account - var currentTranslate = this.translate(); + cells.forEach(function(cell, index) { + cell.set('z', z + index, opt); + }); - return g.rect({ - x: crect.left - clientCTM.e + currentTranslate.tx, - y: crect.top - clientCTM.f + currentTranslate.ty, - width: crect.width, - height: crect.height - }); + this.stopBatch('to-front'); + } + } + + return this; }, - // Returns a geometry rectangle represeting the entire - // paper area (coordinates from the left paper border to the right one - // and the top border to the bottom one). - getArea: function() { + toBack: function(opt) { - return this.paperToLocalRect({ - x: 0, - y: 0, - width: this.options.width, - height: this.options.height - }); - }, + var graph = this.graph; + if (graph) { - getRestrictedArea: function() { + opt = opt || {}; - var restrictedArea; + var z = graph.minZIndex(); - if (joint.util.isFunction(this.options.restrictTranslate)) { - // A method returning a bounding box - restrictedArea = this.options.restrictTranslate.apply(this, arguments); - } else if (this.options.restrictTranslate === true) { - // The paper area - restrictedArea = this.getArea(); - } else { - // Either false or a bounding box - restrictedArea = this.options.restrictTranslate || null; - } + var cells; - return restrictedArea; - }, + if (opt.deep) { + cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + cells.unshift(this); + } else { + cells = [this]; + } - createViewForModel: function(cell) { + var collection = graph.get('cells'); + var shouldUpdate = (collection.indexOf(this) !== 0); + if (!shouldUpdate) { + shouldUpdate = cells.some(function(cell, index) { + return cell.get('z') !== z + index; + }); + } - // A class taken from the paper options. - var optionalViewClass; + if (shouldUpdate) { + this.startBatch('to-back'); - // A default basic class (either dia.ElementView or dia.LinkView) - var defaultViewClass; + z -= cells.length; - // A special class defined for this model in the corresponding namespace. - // e.g. joint.shapes.basic.Rect searches for joint.shapes.basic.RectView - var namespace = this.options.cellViewNamespace; - var type = cell.get('type') + 'View'; - var namespaceViewClass = joint.util.getByPath(namespace, type, '.'); + cells.forEach(function(cell, index) { + cell.set('z', z + index, opt); + }); - if (cell.isLink()) { - optionalViewClass = this.options.linkView; - defaultViewClass = joint.dia.LinkView; - } else { - optionalViewClass = this.options.elementView; - defaultViewClass = joint.dia.ElementView; + this.stopBatch('to-back'); + } } - // a) the paper options view is a class (deprecated) - // 1. search the namespace for a view - // 2. if no view was found, use view from the paper options - // b) the paper options view is a function - // 1. call the function from the paper options - // 2. if no view was return, search the namespace for a view - // 3. if no view was found, use the default - var ViewClass = (optionalViewClass.prototype instanceof Backbone.View) - ? namespaceViewClass || optionalViewClass - : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass; - - return new ViewClass({ - model: cell, - interactive: this.options.interactive - }); + return this; }, - onCellAdded: function(cell, graph, opt) { + parent: function(parent, opt) { - if (this.options.async && opt.async !== false && joint.util.isNumber(opt.position)) { + // getter + if (parent === undefined) return this.get('parent'); + // setter + return this.set('parent', parent, opt); + }, - this._asyncCells = this._asyncCells || []; - this._asyncCells.push(cell); - - if (opt.position == 0) { + embed: function(cell, opt) { - if (this._frameId) throw new Error('another asynchronous rendering in progress'); + if (this === cell || this.isEmbeddedIn(cell)) { - this.asyncRenderViews(this._asyncCells, opt); - delete this._asyncCells; - } + throw new Error('Recursive embedding not allowed.'); } else { - this.renderView(cell); - } - }, + this.startBatch('embed'); - removeView: function(cell) { + var embeds = joint.util.assign([], this.get('embeds')); - var view = this._views[cell.id]; + // We keep all element ids after link ids. + embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); - if (view) { - view.remove(); - delete this._views[cell.id]; + cell.parent(this.id, opt); + this.set('embeds', joint.util.uniq(embeds), opt); + + this.stopBatch('embed'); } - return view; + return this; }, - renderView: function(cell) { + unembed: function(cell, opt) { - var view = this._views[cell.id] = this.createViewForModel(cell); + this.startBatch('unembed'); - V(this.viewport).append(view.el); - view.paper = this; - view.render(); + cell.unset('parent', opt); + this.set('embeds', joint.util.without(this.get('embeds'), cell.id), opt); - // This is the only way to prevent image dragging in Firefox that works. - // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help. - $(view.el).find('image').on('dragstart', function() { return false; }); + this.stopBatch('unembed'); - return view; + return this; }, - beforeRenderViews: function(cells) { + getParentCell: function() { - // Make sure links are always added AFTER elements. - // They wouldn't find their sources/targets in the DOM otherwise. - cells.sort(function(a) { return (a.isLink()) ? 1 : -1; }); + // unlike link.source/target, cell.parent stores id directly as a string + var parentId = this.parent(); + var graph = this.graph; - return cells; + return (parentId && graph && graph.getCell(parentId)) || null; }, - afterRenderViews: function() { - - this.sortViews(); - }, + // Return an array of ancestor cells. + // The array is ordered from the parent of the cell + // to the most distant ancestor. + getAncestors: function() { - resetViews: function(cellsCollection, opt) { + var ancestors = []; - // clearing views removes any event listeners - this.removeViews(); + if (!this.graph) { + return ancestors; + } - var cells = cellsCollection.models.slice(); + var parentCell = this.getParentCell(); + while (parentCell) { + ancestors.push(parentCell); + parentCell = parentCell.getParentCell(); + } - // `beforeRenderViews()` can return changed cells array (e.g sorted). - cells = this.beforeRenderViews(cells, opt) || cells; + return ancestors; + }, - this.cancelRenderViews(); + getEmbeddedCells: function(opt) { - if (this.options.async) { + opt = opt || {}; - this.asyncRenderViews(cells, opt); - // Sort the cells once all elements rendered (see asyncRenderViews()). + // Cell models can only be retrieved when this element is part of a collection. + // There is no way this element knows about other cells otherwise. + // This also means that calling e.g. `translate()` on an element with embeds before + // adding it to a graph does not translate its embeds. + if (this.graph) { - } else { + var cells; - for (var i = 0, n = cells.length; i < n; i++) { - this.renderView(cells[i]); - } + if (opt.deep) { - // Sort the cells in the DOM manually as we might have changed the order they - // were added to the DOM (see above). - this.sortViews(); - } - }, + if (opt.breadthFirst) { - cancelRenderViews: function() { - if (this._frameId) { - joint.util.cancelFrame(this._frameId); - delete this._frameId; - } - }, + // breadthFirst algorithm + cells = []; + var queue = this.getEmbeddedCells(); - removeViews: function() { + while (queue.length > 0) { - joint.util.invoke(this._views, 'remove'); + var parent = queue.shift(); + cells.push(parent); + queue.push.apply(queue, parent.getEmbeddedCells()); + } - this._views = {}; - }, + } else { - asyncBatchAdded: joint.util.noop, + // depthFirst algorithm + cells = this.getEmbeddedCells(); + cells.forEach(function(cell) { + cells.push.apply(cells, cell.getEmbeddedCells(opt)); + }); + } - asyncRenderViews: function(cells, opt) { + } else { - if (this._frameId) { + cells = joint.util.toArray(this.get('embeds')).map(this.graph.getCell, this.graph); + } - var batchSize = (this.options.async && this.options.async.batchSize) || 50; - var batchCells = cells.splice(0, batchSize); + return cells; + } + return []; + }, - batchCells.forEach(function(cell) { + isEmbeddedIn: function(cell, opt) { - // The cell has to be part of the graph. - // There is a chance in asynchronous rendering - // that a cell was removed before it's rendered to the paper. - if (cell.graph === this.model) this.renderView(cell); + var cellId = joint.util.isString(cell) ? cell : cell.id; + var parentId = this.parent(); - }, this); + opt = joint.util.defaults({ deep: true }, opt); - this.asyncBatchAdded(); - } + // See getEmbeddedCells(). + if (this.graph && opt.deep) { - if (!cells.length) { + while (parentId) { + if (parentId === cellId) { + return true; + } + parentId = this.graph.getCell(parentId).parent(); + } - // No cells left to render. - delete this._frameId; - this.afterRenderViews(opt); - this.trigger('render:done', opt); + return false; } else { - // Schedule a next batch to render. - this._frameId = joint.util.nextFrame(function() { - this.asyncRenderViews(cells, opt); - }, this); + // When this cell is not part of a collection check + // at least whether it's a direct child of given cell. + return parentId === cellId; } }, - sortViews: function() { + // Whether or not the cell is embedded in any other cell. + isEmbedded: function() { - // Run insertion sort algorithm in order to efficiently sort DOM elements according to their - // associated model `z` attribute. + return !!this.parent(); + }, - var $cells = $(this.viewport).children('[model-id]'); - var cells = this.model.get('cells'); + // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). + // Shallow cloning simply clones the cell and returns a new cell with different ID. + // Deep cloning clones the cell and all its embedded cells recursively. + clone: function(opt) { - joint.util.sortElements($cells, function(a, b) { + opt = opt || {}; - var cellA = cells.get($(a).attr('model-id')); - var cellB = cells.get($(b).attr('model-id')); + if (!opt.deep) { + // Shallow cloning. - return (cellA.get('z') || 0) > (cellB.get('z') || 0) ? 1 : -1; - }); - }, + var clone = Backbone.Model.prototype.clone.apply(this, arguments); + // We don't want the clone to have the same ID as the original. + clone.set('id', joint.util.uuid()); + // A shallow cloned element does not carry over the original embeds. + clone.unset('embeds'); + // And can not be embedded in any cell + // as the clone is not part of the graph. + clone.unset('parent'); - scale: function(sx, sy, ox, oy) { + return clone; - // getter - if (sx === undefined) { - return V.matrixToScale(this.matrix()); - } + } else { + // Deep cloning. - // setter - if (sy === undefined) { - sy = sx; - } - if (ox === undefined) { - ox = 0; - oy = 0; + // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. + return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); } + }, - var translate = this.translate(); - - if (ox || oy || translate.tx || translate.ty) { - var newTx = translate.tx - ox * (sx - 1); - var newTy = translate.ty - oy * (sy - 1); - this.translate(newTx, newTy); - } + // A convenient way to set nested properties. + // This method merges the properties you'd like to set with the ones + // stored in the cell and makes sure change events are properly triggered. + // You can either set a nested property with one object + // or use a property path. + // The most simple use case is: + // `cell.prop('name/first', 'John')` or + // `cell.prop({ name: { first: 'John' } })`. + // Nested arrays are supported too: + // `cell.prop('series/0/data/0/degree', 50)` or + // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. + prop: function(props, value, opt) { - var ctm = this.matrix(); - ctm.a = sx || 0; - ctm.d = sy || 0; + var delim = '/'; + var isString = joint.util.isString(props); - this.matrix(ctm); + if (isString || Array.isArray(props)) { + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. - this.trigger('scale', sx, sy, ox, oy); + if (arguments.length > 1) { - return this; - }, + var path; + var pathArray; - // Experimental - do not use in production. - rotate: function(angle, cx, cy) { + if (isString) { + path = props; + pathArray = path.split('/') + } else { + path = props.join(delim); + pathArray = props.slice(); + } - // getter - if (angle === undefined) { - return V.matrixToRotate(this.matrix()); - } + var property = pathArray[0]; + var pathArrayLength = pathArray.length; - // setter + opt = opt || {}; + opt.propertyPath = path; + opt.propertyValue = value; + opt.propertyPathArray = pathArray; - // If the origin is not set explicitely, rotate around the center. Note that - // we must use the plain bounding box (`this.el.getBBox()` instead of the one that gives us - // the real bounding box (`bbox()`) including transformations). - if (cx === undefined) { - var bbox = this.viewport.getBBox(); - cx = bbox.width / 2; - cy = bbox.height / 2; - } + if (pathArrayLength === 1) { + // Property is not nested. We can simply use `set()`. + return this.set(property, value, opt); + } - var ctm = this.matrix().translate(cx,cy).rotate(angle).translate(-cx,-cy); - this.matrix(ctm); + var update = {}; + // Initialize the nested object. Subobjects are either arrays or objects. + // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. + // Note that this imposes a limitation on object keys one can use with Inspector. + // Pure integer keys will cause issues and are therefore not allowed. + var initializer = update; + var prevProperty = property; - return this; - }, + for (var i = 1; i < pathArrayLength; i++) { + var pathItem = pathArray[i]; + var isArrayIndex = Number.isFinite(isString ? Number(pathItem) : pathItem); + initializer = initializer[prevProperty] = isArrayIndex ? [] : {}; + prevProperty = pathItem; + } - translate: function(tx, ty) { + // Fill update with the `value` on `path`. + update = joint.util.setByPath(update, pathArray, value, '/'); - // getter - if (tx === undefined) { - return V.matrixToTranslate(this.matrix()); - } + var baseAttributes = joint.util.merge({}, this.attributes); + // if rewrite mode enabled, we replace value referenced by path with + // the new one (we don't merge). + opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); - // setter + // Merge update with the model attributes. + var attributes = joint.util.merge(baseAttributes, update); + // Finally, set the property to the updated attributes. + return this.set(property, attributes[property], opt); - var ctm = this.matrix(); - ctm.e = tx || 0; - ctm.f = ty || 0; + } else { - this.matrix(ctm); + return joint.util.getByPath(this.attributes, props, delim); + } + } - var newTranslate = this.translate(); - var origin = this.options.origin; - origin.x = newTranslate.tx; - origin.y = newTranslate.ty; + return this.set(joint.util.merge({}, this.attributes, props), value); + }, - this.trigger('translate', newTranslate.tx, newTranslate.ty); + // A convient way to unset nested properties + removeProp: function(path, opt) { - if (this.options.drawGrid) { - this.drawGrid(); + // Once a property is removed from the `attrs` attribute + // the cellView will recognize a `dirty` flag and rerender itself + // in order to remove the attribute from SVG element. + opt = opt || {}; + opt.dirty = true; + + var pathArray = Array.isArray(path) ? path : path.split('/'); + + if (pathArray.length === 1) { + // A top level property + return this.unset(path, opt); } - return this; + // A nested property + var property = pathArray[0]; + var nestedPath = pathArray.slice(1); + var propertyValue = joint.util.cloneDeep(this.get(property)); + + joint.util.unsetByPath(propertyValue, nestedPath, '/'); + + return this.set(property, propertyValue, opt); }, - // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also - // be a selector or a jQuery object. - findView: function($el) { + // A convenient way to set nested attributes. + attr: function(attrs, value, opt) { - var el = joint.util.isString($el) - ? this.viewport.querySelector($el) - : $el instanceof $ ? $el[0] : $el; + var args = Array.from(arguments); + if (args.length === 0) { + return this.get('attrs'); + } - while (el && el !== this.el && el !== document) { + if (Array.isArray(attrs)) { + args[0] = ['attrs'].concat(attrs); + } else if (joint.util.isString(attrs)) { + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. + args[0] = 'attrs/' + attrs; - var id = el.getAttribute('model-id'); - if (id) return this._views[id]; + } else { - el = el.parentNode; + args[0] = { 'attrs' : attrs }; } - return undefined; + return this.prop.apply(this, args); }, - // Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`. - findViewByModel: function(cell) { + // A convenient way to unset nested attributes + removeAttr: function(path, opt) { - var id = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : (cell && cell.id); + if (Array.isArray(path)) { - return this._views[id]; + return this.removeProp(['attrs'].concat(path)); + } + + return this.removeProp('attrs/' + path, opt); }, - // Find all views at given point - findViewsFromPoint: function(p) { + transition: function(path, value, opt, delim) { - p = g.point(p); + delim = delim || '/'; - var views = this.model.getElements().map(this.findViewByModel, this); + var defaults = { + duration: 100, + delay: 10, + timingFunction: joint.util.timing.linear, + valueFunction: joint.util.interpolate.number + }; - return views.filter(function(view) { - return view && view.vel.getBBox({ target: this.viewport }).containsPoint(p); - }, this); - }, + opt = joint.util.assign(defaults, opt); - // Find all views in given area - findViewsInArea: function(rect, opt) { + var firstFrameTime = 0; + var interpolatingFunction; - opt = joint.util.defaults(opt || {}, { strict: false }); - rect = g.rect(rect); + var setter = function(runtime) { - var views = this.model.getElements().map(this.findViewByModel, this); - var method = opt.strict ? 'containsRect' : 'intersect'; + var id, progress, propertyValue; - return views.filter(function(view) { - return view && rect[method](view.vel.getBBox({ target: this.viewport })); - }, this); - }, + firstFrameTime = firstFrameTime || runtime; + runtime -= firstFrameTime; + progress = runtime / opt.duration; - getModelById: function(id) { + if (progress < 1) { + this._transitionIds[path] = id = joint.util.nextFrame(setter); + } else { + progress = 1; + delete this._transitionIds[path]; + } - return this.model.getCell(id); - }, + propertyValue = interpolatingFunction(opt.timingFunction(progress)); - snapToGrid: function(x, y) { + opt.transitionId = id; - // Convert global coordinates to the local ones of the `viewport`. Otherwise, - // improper transformation would be applied when the viewport gets transformed (scaled/rotated). - return this.clientToLocalPoint(x, y).snapToGrid(this.options.gridSize); - }, + this.prop(path, propertyValue, opt); - localToPaperPoint: function(x, y) { - // allow `x` to be a point and `y` undefined - var localPoint = g.Point(x, y); - var paperPoint = V.transformPoint(localPoint, this.matrix()); - return g.Point(paperPoint); - }, + if (!id) this.trigger('transition:end', this, path); - localToPaperRect: function(x, y, width, height) { - // allow `x` to be a rectangle and rest arguments undefined - var localRect = g.Rect(x, y); - var paperRect = V.transformRect(localRect, this.matrix()); - return g.Rect(paperRect); - }, + }.bind(this); - paperToLocalPoint: function(x, y) { - // allow `x` to be a point and `y` undefined - var paperPoint = g.Point(x, y); - var localPoint = V.transformPoint(paperPoint, this.matrix().inverse()); - return g.Point(localPoint); - }, + var initiator = function(callback) { - paperToLocalRect: function(x, y, width, height) { - // allow `x` to be a rectangle and rest arguments undefined - var paperRect = g.Rect(x, y, width, height); - var localRect = V.transformRect(paperRect, this.matrix().inverse()); - return g.Rect(localRect); - }, + this.stopTransitions(path); - localToClientPoint: function(x, y) { - // allow `x` to be a point and `y` undefined - var localPoint = g.Point(x, y); - var clientPoint = V.transformPoint(localPoint, this.clientMatrix()); - return g.Point(clientPoint); - }, + interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); - localToClientRect: function(x, y, width, height) { - // allow `x` to be a point and `y` undefined - var localRect = g.Rect(x, y, width, height); - var clientRect = V.transformRect(localRect, this.clientMatrix()); - return g.Rect(clientRect); - }, + this._transitionIds[path] = joint.util.nextFrame(callback); - // Transform client coordinates to the paper local coordinates. - // Useful when you have a mouse event object and you'd like to get coordinates - // inside the paper that correspond to `evt.clientX` and `evt.clientY` point. - // Example: var localPoint = paper.clientToLocalPoint({ x: evt.clientX, y: evt.clientY }); - clientToLocalPoint: function(x, y) { - // allow `x` to be a point and `y` undefined - var clientPoint = g.Point(x, y); - var localPoint = V.transformPoint(clientPoint, this.clientMatrix().inverse()); - return g.Point(localPoint); - }, + this.trigger('transition:start', this, path); - clientToLocalRect: function(x, y, width, height) { - // allow `x` to be a point and `y` undefined - var clientRect = g.Rect(x, y, width, height); - var localRect = V.transformRect(clientRect, this.clientMatrix().inverse()); - return g.Rect(localRect); - }, + }.bind(this); - localToPagePoint: function(x, y) { - return this.localToPaperPoint(x, y).offset(this.pageOffset()); + return setTimeout(initiator, opt.delay, setter); }, - localToPageRect: function(x, y, width, height) { - return this.localToPaperRect(x, y, width, height).moveAndExpand(this.pageOffset()); - }, + getTransitions: function() { - pageToLocalPoint: function(x, y) { - var pagePoint = g.Point(x, y); - var paperPoint = pagePoint.difference(this.pageOffset()); - return this.paperToLocalPoint(paperPoint); + return Object.keys(this._transitionIds); }, - pageToLocalRect: function(x, y, width, height) { - var pageOffset = this.pageOffset(); - var paperRect = g.Rect(x, y, width, height); - paperRect.x -= pageOffset.x; - paperRect.y -= pageOffset.y; - return this.paperToLocalRect(paperRect); - }, - - clientOffset: function() { - var clientRect = this.svg.getBoundingClientRect(); - return g.Point(clientRect.left, clientRect.top); - }, + stopTransitions: function(path, delim) { - pageOffset: function() { - return this.clientOffset().offset(window.scrollX, window.scrollY); - }, + delim = delim || '/'; - linkAllowed: function(linkViewOrModel) { + var pathArray = path && path.split(delim); - var link; + Object.keys(this._transitionIds).filter(pathArray && function(key) { - if (linkViewOrModel instanceof joint.dia.Link) { - link = linkViewOrModel; - } else if (linkViewOrModel instanceof joint.dia.LinkView) { - link = linkViewOrModel.model; - } else { - throw new Error('Must provide link model or view.'); - } + return joint.util.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); - if (!this.options.multiLinks) { + }).forEach(function(key) { - // Do not allow multiple links to have the same source and target. + joint.util.cancelFrame(this._transitionIds[key]); - var source = link.get('source'); - var target = link.get('target'); + delete this._transitionIds[key]; - if (source.id && target.id) { + this.trigger('transition:end', this, key); - var sourceModel = link.getSourceElement(); + }, this); - if (sourceModel) { + return this; + }, - var connectedLinks = this.model.getConnectedLinks(sourceModel, { - outbound: true, - inbound: false - }); + // A shorcut making it easy to create constructs like the following: + // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. + addTo: function(graph, opt) { - var numSameLinks = connectedLinks.filter(function(_link) { + graph.addCell(this, opt); + return this; + }, - var _source = _link.get('source'); - var _target = _link.get('target'); + // A shortcut for an equivalent call: `paper.findViewByModel(cell)` + // making it easy to create constructs like the following: + // `cell.findView(paper).highlight()` + findView: function(paper) { - return _source && _source.id === source.id && - (!_source.port || (_source.port === source.port)) && - _target && _target.id === target.id && - (!_target.port || (_target.port === target.port)); + return paper.findViewByModel(this); + }, - }).length; + isElement: function() { - if (numSameLinks > 1) { - return false; - } - } - } - } + return false; + }, - if ( - !this.options.linkPinning && - ( - !joint.util.has(link.get('source'), 'id') || - !joint.util.has(link.get('target'), 'id') - ) - ) { - // Link pinning is not allowed and the link is not connected to the target. - return false; - } + isLink: function() { - return true; + return false; }, - getDefaultLink: function(cellView, magnet) { + startBatch: function(name, opt) { - return joint.util.isFunction(this.options.defaultLink) - // default link is a function producing link model - ? this.options.defaultLink.call(this, cellView, magnet) - // default link is the Backbone model - : this.options.defaultLink.clone(); + if (this.graph) { this.graph.startBatch(name, joint.util.assign({}, opt, { cell: this })); } + return this; }, - // Cell highlighting - // ----------------- - resolveHighlighter: function(opt) { + stopBatch: function(name, opt) { - opt = opt || {}; - var highlighterDef = opt.highlighter; - var paperOpt = this.options; + if (this.graph) { this.graph.stopBatch(name, joint.util.assign({}, opt, { cell: this })); } + return this; + } - /* - Expecting opt.highlighter to have the following structure: - { - name: 'highlighter-name', - options: { - some: 'value' - } - } - */ - if (highlighterDef === undefined) { +}, { - // check for built-in types - var type = ['embedding', 'connecting', 'magnetAvailability', 'elementAvailability'].find(function(type) { - return !!opt[type]; - }); + getAttributeDefinition: function(attrName) { - highlighterDef = (type && paperOpt.highlighting[type]) || paperOpt.highlighting['default']; - } + var defNS = this.attributes; + var globalDefNS = joint.dia.attributes; + return (defNS && defNS[attrName]) || globalDefNS[attrName]; + }, - // Do nothing if opt.highlighter is falsey. - // This allows the case to not highlight cell(s) in certain cases. - // For example, if you want to NOT highlight when embedding elements. - if (!highlighterDef) return false; + define: function(type, defaults, protoProps, staticProps) { - // Allow specifying a highlighter by name. - if (joint.util.isString(highlighterDef)) { - highlighterDef = { - name: highlighterDef - }; - } + protoProps = joint.util.assign({ + defaults: joint.util.defaultsDeep({ type: type }, defaults, this.prototype.defaults) + }, protoProps); - var name = highlighterDef.name; - var highlighter = paperOpt.highlighterNamespace[name]; + var Cell = this.extend(protoProps, staticProps); + joint.util.setByPath(joint.shapes, type, Cell, '.'); + return Cell; + } +}); - // Highlighter validation - if (!highlighter) { - throw new Error('Unknown highlighter ("' + name + '")'); - } - if (typeof highlighter.highlight !== 'function') { - throw new Error('Highlighter ("' + name + '") is missing required highlight() method'); - } - if (typeof highlighter.unhighlight !== 'function') { - throw new Error('Highlighter ("' + name + '") is missing required unhighlight() method'); - } +// joint.dia.CellView base view and controller. +// -------------------------------------------- - return { - highlighter: highlighter, - options: highlighterDef.options || {}, - name: name - }; - }, +// This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. - onCellHighlight: function(cellView, magnetEl, opt) { +joint.dia.CellView = joint.mvc.View.extend({ - opt = this.resolveHighlighter(opt); - if (!opt) return; - if (!magnetEl.id) { - magnetEl.id = V.uniqueId(); - } + tagName: 'g', - var key = opt.name + magnetEl.id + JSON.stringify(opt.options); - if (!this._highlights[key]) { + svgElement: true, - var highlighter = opt.highlighter; - highlighter.highlight(cellView, magnetEl, joint.util.assign({}, opt.options)); + selector: 'root', - this._highlights[key] = { - cellView: cellView, - magnetEl: magnetEl, - opt: opt.options, - highlighter: highlighter - }; + className: function() { + + var classNames = ['cell']; + var type = this.model.get('type'); + + if (type) { + + type.toLowerCase().split('.').forEach(function(value, index, list) { + classNames.push('type-' + list.slice(0, index + 1).join('-')); + }); } + + return classNames.join(' '); }, - onCellUnhighlight: function(cellView, magnetEl, opt) { + attributes: function() { - opt = this.resolveHighlighter(opt); - if (!opt) return; + return { 'model-id': this.model.id }; + }, - var key = opt.name + magnetEl.id + JSON.stringify(opt.options); - var highlight = this._highlights[key]; - if (highlight) { + constructor: function(options) { - // Use the cellView and magnetEl that were used by the highlighter.highlight() method. - highlight.highlighter.unhighlight(highlight.cellView, highlight.magnetEl, highlight.opt); + // Make sure a global unique id is assigned to this view. Store this id also to the properties object. + // The global unique id makes sure that the same view can be rendered on e.g. different machines and + // still be associated to the same object among all those clients. This is necessary for real-time + // collaboration mechanism. + options.id = options.id || joint.util.guid(this); - this._highlights[key] = null; - } + joint.mvc.View.call(this, options); }, - // Interaction. - // ------------ - - mousedblclick: function(evt) { + init: function() { - evt.preventDefault(); - evt = joint.util.normalizeEvent(evt); + joint.util.bindAll(this, 'remove', 'update'); - var view = this.findView(evt.target); - if (this.guard(evt, view)) return; + // Store reference to this to the <g> DOM element so that the view is accessible through the DOM tree. + this.$el.data('view', this); - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + // Add the cell's type to the view's element as a data attribute. + this.$el.attr('data-type', this.model.get('type')); - if (view) { + this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); + }, - view.pointerdblclick(evt, localPoint.x, localPoint.y); + onChangeAttrs: function(cell, attrs, opt) { - } else { + if (opt.dirty) { - this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y); + // dirty flag could be set when a model attribute was removed and it needs to be cleared + // also from the DOM element. See cell.removeAttr(). + return this.render(); } - }, - mouseclick: function(evt) { + return this.update(cell, attrs, opt); + }, - // Trigger event when mouse not moved. - if (this._mousemoved <= this.options.clickThreshold) { + // Return `true` if cell link is allowed to perform a certain UI `feature`. + // Example: `can('vertexMove')`, `can('labelMove')`. + can: function(feature) { - evt = joint.util.normalizeEvent(evt); + var interactive = joint.util.isFunction(this.options.interactive) + ? this.options.interactive(this) + : this.options.interactive; - var view = this.findView(evt.target); - if (this.guard(evt, view)) return; + return (joint.util.isObject(interactive) && interactive[feature] !== false) || + (joint.util.isBoolean(interactive) && interactive !== false); + }, - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + findBySelector: function(selector, root, selectors) { - if (view) { + root || (root = this.el); + selectors || (selectors = this.selectors); - view.pointerclick(evt, localPoint.x, localPoint.y); + // These are either descendants of `this.$el` of `this.$el` itself. + // `.` is a special selector used to select the wrapping `<g>` element. + if (!selector || selector === '.') return [root]; + if (selectors && selectors[selector]) return [selectors[selector]]; + // Maintaining backwards compatibility + // e.g. `circle:first` would fail with querySelector() call + return $(root).find(selector).toArray(); + }, - } else { + notify: function(eventName) { - this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y); - } - } - }, + if (this.paper) { - // Guard guards the event received. If the event is not interesting, guard returns `true`. - // Otherwise, it return `false`. - guard: function(evt, view) { + var args = Array.prototype.slice.call(arguments, 1); - if (this.options.guard && this.options.guard(evt, view)) { - return true; - } + // Trigger the event on both the element itself and also on the paper. + this.trigger.apply(this, [eventName].concat(args)); - if (evt.data && evt.data.guarded !== undefined) { - return evt.data.guarded; + // Paper event handlers receive the view object as the first argument. + this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); } + }, - if (view && view.model && (view.model instanceof joint.dia.Cell)) { - return false; - } + // ** Deprecated ** + getStrokeBBox: function(el) { + // Return a bounding box rectangle that takes into account stroke. + // Note that this is a naive and ad-hoc implementation that does not + // works only in certain cases and should be replaced as soon as browsers will + // start supporting the getStrokeBBox() SVG method. + // @TODO any better solution is very welcome! - if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { - return false; - } + var isMagnet = !!el; - return true; // Event guarded. Paper should not react on it in any way. - }, + el = el || this.el; + var bbox = V(el).getBBox({ target: this.paper.viewport }); + var strokeWidth; + if (isMagnet) { - contextmenu: function(evt) { + strokeWidth = V(el).attr('stroke-width'); - evt = joint.util.normalizeEvent(evt); + } else { - if (this.options.preventContextMenu) { - evt.preventDefault(); + strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); } - var view = this.findView(evt.target); - if (this.guard(evt, view)) return; - - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - - if (view) { - - view.contextmenu(evt, localPoint.x, localPoint.y); - - } else { + strokeWidth = parseFloat(strokeWidth) || 0; - this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y); - } + return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); }, - pointerdown: function(evt) { + getBBox: function() { - this.bindDocumentEvents(); + return this.vel.getBBox({ target: this.paper.svg }); + }, - evt = joint.util.normalizeEvent(evt); + highlight: function(el, opt) { - var view = this.findView(evt.target); - if (this.guard(evt, view)) return; + el = !el ? this.el : this.$(el)[0] || this.el; - this._mousemoved = 0; + // set partial flag if the highlighted element is not the entire view. + opt = opt || {}; + opt.partial = (el !== this.el); - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + this.notify('cell:highlight', el, opt); + return this; + }, - if (view) { + unhighlight: function(el, opt) { - evt.preventDefault(); + el = !el ? this.el : this.$(el)[0] || this.el; - this.sourceView = view; + opt = opt || {}; + opt.partial = el != this.el; - view.pointerdown(evt, localPoint.x, localPoint.y); + this.notify('cell:unhighlight', el, opt); + return this; + }, - } else { + // Find the closest element that has the `magnet` attribute set to `true`. If there was not such + // an element found, return the root element of the cell view. + findMagnet: function(el) { - if (this.options.preventDefaultBlankAction) { - evt.preventDefault(); - } + var $el = this.$(el); + var $rootEl = this.$el; - this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y); + if ($el.length === 0) { + $el = $rootEl; } - }, - - pointermove: function(evt) { - var view = this.sourceView; - if (view) { + do { - evt.preventDefault(); + var magnet = $el.attr('magnet'); + if ((magnet || $el.is($rootEl)) && magnet !== 'false') { + return $el[0]; + } - // Mouse moved counter. - var mousemoved = ++this._mousemoved; - if (mousemoved > this.options.moveThreshold) { + $el = $el.parent(); - evt = joint.util.normalizeEvent(evt); + } while ($el.length > 0); - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - view.pointermove(evt, localPoint.x, localPoint.y); - } - } + // If the overall cell has set `magnet === false`, then return `undefined` to + // announce there is no magnet found for this cell. + // This is especially useful to set on cells that have 'ports'. In this case, + // only the ports have set `magnet === true` and the overall element has `magnet === false`. + return undefined; }, - pointerup: function(evt) { - - this.unbindDocumentEvents(); - - evt = joint.util.normalizeEvent(evt); + // Construct a unique selector for the `el` element within this view. + // `prevSelector` is being collected through the recursive call. + // No value for `prevSelector` is expected when using this method. + getSelector: function(el, prevSelector) { - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + if (el === this.el) { + return prevSelector; + } - if (this.sourceView) { + var selector; - this.sourceView.pointerup(evt, localPoint.x, localPoint.y); + if (el) { - //"delete sourceView" occasionally throws an error in chrome (illegal access exception) - this.sourceView = null; + var nthChild = V(el).index() + 1; + selector = el.tagName + ':nth-child(' + nthChild + ')'; - } else { + if (prevSelector) { + selector += ' > ' + prevSelector; + } - this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); + selector = this.getSelector(el.parentNode, selector); } - }, - - mousewheel: function(evt) { - - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (this.guard(evt, view)) return; - - var originalEvent = evt.originalEvent; - var localPoint = this.snapToGrid({ x: originalEvent.clientX, y: originalEvent.clientY }); - var delta = Math.max(-1, Math.min(1, (originalEvent.wheelDelta || -originalEvent.detail))); - if (view) { + return selector; + }, - view.mousewheel(evt, localPoint.x, localPoint.y, delta); + getLinkEnd: function(magnet, x, y, link, endType) { - } else { + var model = this.model; + var id = model.id; + var port = this.findAttribute('port', magnet); + // Find a unique `selector` of the element under pointer that is a magnet. + var selector = magnet.getAttribute('joint-selector'); - this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta); + var end = { id: id }; + if (selector != null) end.magnet = selector; + if (port != null) { + end.port = port; + if (!model.hasPort(port) && !selector) { + // port created via the `port` attribute (not API) + end.selector = this.getSelector(magnet); + } + } else if (selector == null && this.el !== magnet) { + end.selector = this.getSelector(magnet); } - }, - - cellMouseover: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view) { - if (this.guard(evt, view)) return; - view.mouseover(evt); + var paper = this.paper; + var connectionStrategy = paper.options.connectionStrategy; + if (typeof connectionStrategy === 'function') { + var strategy = connectionStrategy.call(paper, end, this, magnet, new g.Point(x, y), link, endType); + if (strategy) end = strategy; } - }, - - cellMouseout: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view) { - if (this.guard(evt, view)) return; - view.mouseout(evt); - } + return end; }, - cellMouseenter: function(evt) { + getMagnetFromLinkEnd: function(end) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view && !this.guard(evt, view)) { - view.mouseenter(evt); + var root = this.el; + var port = end.port; + var selector = end.magnet; + var magnet; + if (port != null && this.model.hasPort(port)) { + magnet = this.findPortNode(port, selector) || root; + } else { + magnet = this.findBySelector(selector || end.selector, root, this.selectors)[0]; } - }, - - cellMouseleave: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view && !this.guard(evt, view)) { - view.mouseleave(evt); - } + return magnet; }, - cellEvent: function(evt) { + findAttribute: function(attributeName, node) { - evt = joint.util.normalizeEvent(evt); + if (!node) return null; - var currentTarget = evt.currentTarget; - var eventName = currentTarget.getAttribute('event'); - if (eventName) { - var view = this.findView(currentTarget); - if (view && !this.guard(evt, view)) { - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - view.event(evt, eventName, localPoint.x, localPoint.y); + var attributeValue = node.getAttribute(attributeName); + if (attributeValue === null) { + if (node === this.el) return null; + var currentNode = node.parentNode; + while (currentNode && currentNode !== this.el && currentNode.nodeType === 1) { + attributeValue = currentNode.getAttribute(attributeName) + if (attributeValue !== null) break; + currentNode = currentNode.parentNode; } } + return attributeValue; }, - setGridSize: function(gridSize) { - - this.options.gridSize = gridSize; - - if (this.options.drawGrid) { - this.drawGrid(); - } + getAttributeDefinition: function(attrName) { - return this; + return this.model.constructor.getAttributeDefinition(attrName); }, - clearGrid: function() { + setNodeAttributes: function(node, attrs) { - if (this.$grid) { - this.$grid.css('backgroundImage', 'none'); + if (!joint.util.isEmpty(attrs)) { + if (node instanceof SVGElement) { + V(node).attr(attrs); + } else { + $(node).attr(attrs); + } } - return this; }, - _getGriRefs: function () { - - if (!this._gridCache) { + processNodeAttributes: function(node, attrs) { - this._gridCache = { - root: V('svg', { width: '100%', height: '100%' }, V('defs')), - patterns: {}, - add: function (id, vel) { - V(this.root.node.childNodes[0]).append(vel); - this.patterns[id] = vel; - this.root.append(V('rect', { width: "100%", height: "100%", fill: 'url(#' + id + ')' })); - }, - get: function (id) { - return this.patterns[id] - }, - exist: function (id) { - return this.patterns[id] !== undefined; + var attrName, attrVal, def, i, n; + var normalAttrs, setAttrs, positionAttrs, offsetAttrs; + var relatives = []; + // divide the attributes between normal and special + for (attrName in attrs) { + if (!attrs.hasOwnProperty(attrName)) continue; + attrVal = attrs[attrName]; + def = this.getAttributeDefinition(attrName); + if (def && (!joint.util.isFunction(def.qualify) || def.qualify.call(this, attrVal, node, attrs))) { + if (joint.util.isString(def.set)) { + normalAttrs || (normalAttrs = {}); + normalAttrs[def.set] = attrVal; + } + if (attrVal !== null) { + relatives.push(attrName, def); } + } else { + normalAttrs || (normalAttrs = {}); + normalAttrs[joint.util.toKebabCase(attrName)] = attrVal; } } - return this._gridCache; + // handle the rest of attributes via related method + // from the special attributes namespace. + for (i = 0, n = relatives.length; i < n; i+=2) { + attrName = relatives[i]; + def = relatives[i+1]; + attrVal = attrs[attrName]; + if (joint.util.isFunction(def.set)) { + setAttrs || (setAttrs = {}); + setAttrs[attrName] = attrVal; + } + if (joint.util.isFunction(def.position)) { + positionAttrs || (positionAttrs = {}); + positionAttrs[attrName] = attrVal; + } + if (joint.util.isFunction(def.offset)) { + offsetAttrs || (offsetAttrs = {}); + offsetAttrs[attrName] = attrVal; + } + } + + return { + raw: attrs, + normal: normalAttrs, + set: setAttrs, + position: positionAttrs, + offset: offsetAttrs + }; }, - setGrid:function (drawGrid) { + updateRelativeAttributes: function(node, attrs, refBBox, opt) { - this.clearGrid(); + opt || (opt = {}); - this._gridCache = null; - this._gridSettings = []; + var attrName, attrVal, def; + var rawAttrs = attrs.raw || {}; + var nodeAttrs = attrs.normal || {}; + var setAttrs = attrs.set; + var positionAttrs = attrs.position; + var offsetAttrs = attrs.offset; - var optionsList = Array.isArray(drawGrid) ? drawGrid : [drawGrid || {}]; - optionsList.forEach(function (item) { - this._gridSettings.push.apply(this._gridSettings, this._resolveDrawGridOption(item)); - }, this); - return this; - }, - - _resolveDrawGridOption: function (opt) { + for (attrName in setAttrs) { + attrVal = setAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // SET - set function should return attributes to be set on the node, + // which will affect the node dimensions based on the reference bounding + // box. e.g. `width`, `height`, `d`, `rx`, `ry`, `points + var setResult = def.set.call(this, attrVal, refBBox.clone(), node, rawAttrs); + if (joint.util.isObject(setResult)) { + joint.util.assign(nodeAttrs, setResult); + } else if (setResult !== undefined) { + nodeAttrs[attrName] = setResult; + } + } - var namespace = this.constructor.gridPatterns; - if (joint.util.isString(opt) && Array.isArray(namespace[opt])) { - return namespace[opt].map(function(item) { - return joint.util.assign({}, item); - }); + if (node instanceof HTMLElement) { + // TODO: setting the `transform` attribute on HTMLElements + // via `node.style.transform = 'matrix(...)';` would introduce + // a breaking change (e.g. basic.TextBlock). + this.setNodeAttributes(node, nodeAttrs); + return; } - var options = opt || { args: [{}] }; - var isArray = Array.isArray(options); - var name = options.name; + // The final translation of the subelement. + var nodeTransform = nodeAttrs.transform; + var nodeMatrix = V.transformStringToMatrix(nodeTransform); + var nodePosition = g.Point(nodeMatrix.e, nodeMatrix.f); + if (nodeTransform) { + nodeAttrs = joint.util.omit(nodeAttrs, 'transform'); + nodeMatrix.e = nodeMatrix.f = 0; + } - if (!isArray && !name && !options.markup ) { - name = 'dot'; + // Calculate node scale determined by the scalable group + // only if later needed. + var sx, sy, translation; + if (positionAttrs || offsetAttrs) { + var nodeScale = this.getNodeScale(node, opt.scalableNode); + sx = nodeScale.sx; + sy = nodeScale.sy; } - if (name && Array.isArray(namespace[name])) { - var pattern = namespace[name].map(function(item) { - return joint.util.assign({}, item); - }); + var positioned = false; + for (attrName in positionAttrs) { + attrVal = positionAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // POSITION - position function should return a point from the + // reference bounding box. The default position of the node is x:0, y:0 of + // the reference bounding box or could be further specify by some + // SVG attributes e.g. `x`, `y` + translation = def.position.call(this, attrVal, refBBox.clone(), node, rawAttrs); + if (translation) { + nodePosition.offset(g.Point(translation).scale(sx, sy)); + positioned || (positioned = true); + } + } - var args = Array.isArray(options.args) ? options.args : [options.args || {}]; + // The node bounding box could depend on the `size` set from the previous loop. + // Here we know, that all the size attributes have been already set. + this.setNodeAttributes(node, nodeAttrs); - joint.util.defaults(args[0], joint.util.omit(opt, 'args')); - for (var i = 0; i < args.length; i++) { - if (pattern[i]) { - joint.util.assign(pattern[i], args[i]); + var offseted = false; + if (offsetAttrs) { + // Check if the node is visible + var nodeClientRect = node.getBoundingClientRect(); + if (nodeClientRect.width > 0 && nodeClientRect.height > 0) { + var nodeBBox = V.transformRect(node.getBBox(), nodeMatrix).scale(1 / sx, 1 / sy); + for (attrName in offsetAttrs) { + attrVal = offsetAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // OFFSET - offset function should return a point from the element + // bounding box. The default offset point is x:0, y:0 (origin) or could be further + // specify with some SVG attributes e.g. `text-anchor`, `cx`, `cy` + translation = def.offset.call(this, attrVal, nodeBBox, node, rawAttrs); + if (translation) { + nodePosition.offset(g.Point(translation).scale(sx, sy)); + offseted || (offseted = true); + } } } - return pattern; } - return isArray ? options : [options]; + // Do not touch node's transform attribute if there is no transformation applied. + if (nodeTransform !== undefined || positioned || offseted) { + // Round the coordinates to 1 decimal point. + nodePosition.round(1); + nodeMatrix.e = nodePosition.x; + nodeMatrix.f = nodePosition.y; + node.setAttribute('transform', V.matrixToTransformString(nodeMatrix)); + // TODO: store nodeMatrix metrics? + } }, - drawGrid: function(opt) { + getNodeScale: function(node, scalableNode) { - var gridSize = this.options.gridSize; - if (gridSize <= 1) { - return this.clearGrid(); + // Check if the node is a descendant of the scalable group. + var sx, sy; + if (scalableNode && scalableNode.contains(node)) { + var scale = scalableNode.scale(); + sx = 1 / scale.sx; + sy = 1 / scale.sy; + } else { + sx = 1; + sy = 1; } - var localOptions = Array.isArray(opt) ? opt : [opt]; + return { sx: sx, sy: sy }; + }, - var ctm = this.matrix(); - var refs = this._getGriRefs(); + findNodesAttributes: function(attrs, root, selectorCache, selectors) { - this._gridSettings.forEach(function (gridLayerSetting, index) { + // TODO: merge attributes in order defined by `index` property - var id = 'pattern_' + index; - var options = joint.util.merge(gridLayerSetting, localOptions[index], { - sx: ctm.a || 1, - sy: ctm.d || 1, - ox: ctm.e || 0, - oy: ctm.f || 0 - }); + // most browsers sort elements in attrs by order of addition + // which is useful but not required - options.width = gridSize * (ctm.a || 1) * (options.scaleFactor || 1); - options.height = gridSize * (ctm.d || 1) * (options.scaleFactor || 1); + // link.updateLabels() relies on that assumption for merging label attrs over default label attrs - if (!refs.exist(id)) { - refs.add(id, V('pattern', { id: id, patternUnits: 'userSpaceOnUse' }, V(options.markup))); + var nodesAttrs = {}; + + for (var selector in attrs) { + if (!attrs.hasOwnProperty(selector)) continue; + var selected = selectorCache[selector] = this.findBySelector(selector, root, selectors); + for (var i = 0, n = selected.length; i < n; i++) { + var node = selected[i]; + var nodeId = V.ensureId(node); + var nodeAttrs = attrs[selector]; + var prevNodeAttrs = nodesAttrs[nodeId]; + if (prevNodeAttrs) { + if (!prevNodeAttrs.merged) { + prevNodeAttrs.merged = true; + // if prevNode attrs is `null`, replace with `{}` + prevNodeAttrs.attributes = joint.util.cloneDeep(prevNodeAttrs.attributes) || {}; + } + // if prevNode attrs not set (or `null` or`{}`), use node attrs + // if node attrs not set (or `null` or `{}`), use prevNode attrs + joint.util.merge(prevNodeAttrs.attributes, nodeAttrs); + } else { + nodesAttrs[nodeId] = { + attributes: nodeAttrs, + node: node, + merged: false + }; + } } + } - var patternDefVel = refs.get(id); + return nodesAttrs; + }, - if (joint.util.isFunction(options.update)) { - options.update(patternDefVel.node.childNodes[0], options); - } + // Default is to process the `model.attributes.attrs` object and set attributes on subelements based on the selectors, + // unless `attrs` parameter was passed. + updateDOMSubtreeAttributes: function(rootNode, attrs, opt) { - var x = options.ox % options.width; - if (x < 0) x += options.width; + opt || (opt = {}); + opt.rootBBox || (opt.rootBBox = g.Rect()); + opt.selectors || (opt.selectors = this.selectors); // selector collection to use - var y = options.oy % options.height; - if (y < 0) y += options.height; + // Cache table for query results and bounding box calculation. + // Note that `selectorCache` needs to be invalidated for all + // `updateAttributes` calls, as the selectors might pointing + // to nodes designated by an attribute or elements dynamically + // created. + var selectorCache = {}; + var bboxCache = {}; + var relativeItems = []; + var item, node, nodeAttrs, nodeData, processedAttrs; - patternDefVel.attr({ - x: x, - y: y, - width: options.width, - height: options.height - }); - }); + var roAttrs = opt.roAttributes; + var nodesAttrs = this.findNodesAttributes(roAttrs || attrs, rootNode, selectorCache, opt.selectors); + // `nodesAttrs` are different from all attributes, when + // rendering only attributes sent to this method. + var nodesAllAttrs = (roAttrs) + ? nodesAllAttrs = this.findNodesAttributes(attrs, rootNode, selectorCache, opt.selectors) + : nodesAttrs; - var patternUri = new XMLSerializer().serializeToString(refs.root.node); - patternUri = 'url(data:image/svg+xml;base64,' + btoa(patternUri) + ')'; + for (var nodeId in nodesAttrs) { + nodeData = nodesAttrs[nodeId]; + nodeAttrs = nodeData.attributes; + node = nodeData.node; + processedAttrs = this.processNodeAttributes(node, nodeAttrs); - this.$grid.css('backgroundImage', patternUri); + if (!processedAttrs.set && !processedAttrs.position && !processedAttrs.offset) { + // Set all the normal attributes right on the SVG/HTML element. + this.setNodeAttributes(node, processedAttrs.normal); - return this; - }, + } else { - updateBackgroundImage: function(opt) { + var nodeAllAttrs = nodesAllAttrs[nodeId] && nodesAllAttrs[nodeId].attributes; + var refSelector = (nodeAllAttrs && (nodeAttrs.ref === undefined)) + ? nodeAllAttrs.ref + : nodeAttrs.ref; - opt = opt || {}; + var refNode; + if (refSelector) { + refNode = (selectorCache[refSelector] || this.findBySelector(refSelector, rootNode, opt.selectors))[0]; + if (!refNode) { + throw new Error('dia.ElementView: "' + refSelector + '" reference does not exist.'); + } + } else { + refNode = null; + } - var backgroundPosition = opt.position || 'center'; - var backgroundSize = opt.size || 'auto auto'; + item = { + node: node, + refNode: refNode, + processedAttributes: processedAttrs, + allAttributes: nodeAllAttrs + }; - var currentScale = this.scale(); - var currentTranslate = this.translate(); + // If an element in the list is positioned relative to this one, then + // we want to insert this one before it in the list. + var itemIndex = relativeItems.findIndex(function(item) { + return item.refNode === node; + }); - // backgroundPosition - if (joint.util.isObject(backgroundPosition)) { - var x = currentTranslate.tx + (currentScale.sx * (backgroundPosition.x || 0)); - var y = currentTranslate.ty + (currentScale.sy * (backgroundPosition.y || 0)); - backgroundPosition = x + 'px ' + y + 'px'; + if (itemIndex > -1) { + relativeItems.splice(itemIndex, 0, item); + } else { + relativeItems.push(item); + } + } } - // backgroundSize - if (joint.util.isObject(backgroundSize)) { - backgroundSize = g.rect(backgroundSize).scale(currentScale.sx, currentScale.sy); - backgroundSize = backgroundSize.width + 'px ' + backgroundSize.height + 'px'; - } + for (var i = 0, n = relativeItems.length; i < n; i++) { + item = relativeItems[i]; + node = item.node; + refNode = item.refNode; - this.$background.css({ - backgroundSize: backgroundSize, - backgroundPosition: backgroundPosition - }); - }, + // Find the reference element bounding box. If no reference was provided, we + // use the optional bounding box. + var refNodeId = refNode ? V.ensureId(refNode) : ''; + var refBBox = bboxCache[refNodeId]; + if (!refBBox) { + // Get the bounding box of the reference element relative to the `rotatable` `<g>` (without rotation) + // or to the root `<g>` element if no rotatable group present if reference node present. + // Uses the bounding box provided. + refBBox = bboxCache[refNodeId] = (refNode) + ? V(refNode).getBBox({ target: (opt.rotatableNode || rootNode) }) + : opt.rootBBox; + } - drawBackgroundImage: function(img, opt) { + if (roAttrs) { + // if there was a special attribute affecting the position amongst passed-in attributes + // we have to merge it with the rest of the element's attributes as they are necessary + // to update the position relatively (i.e `ref-x` && 'ref-dx') + processedAttrs = this.processNodeAttributes(node, item.allAttributes); + this.mergeProcessedAttributes(processedAttrs, item.processedAttributes); - // Clear the background image if no image provided - if (!(img instanceof HTMLImageElement)) { - this.$background.css('backgroundImage', ''); - return; + } else { + processedAttrs = item.processedAttributes; + } + + this.updateRelativeAttributes(node, processedAttrs, refBBox, opt); } + }, - opt = opt || {}; + mergeProcessedAttributes: function(processedAttrs, roProcessedAttrs) { - var backgroundImage; - var backgroundSize = opt.size; - var backgroundRepeat = opt.repeat || 'no-repeat'; - var backgroundOpacity = opt.opacity || 1; - var backgroundQuality = Math.abs(opt.quality) || 1; - var backgroundPattern = this.constructor.backgroundPatterns[joint.util.camelCase(backgroundRepeat)]; + processedAttrs.set || (processedAttrs.set = {}); + processedAttrs.position || (processedAttrs.position = {}); + processedAttrs.offset || (processedAttrs.offset = {}); - if (joint.util.isFunction(backgroundPattern)) { - // 'flip-x', 'flip-y', 'flip-xy', 'watermark' and custom - img.width *= backgroundQuality; - img.height *= backgroundQuality; - var canvas = backgroundPattern(img, opt); - if (!(canvas instanceof HTMLCanvasElement)) { - throw new Error('dia.Paper: background pattern must return an HTML Canvas instance'); - } + joint.util.assign(processedAttrs.set, roProcessedAttrs.set); + joint.util.assign(processedAttrs.position, roProcessedAttrs.position); + joint.util.assign(processedAttrs.offset, roProcessedAttrs.offset); - backgroundImage = canvas.toDataURL('image/png'); - backgroundRepeat = 'repeat'; - if (joint.util.isObject(backgroundSize)) { - // recalculate the tile size if an object passed in - backgroundSize.width *= canvas.width / img.width; - backgroundSize.height *= canvas.height / img.height; - } else if (backgroundSize === undefined) { - // calcule the tile size if no provided - opt.size = { - width: canvas.width / backgroundQuality, - height: canvas.height / backgroundQuality - }; - } - } else { - // backgroundRepeat: - // no-repeat', 'round', 'space', 'repeat', 'repeat-x', 'repeat-y' - backgroundImage = img.src; - if (backgroundSize === undefined) { - // pass the image size for the backgroundSize if no size provided - opt.size = { - width: img.width, - height: img.height - }; - } + // Handle also the special transform property. + var transform = processedAttrs.normal && processedAttrs.normal.transform; + if (transform !== undefined && roProcessedAttrs.normal) { + roProcessedAttrs.normal.transform = transform; } + processedAttrs.normal = roProcessedAttrs.normal; + }, - this.$background.css({ - opacity: backgroundOpacity, - backgroundRepeat: backgroundRepeat, - backgroundImage: 'url(' + backgroundImage + ')' - }); - - this.updateBackgroundImage(opt); + onRemove: function() { + this.removeTools(); }, - updateBackgroundColor: function(color) { + _toolsView: null, - this.$el.css('backgroundColor', color || ''); + hasTools: function(name) { + var toolsView = this._toolsView; + if (!toolsView) return false; + if (!name) return true; + return (toolsView.getName() === name); }, - drawBackground: function(opt) { - - opt = opt || {}; + addTools: function(toolsView) { - this.updateBackgroundColor(opt.color); + this.removeTools(); - if (opt.image) { - opt = this._background = joint.util.cloneDeep(opt); - var img = document.createElement('img'); - img.onload = this.drawBackgroundImage.bind(this, img, opt); - img.src = opt.image; - } else { - this.drawBackgroundImage(null); - this._background = null; + if (toolsView instanceof joint.dia.ToolsView) { + this._toolsView = toolsView; + toolsView.configure({ relatedView: this }); + toolsView.listenTo(this.paper, 'tools:event', this.onToolEvent.bind(this)); + toolsView.mount(); } - return this; }, - setInteractivity: function(value) { + updateTools: function(opt) { - this.options.interactive = value; + var toolsView = this._toolsView; + if (toolsView) toolsView.update(opt); + return this; + }, - joint.util.invoke(this._views, 'setInteractivity', value); + removeTools: function() { + + var toolsView = this._toolsView; + if (toolsView) { + toolsView.remove(); + this._toolsView = null; + } + return this; }, - // Paper Defs + hideTools: function() { - isDefined: function(defId) { - return !!this.svg.getElementById(defId); + var toolsView = this._toolsView; + if (toolsView) toolsView.hide(); + return this; }, - defineFilter: function(filter) { + showTools: function() { - if (!joint.util.isObject(filter)) { - throw new TypeError('dia.Paper: defineFilter() requires 1. argument to be an object.'); - } + var toolsView = this._toolsView; + if (toolsView) toolsView.show(); + return this; + }, - var filterId = filter.id; - var name = filter.name; - // Generate a hash code from the stringified filter definition. This gives us - // a unique filter ID for different definitions. - if (!filterId) { - filterId = name + this.svg.id + joint.util.hashCode(JSON.stringify(filter)); + onToolEvent: function(event) { + switch (event) { + case 'remove': + this.removeTools(); + break; + case 'hide': + this.hideTools(); + break; + case 'show': + this.showTools(); + break; } - // If the filter already exists in the document, - // we're done and we can just use it (reference it using `url()`). - // If not, create one. - if (!this.isDefined(filterId)) { + }, - var namespace = joint.util.filter; - var filterSVGString = namespace[name] && namespace[name](filter.args || {}); - if (!filterSVGString) { - throw new Error('Non-existing filter ' + name); - } + // Interaction. The controller part. + // --------------------------------- - // Set the filter area to be 3x the bounding box of the cell - // and center the filter around the cell. - var filterAttrs = joint.util.assign({ - filterUnits: 'objectBoundingBox', - x: -1, - y: -1, - width: 3, - height: 3 - }, filter.attrs, { - id: filterId - }); + // Interaction is handled by the paper and delegated to the view in interest. + // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. + // If necessary, real coordinates can be obtained from the `evt` event object. - V(filterSVGString, filterAttrs).appendTo(this.defs); - } + // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, + // i.e. `joint.dia.Element` and `joint.dia.Link`. - return filterId; - }, + pointerdblclick: function(evt, x, y) { - defineGradient: function(gradient) { + this.notify('cell:pointerdblclick', evt, x, y); + }, - if (!joint.util.isObject(gradient)) { - throw new TypeError('dia.Paper: defineGradient() requires 1. argument to be an object.'); - } + pointerclick: function(evt, x, y) { - var gradientId = gradient.id; - var type = gradient.type; - var stops = gradient.stops; - // Generate a hash code from the stringified filter definition. This gives us - // a unique filter ID for different definitions. - if (!gradientId) { - gradientId = type + this.svg.id + joint.util.hashCode(JSON.stringify(gradient)); - } - // If the gradient already exists in the document, - // we're done and we can just use it (reference it using `url()`). - // If not, create one. - if (!this.isDefined(gradientId)) { + this.notify('cell:pointerclick', evt, x, y); + }, - var stopTemplate = joint.util.template('<stop offset="${offset}" stop-color="${color}" stop-opacity="${opacity}"/>'); - var gradientStopsStrings = joint.util.toArray(stops).map(function(stop) { - return stopTemplate({ - offset: stop.offset, - color: stop.color, - opacity: Number.isFinite(stop.opacity) ? stop.opacity : 1 - }); - }); + contextmenu: function(evt, x, y) { - var gradientSVGString = [ - '<' + type + '>', - gradientStopsStrings.join(''), - '</' + type + '>' - ].join(''); + this.notify('cell:contextmenu', evt, x, y); + }, - var gradientAttrs = joint.util.assign({ id: gradientId }, gradient.attrs); + pointerdown: function(evt, x, y) { - V(gradientSVGString, gradientAttrs).appendTo(this.defs); + if (this.model.graph) { + this.model.startBatch('pointer'); + this._graph = this.model.graph; } - return gradientId; + this.notify('cell:pointerdown', evt, x, y); }, - defineMarker: function(marker) { + pointermove: function(evt, x, y) { - if (!joint.util.isObject(marker)) { - throw new TypeError('dia.Paper: defineMarker() requires 1. argument to be an object.'); - } + this.notify('cell:pointermove', evt, x, y); + }, - var markerId = marker.id; + pointerup: function(evt, x, y) { - // Generate a hash code from the stringified filter definition. This gives us - // a unique filter ID for different definitions. - if (!markerId) { - markerId = this.svg.id + joint.util.hashCode(JSON.stringify(marker)); + this.notify('cell:pointerup', evt, x, y); + + if (this._graph) { + // we don't want to trigger event on model as model doesn't + // need to be member of collection anymore (remove) + this._graph.stopBatch('pointer', { cell: this.model }); + delete this._graph; } + }, - if (!this.isDefined(markerId)) { + mouseover: function(evt) { - var attrs = joint.util.omit(marker, 'type', 'userSpaceOnUse'); - var pathMarker = V('marker', { - id: markerId, - orient: 'auto', - overflow: 'visible', - markerUnits: marker.markerUnits || 'userSpaceOnUse' - }, [ - V(marker.type || 'path', attrs) - ]); + this.notify('cell:mouseover', evt); + }, - pathMarker.appendTo(this.defs); - } + mouseout: function(evt) { - return markerId; - } + this.notify('cell:mouseout', evt); + }, -}, { + mouseenter: function(evt) { - backgroundPatterns: { + this.notify('cell:mouseenter', evt); + }, - flipXy: function(img) { - // d b - // q p - - var canvas = document.createElement('canvas'); - var imgWidth = img.width; - var imgHeight = img.height; - - canvas.width = 2 * imgWidth; - canvas.height = 2 * imgHeight; + mouseleave: function(evt) { - var ctx = canvas.getContext('2d'); - // top-left image - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); - // xy-flipped bottom-right image - ctx.setTransform(-1, 0, 0, -1, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); - // x-flipped top-right image - ctx.setTransform(-1, 0, 0, 1, canvas.width, 0); - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); - // y-flipped bottom-left image - ctx.setTransform(1, 0, 0, -1, 0, canvas.height); - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + this.notify('cell:mouseleave', evt); + }, - return canvas; - }, + mousewheel: function(evt, x, y, delta) { - flipX: function(img) { - // d b - // d b + this.notify('cell:mousewheel', evt, x, y, delta); + }, - var canvas = document.createElement('canvas'); - var imgWidth = img.width; - var imgHeight = img.height; + onevent: function(evt, eventName, x, y) { - canvas.width = imgWidth * 2; - canvas.height = imgHeight; + this.notify(eventName, evt, x, y); + }, - var ctx = canvas.getContext('2d'); - // left image - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); - // flipped right image - ctx.translate(2 * imgWidth, 0); - ctx.scale(-1, 1); - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + onmagnet: function() { - return canvas; - }, + // noop + }, - flipY: function(img) { - // d d - // q q + setInteractivity: function(value) { - var canvas = document.createElement('canvas'); - var imgWidth = img.width; - var imgHeight = img.height; + this.options.interactive = value; + } +}, { - canvas.width = imgWidth; - canvas.height = imgHeight * 2; + dispatchToolsEvent: function(paper, event) { + if ((typeof event === 'string') && (paper instanceof joint.dia.Paper)) { + paper.trigger('tools:event', event); + } + } +}); - var ctx = canvas.getContext('2d'); - // top image - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); - // flipped bottom image - ctx.translate(0, 2 * imgHeight); - ctx.scale(1, -1); - ctx.drawImage(img, 0, 0, imgWidth, imgHeight); - return canvas; - }, +// joint.dia.Element base model. +// ----------------------------- - watermark: function(img, opt) { - // d - // d +joint.dia.Element = joint.dia.Cell.extend({ - opt = opt || {}; + defaults: { + position: { x: 0, y: 0 }, + size: { width: 1, height: 1 }, + angle: 0 + }, - var imgWidth = img.width; - var imgHeight = img.height; + initialize: function() { - var canvas = document.createElement('canvas'); - canvas.width = imgWidth * 3; - canvas.height = imgHeight * 3; + this._initializePorts(); + joint.dia.Cell.prototype.initialize.apply(this, arguments); + }, - var ctx = canvas.getContext('2d'); - var angle = joint.util.isNumber(opt.watermarkAngle) ? -opt.watermarkAngle : -20; - var radians = g.toRad(angle); - var stepX = canvas.width / 4; - var stepY = canvas.height / 4; + /** + * @abstract + */ + _initializePorts: function() { + // implemented in ports.js + }, - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 4; j++) { - if ((i + j) % 2 > 0) { - // reset the current transformations - ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY); - ctx.rotate(radians); - ctx.drawImage(img, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight); - } - } - } + isElement: function() { - return canvas; - } + return true; }, - gridPatterns: { - dot: [{ - color: '#AAAAAA', - thickness: 1, - markup: 'rect', - update: function(el, opt) { - V(el).attr({ - width: opt.thickness * opt.sx, - height: opt.thickness * opt.sy, - fill: opt.color - }); - } - }], - fixedDot: [{ - color: '#AAAAAA', - thickness: 1, - markup: 'rect', - update: function(el, opt) { - var size = opt.sx <= 1 ? opt.thickness * opt.sx : opt.thickness; - V(el).attr({ width: size, height: size, fill: opt.color }); - } - }], - mesh: [{ - color: '#AAAAAA', - thickness: 1, - markup: 'path', - update: function(el, opt) { - - var d; - var width = opt.width; - var height = opt.height; - var thickness = opt.thickness; + position: function(x, y, opt) { - if (width - thickness >= 0 && height - thickness >= 0) { - d = ['M', width, 0, 'H0 M0 0 V0', height].join(' '); - } else { - d = 'M 0 0 0 0'; - } + var isSetter = joint.util.isNumber(y); - V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness }); - } - }], - doubleMesh: [{ - color: '#AAAAAA', - thickness: 1, - markup: 'path', - update: function(el, opt) { + opt = (isSetter ? opt : x) || {}; - var d; - var width = opt.width; - var height = opt.height; - var thickness = opt.thickness; + // option `parentRelative` for setting the position relative to the element's parent. + if (opt.parentRelative) { - if (width - thickness >= 0 && height - thickness >= 0) { - d = ['M', width, 0, 'H0 M0 0 V0', height].join(' '); - } else { - d = 'M 0 0 0 0'; - } + // Getting the parent's position requires the collection. + // Cell.parent() holds cell id only. + if (!this.graph) throw new Error('Element must be part of a graph.'); - V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness }); - } - }, { - color: '#000000', - thickness: 3, - scaleFactor: 4, - markup: 'path', - update: function(el, opt) { + var parent = this.getParentCell(); + var parentPosition = parent && !parent.isLink() + ? parent.get('position') + : { x: 0, y: 0 }; + } - var d; - var width = opt.width; - var height = opt.height; - var thickness = opt.thickness; + if (isSetter) { - if (width - thickness >= 0 && height - thickness >= 0) { - d = ['M', width, 0, 'H0 M0 0 V0', height].join(' '); - } else { - d = 'M 0 0 0 0'; - } + if (opt.parentRelative) { + x += parentPosition.x; + y += parentPosition.y; + } - V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness }); + if (opt.deep) { + var currentPosition = this.get('position'); + this.translate(x - currentPosition.x, y - currentPosition.y, opt); + } else { + this.set('position', { x: x, y: y }, opt); } - }] - } -}); -(function(joint, _, util) { + return this; - var PortData = function(data) { + } else { // Getter returns a geometry point. - var clonedData = util.cloneDeep(data) || {}; - this.ports = []; - this.groups = {}; - this.portLayoutNamespace = joint.layout.Port; - this.portLabelLayoutNamespace = joint.layout.PortLabel; + var elementPosition = g.point(this.get('position')); - this._init(clonedData); - }; + return opt.parentRelative + ? elementPosition.difference(parentPosition) + : elementPosition; + } + }, - PortData.prototype = { + translate: function(tx, ty, opt) { - getPorts: function() { - return this.ports; - }, + tx = tx || 0; + ty = ty || 0; - getGroup: function(name) { - return this.groups[name] || {}; - }, + if (tx === 0 && ty === 0) { + // Like nothing has happened. + return this; + } - getPortsByGroup: function(groupName) { + opt = opt || {}; + // Pass the initiator of the translation. + opt.translateBy = opt.translateBy || this.id; - return this.ports.filter(function(port) { - return port.group === groupName; - }); - }, + var position = this.get('position') || { x: 0, y: 0 }; - getGroupPortsMetrics: function(groupName, elBBox) { - - var group = this.getGroup(groupName); - var ports = this.getPortsByGroup(groupName); + if (opt.restrictedArea && opt.translateBy === this.id) { - var groupPosition = group.position || {}; - var groupPositionName = groupPosition.name; - var namespace = this.portLayoutNamespace; - if (!namespace[groupPositionName]) { - groupPositionName = 'left'; - } + // We are restricting the translation for the element itself only. We get + // the bounding box of the element including all its embeds. + // All embeds have to be translated the exact same way as the element. + var bbox = this.getBBox({ deep: true }); + var ra = opt.restrictedArea; + //- - - - - - - - - - - - -> ra.x + ra.width + // - - - -> position.x | + // -> bbox.x + // ▓▓▓▓▓▓▓ | + // ░░░░░░░▓▓▓▓▓▓▓ + // ░░░░░░░░░ | + // ▓▓▓▓▓▓▓▓░░░░░░░ + // ▓▓▓▓▓▓▓▓ | + // <-dx-> | restricted area right border + // <-width-> | ░ translated element + // <- - bbox.width - -> ▓ embedded element + var dx = position.x - bbox.x; + var dy = position.y - bbox.y; + // Find the maximal/minimal coordinates that the element can be translated + // while complies the restrictions. + var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); + var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); + // recalculate the translation taking the resctrictions into account. + tx = x - position.x; + ty = y - position.y; + } - var groupArgs = groupPosition.args || {}; - var portsArgs = ports.map(function(port) { - return port && port.position && port.position.args; - }); - var groupPortTransformations = namespace[groupPositionName](portsArgs, elBBox, groupArgs); + var translatedPosition = { + x: position.x + tx, + y: position.y + ty + }; - var accumulator = { - ports: ports, - result: [] - }; + // To find out by how much an element was translated in event 'change:position' handlers. + opt.tx = tx; + opt.ty = ty; - util.toArray(groupPortTransformations).reduce(function(res, portTransformation, index) { - var port = res.ports[index]; - res.result.push({ - portId: port.id, - portTransformation: portTransformation, - labelTransformation: this._getPortLabelLayout(port, g.Point(portTransformation), elBBox), - portAttrs: port.attrs, - portSize: port.size, - labelSize: port.label.size - }); - return res; - }.bind(this), accumulator); + if (opt.transition) { - return accumulator.result; - }, + if (!joint.util.isObject(opt.transition)) opt.transition = {}; - _getPortLabelLayout: function(port, portPosition, elBBox) { + this.transition('position', translatedPosition, joint.util.assign({}, opt.transition, { + valueFunction: joint.util.interpolate.object + })); - var namespace = this.portLabelLayoutNamespace; - var labelPosition = port.label.position.name || 'left'; + } else { - if (namespace[labelPosition]) { - return namespace[labelPosition](portPosition, elBBox, port.label.position.args); - } + this.set('position', translatedPosition, opt); + } - return null; - }, + // Recursively call `translate()` on all the embeds cells. + joint.util.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); - _init: function(data) { + return this; + }, - // prepare groups - if (util.isObject(data.groups)) { - var groups = Object.keys(data.groups); - for (var i = 0, n = groups.length; i < n; i++) { - var key = groups[i]; - this.groups[key] = this._evaluateGroup(data.groups[key]); - } - } + size: function(width, height, opt) { - // prepare ports - var ports = util.toArray(data.items); - for (var j = 0, m = ports.length; j < m; j++) { - this.ports.push(this._evaluatePort(ports[j])); - } - }, + var currentSize = this.get('size'); + // Getter + // () signature + if (width === undefined) { + return { + width: currentSize.width, + height: currentSize.height + }; + } + // Setter + // (size, opt) signature + if (joint.util.isObject(width)) { + opt = height; + height = joint.util.isNumber(width.height) ? width.height : currentSize.height; + width = joint.util.isNumber(width.width) ? width.width : currentSize.width; + } - _evaluateGroup: function(group) { + return this.resize(width, height, opt); + }, - return util.merge(group, { - position: this._getPosition(group.position, true), - label: this._getLabel(group, true) - }); - }, + resize: function(width, height, opt) { - _evaluatePort: function(port) { + opt = opt || {}; - var evaluated = util.assign({}, port); + this.startBatch('resize', opt); - var group = this.getGroup(port.group); + if (opt.direction) { - evaluated.markup = evaluated.markup || group.markup; - evaluated.attrs = util.merge({}, group.attrs, evaluated.attrs); - evaluated.position = this._createPositionNode(group, evaluated); - evaluated.label = util.merge({}, group.label, this._getLabel(evaluated)); - evaluated.z = this._getZIndex(group, evaluated); - evaluated.size = util.assign({}, group.size, evaluated.size); + var currentSize = this.get('size'); - return evaluated; - }, + switch (opt.direction) { - _getZIndex: function(group, port) { + case 'left': + case 'right': + // Don't change height when resizing horizontally. + height = currentSize.height; + break; - if (util.isNumber(port.z)) { - return port.z; - } - if (util.isNumber(group.z) || group.z === 'auto') { - return group.z; + case 'top': + case 'bottom': + // Don't change width when resizing vertically. + width = currentSize.width; + break; } - return 'auto'; - }, - - _createPositionNode: function(group, port) { - return util.merge({ - name: 'left', - args: {} - }, group.position, { args: port.args }); - }, + // Get the angle and clamp its value between 0 and 360 degrees. + var angle = g.normalizeAngle(this.get('angle') || 0); - _getPosition: function(position, setDefault) { + var quadrant = { + 'top-right': 0, + 'right': 0, + 'top-left': 1, + 'top': 1, + 'bottom-left': 2, + 'left': 2, + 'bottom-right': 3, + 'bottom': 3 + }[opt.direction]; - var args = {}; - var positionName; + if (opt.absolute) { - if (util.isFunction(position)) { - positionName = 'fn'; - args.fn = position; - } else if (util.isString(position)) { - positionName = position; - } else if (position === undefined) { - positionName = setDefault ? 'left' : null; - } else if (Array.isArray(position)) { - positionName = 'absolute'; - args.x = position[0]; - args.y = position[1]; - } else if (util.isObject(position)) { - positionName = position.name; - util.assign(args, position.args); + // We are taking the element's rotation into account + quadrant += Math.floor((angle + 45) / 90); + quadrant %= 4; } - var result = { args: args }; + // This is a rectangle in size of the unrotated element. + var bbox = this.getBBox(); - if (positionName) { - result.name = positionName; - } - return result; - }, + // Pick the corner point on the element, which meant to stay on its place before and + // after the rotation. + var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); - _getLabel: function(item, setDefaults) { + // Find an image of the previous indent point. This is the position, where is the + // point actually located on the screen. + var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); - var label = item.label || {}; + // Every point on the element rotates around a circle with the centre of rotation + // in the middle of the element while the whole element is being rotated. That means + // that the distance from a point in the corner of the element (supposed its always rect) to + // the center of the element doesn't change during the rotation and therefore it equals + // to a distance on unrotated element. + // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. + var radius = Math.sqrt((width * width) + (height * height)) / 2; - var ret = label; - ret.position = this._getPosition(label.position, setDefaults); + // Now we are looking for an angle between x-axis and the line starting at image of fixed point + // and ending at the center of the element. We call this angle `alpha`. - return ret; - } - }; + // The image of a fixed point is located in n-th quadrant. For each quadrant passed + // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. + // + // 3 | 2 + // --c-- Quadrant positions around the element's center `c` + // 0 | 1 + // + var alpha = quadrant * Math.PI / 2; - util.assign(joint.dia.Element.prototype, { + // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis + // going through the center of the element) and line crossing the indent of the fixed point and the center + // of the element. This is the angle we need but on the unrotated element. + alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); - _initializePorts: function() { + // Lastly we have to deduct the original angle the element was rotated by and that's it. + alpha -= g.toRad(angle); - this._createPortData(); - this.on('change:ports', function() { + // With this angle and distance we can easily calculate the centre of the unrotated element. + // Note that fromPolar constructor accepts an angle in radians. + var center = g.point.fromPolar(radius, alpha, imageFixedPoint); - this._processRemovedPort(); - this._createPortData(); - }, this); - }, + // The top left corner on the unrotated element has to be half a width on the left + // and half a height to the top from the center. This will be the origin of rectangle + // we were looking for. + var origin = g.point(center).offset(width / -2, height / -2); - /** - * remove links tied wiht just removed element - * @private - */ - _processRemovedPort: function() { + // Resize the element (before re-positioning it). + this.set('size', { width: width, height: height }, opt); - var current = this.get('ports') || {}; - var currentItemsMap = {}; + // Finally, re-position the element. + this.position(origin.x, origin.y, opt); - util.toArray(current.items).forEach(function(item) { - currentItemsMap[item.id] = true; - }); + } else { - var previous = this.previous('ports') || {}; - var removed = {}; + // Resize the element. + this.set('size', { width: width, height: height }, opt); + } - util.toArray(previous.items).forEach(function(item) { - if (!currentItemsMap[item.id]) { - removed[item.id] = true; - } - }); + this.stopBatch('resize', opt); - var graph = this.graph; - if (graph && !util.isEmpty(removed)) { + return this; + }, - var inboundLinks = graph.getConnectedLinks(this, { inbound: true }); - inboundLinks.forEach(function(link) { + scale: function(sx, sy, origin, opt) { - if (removed[link.get('target').port]) link.remove(); - }); + var scaledBBox = this.getBBox().scale(sx, sy, origin); + this.startBatch('scale', opt); + this.position(scaledBBox.x, scaledBBox.y, opt); + this.resize(scaledBBox.width, scaledBBox.height, opt); + this.stopBatch('scale'); + return this; + }, - var outboundLinks = graph.getConnectedLinks(this, { outbound: true }); - outboundLinks.forEach(function(link) { + fitEmbeds: function(opt) { - if (removed[link.get('source').port]) link.remove(); - }); - } - }, + opt = opt || {}; - /** - * @returns {boolean} - */ - hasPorts: function() { + // Getting the children's size and position requires the collection. + // Cell.get('embdes') helds an array of cell ids only. + if (!this.graph) throw new Error('Element must be part of a graph.'); - return this.prop('ports/items').length > 0; - }, + var embeddedCells = this.getEmbeddedCells(); - /** - * @param {string} id - * @returns {boolean} - */ - hasPort: function(id) { + if (embeddedCells.length > 0) { - return this.getPortIndex(id) !== -1; - }, + this.startBatch('fit-embeds', opt); - /** - * @returns {Array<object>} - */ - getPorts: function() { + if (opt.deep) { + // Recursively apply fitEmbeds on all embeds first. + joint.util.invoke(embeddedCells, 'fitEmbeds', opt); + } - return util.cloneDeep(this.prop('ports/items')) || []; - }, + // Compute cell's size and position based on the children bbox + // and given padding. + var bbox = this.graph.getCellsBBox(embeddedCells); + var padding = joint.util.normalizeSides(opt.padding); - /** - * @param {string} id - * @returns {object} - */ - getPort: function(id) { + // Apply padding computed above to the bbox. + bbox.moveAndExpand({ + x: -padding.left, + y: -padding.top, + width: padding.right + padding.left, + height: padding.bottom + padding.top + }); - return util.cloneDeep(util.toArray(this.prop('ports/items')).find( function(port) { - return port.id && port.id === id; - })); - }, + // Set new element dimensions finally. + this.set({ + position: { x: bbox.x, y: bbox.y }, + size: { width: bbox.width, height: bbox.height } + }, opt); - /** - * @param {string} groupName - * @returns {Object<portId, {x: number, y: number, angle: number}>} - */ - getPortsPositions: function(groupName) { + this.stopBatch('fit-embeds'); + } - var portsMetrics = this._portSettingsData.getGroupPortsMetrics(groupName, g.Rect(this.size())); + return this; + }, - return portsMetrics.reduce(function(positions, metrics) { - var transformation = metrics.portTransformation; - positions[metrics.portId] = { - x: transformation.x, - y: transformation.y, - angle: transformation.angle - }; - return positions; - }, {}); - }, + // Rotate element by `angle` degrees, optionally around `origin` point. + // If `origin` is not provided, it is considered to be the center of the element. + // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not + // the difference from the previous angle. + rotate: function(angle, absolute, origin, opt) { - /** - * @param {string|Port} port port id or port - * @returns {number} port index - */ - getPortIndex: function(port) { + if (origin) { - var id = util.isObject(port) ? port.id : port; + var center = this.getBBox().center(); + var size = this.get('size'); + var position = this.get('position'); + center.rotate(origin, this.get('angle') - angle); + var dx = center.x - size.width / 2 - position.x; + var dy = center.y - size.height / 2 - position.y; + this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); + this.position(position.x + dx, position.y + dy, opt); + this.rotate(angle, absolute, null, opt); + this.stopBatch('rotate'); - if (!this._isValidPortId(id)) { - return -1; - } + } else { - return util.toArray(this.prop('ports/items')).findIndex(function(item) { - return item.id === id; - }); - }, + this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360, opt); + } - /** - * @param {object} port - * @param {object} [opt] - * @returns {joint.dia.Element} - */ - addPort: function(port, opt) { + return this; + }, - if (!util.isObject(port) || Array.isArray(port)) { - throw new Error('Element: addPort requires an object.'); - } + angle: function() { + return g.normalizeAngle(this.get('angle') || 0); + }, - var ports = util.assign([], this.prop('ports/items')); - ports.push(port); - this.prop('ports/items', ports, opt); + getBBox: function(opt) { - return this; - }, + opt = opt || {}; - /** - * @param {string} portId - * @param {string|object=} path - * @param {*=} value - * @param {object=} opt - * @returns {joint.dia.Element} - */ - portProp: function(portId, path, value, opt) { + if (opt.deep && this.graph) { - var index = this.getPortIndex(portId); + // Get all the embedded elements using breadth first algorithm, + // that doesn't use recursion. + var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + // Add the model itself. + elements.push(this); - if (index === -1) { - throw new Error('Element: unable to find port with id ' + portId); - } + return this.graph.getCellsBBox(elements); + } - var args = Array.prototype.slice.call(arguments, 1); - if (Array.isArray(path)) { - args[0] = ['ports', 'items', index].concat(path); - } else if (util.isString(path)) { + var position = this.get('position'); + var size = this.get('size'); - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. - args[0] = ['ports/items/', index, '/', path].join(''); + return new g.Rect(position.x, position.y, size.width, size.height); + } +}); - } else { +// joint.dia.Element base view and controller. +// ------------------------------------------- - args = ['ports/items/' + index]; - if (util.isPlainObject(path)) { - args.push(path); - args.push(value); - } - } +joint.dia.ElementView = joint.dia.CellView.extend({ - return this.prop.apply(this, args); - }, + /** + * @abstract + */ + _removePorts: function() { + // implemented in ports.js + }, - _validatePorts: function() { + /** + * + * @abstract + */ + _renderPorts: function() { + // implemented in ports.js + }, - var portsAttr = this.get('ports') || {}; + className: function() { - var errorMessages = []; - portsAttr = portsAttr || {}; - var ports = util.toArray(portsAttr.items); + var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); - ports.forEach(function(p) { + classNames.push('element'); - if (typeof p !== 'object') { - errorMessages.push('Element: invalid port ', p); - } + return classNames.join(' '); + }, - if (!this._isValidPortId(p.id)) { - p.id = util.uuid(); - } - }, this); + metrics: null, - if (joint.util.uniq(ports, 'id').length !== ports.length) { - errorMessages.push('Element: found id duplicities in ports.'); - } + initialize: function() { - return errorMessages; - }, + joint.dia.CellView.prototype.initialize.apply(this, arguments); - /** - * @param {string} id port id - * @returns {boolean} - * @private - */ - _isValidPortId: function(id) { + var model = this.model; - return id !== null && id !== undefined && !util.isObject(id); - }, + this.listenTo(model, 'change:position', this.translate); + this.listenTo(model, 'change:size', this.resize); + this.listenTo(model, 'change:angle', this.rotate); + this.listenTo(model, 'change:markup', this.render); - addPorts: function(ports, opt) { + this._initializePorts(); - if (ports.length) { - this.prop('ports/items', util.assign([], this.prop('ports/items')).concat(ports), opt); - } + this.metrics = {}; + }, - return this; - }, + /** + * @abstract + */ + _initializePorts: function() { - removePort: function(port, opt) { + }, - var options = opt || {}; - var ports = util.assign([], this.prop('ports/items')); + update: function(cell, renderingOnlyAttrs) { - var index = this.getPortIndex(port); + this.metrics = {}; - if (index !== -1) { - ports.splice(index, 1); - options.rewrite = true; - this.prop('ports/items', ports, options); - } + this._removePorts(); - return this; - }, + var model = this.model; + var modelAttrs = model.attr(); + this.updateDOMSubtreeAttributes(this.el, modelAttrs, { + rootBBox: new g.Rect(model.size()), + selectors: this.selectors, + scalableNode: this.scalableNode, + rotatableNode: this.rotatableNode, + // Use rendering only attributes if they differs from the model attributes + roAttributes: (renderingOnlyAttrs === modelAttrs) ? null : renderingOnlyAttrs + }); - /** - * @private - */ - _createPortData: function() { + this._renderPorts(); + }, - var err = this._validatePorts(); + rotatableSelector: 'rotatable', + scalableSelector: 'scalable', + scalableNode: null, + rotatableNode: null, - if (err.length > 0) { - this.set('ports', this.previous('ports')); - throw new Error(err.join(' ')); - } + // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the + // default markup is not desirable. + renderMarkup: function() { - var prevPortData; + var element = this.model; + var markup = element.get('markup') || element.markup; + if (!markup) throw new Error('dia.ElementView: markup required'); + if (Array.isArray(markup)) return this.renderJSONMarkup(markup); + if (typeof markup === 'string') return this.renderStringMarkup(markup); + throw new Error('dia.ElementView: invalid markup'); + }, - if (this._portSettingsData) { + renderJSONMarkup: function(markup) { - prevPortData = this._portSettingsData.getPorts(); - } + var doc = joint.util.parseDOMJSON(markup); + // Selectors + var selectors = this.selectors = doc.selectors; + var rootSelector = this.selector; + if (selectors[rootSelector]) throw new Error('dia.ElementView: ambiguous root selector.'); + selectors[rootSelector] = this.el; + // Cache transformation groups + this.rotatableNode = V(selectors[this.rotatableSelector]) || null; + this.scalableNode = V(selectors[this.scalableSelector]) || null; + // Fragment + this.vel.append(doc.fragment); + }, - this._portSettingsData = new PortData(this.get('ports')); + renderStringMarkup: function(markup) { - var curPortData = this._portSettingsData.getPorts(); + var vel = this.vel; + vel.append(V(markup)); + // Cache transformation groups + this.rotatableNode = vel.findOne('.rotatable'); + this.scalableNode = vel.findOne('.scalable'); - if (prevPortData) { + var selectors = this.selectors = {}; + selectors[this.selector] = this.el; + }, - var added = curPortData.filter(function(item) { - if (!prevPortData.find(function(prevPort) { return prevPort.id === item.id;})) { - return item; - } - }); + render: function() { - var removed = prevPortData.filter(function(item) { - if (!curPortData.find(function(curPort) { return curPort.id === item.id;})) { - return item; - } - }); + this.vel.empty(); + this.renderMarkup(); + if (this.scalableNode) { + // Double update is necessary for elements with the scalable group only + // Note the resize() triggers the other `update`. + this.update(); + } + this.resize(); + if (this.rotatableNode) { + // Translate transformation is applied on `this.el` while the rotation transformation + // on `this.rotatableNode` + this.rotate(); + this.translate(); + return this; + } + this.updateTransformation(); + return this; + }, - if (removed.length > 0) { - this.trigger('ports:remove', this, removed); - } + resize: function() { - if (added.length > 0) { - this.trigger('ports:add', this, added); - } - } - } - }); + if (this.scalableNode) return this.sgResize.apply(this, arguments); + if (this.model.attributes.angle) this.rotate(); + this.update(); + }, - util.assign(joint.dia.ElementView.prototype, { + translate: function() { - portContainerMarkup: '<g class="joint-port"/>', - portMarkup: '<circle class="joint-port-body" r="10" fill="#FFFFFF" stroke="#000000"/>', - portLabelMarkup: '<text class="joint-port-label" fill="#000000"/>', - /** @type {Object<string, {portElement: Vectorizer, portLabelElement: Vectorizer}>} */ - _portElementsCache: null, + if (this.rotatableNode) return this.rgTranslate(); + this.updateTransformation(); + }, - /** - * @private - */ - _initializePorts: function() { + rotate: function() { - this._portElementsCache = {}; + if (this.rotatableNode) return this.rgRotate(); + this.updateTransformation(); + }, - this.listenTo(this.model, 'change:ports', function() { + updateTransformation: function() { - this._refreshPorts(); - }); - }, + var transformation = this.getTranslateString(); + var rotateString = this.getRotateString(); + if (rotateString) transformation += ' ' + rotateString; + this.vel.attr('transform', transformation); + }, - /** - * @typedef {Object} Port - * - * @property {string} id - * @property {Object} position - * @property {Object} label - * @property {Object} attrs - * @property {string} markup - * @property {string} group - */ + getTranslateString: function() { - /** - * @private - */ - _refreshPorts: function() { + var position = this.model.attributes.position; + return 'translate(' + position.x + ',' + position.y + ')'; + }, - this._removePorts(); - this._portElementsCache = {}; + getRotateString: function() { + var attributes = this.model.attributes; + var angle = attributes.angle; + if (!angle) return null; + var size = attributes.size; + return 'rotate(' + angle + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'; + }, - this._renderPorts(); - }, + getBBox: function(opt) { - /** - * @private - */ - _renderPorts: function() { + var bbox; + if (opt && opt.useModelGeometry) { + var model = this.model; + bbox = model.getBBox().bbox(model.angle()); + } else { + bbox = this.getNodeBBox(this.el); + } - // references to rendered elements without z-index - var elementReferences = []; - var elem = this._getContainerElement(); + return this.paper.localToPaperRect(bbox); + }, - for (var i = 0, count = elem.node.childNodes.length; i < count; i++) { - elementReferences.push(elem.node.childNodes[i]); - } + nodeCache: function(magnet) { - var portsGropsByZ = util.groupBy(this.model._portSettingsData.getPorts(), 'z'); - var withoutZKey = 'auto'; + var id = V.ensureId(magnet); + var metrics = this.metrics[id]; + if (!metrics) metrics = this.metrics[id] = {}; + return metrics; + }, - // render non-z first - util.toArray(portsGropsByZ[withoutZKey]).forEach(function(port) { - var portElement = this._getPortElement(port); - elem.append(portElement); - elementReferences.push(portElement); - }, this); + getNodeData: function(magnet) { - var groupNames = Object.keys(portsGropsByZ); - for (var k = 0; k < groupNames.length; k++) { - var groupName = groupNames[k]; - if (groupName !== withoutZKey) { - var z = parseInt(groupName, 10); - this._appendPorts(portsGropsByZ[groupName], z, elementReferences); - } - } + var metrics = this.nodeCache(magnet); + if (!metrics.data) metrics.data = {}; + return metrics.data; + }, - this._updatePorts(); - }, + getNodeBBox: function(magnet) { - /** - * @returns {V} - * @private - */ - _getContainerElement: function() { + var rect = this.getNodeBoundingRect(magnet); + var magnetMatrix = this.getNodeMatrix(magnet); + var translateMatrix = this.getRootTranslateMatrix(); + var rotateMatrix = this.getRootRotateMatrix(); + return V.transformRect(rect, translateMatrix.multiply(rotateMatrix).multiply(magnetMatrix)); + }, - return this.rotatableNode || this.vel; - }, + getNodeBoundingRect: function(magnet) { - /** - * @param {Array<Port>}ports - * @param {number} z - * @param refs - * @private - */ - _appendPorts: function(ports, z, refs) { + var metrics = this.nodeCache(magnet); + if (metrics.boundingRect === undefined) metrics.boundingRect = V(magnet).getBBox(); + return new g.Rect(metrics.boundingRect); + }, - var containerElement = this._getContainerElement(); - var portElements = util.toArray(ports).map(this._getPortElement, this); + getNodeUnrotatedBBox: function(magnet) { - if (refs[z] || z < 0) { - V(refs[Math.max(z, 0)]).before(portElements); - } else { - containerElement.append(portElements); - } - }, + var rect = this.getNodeBoundingRect(magnet); + var magnetMatrix = this.getNodeMatrix(magnet); + var translateMatrix = this.getRootTranslateMatrix(); + return V.transformRect(rect, translateMatrix.multiply(magnetMatrix)); + }, - /** - * Try to get element from cache, - * @param port - * @returns {*} - * @private - */ - _getPortElement: function(port) { + getNodeShape: function(magnet) { - if (this._portElementsCache[port.id]) { - return this._portElementsCache[port.id].portElement; - } - return this._createPortElement(port); - }, + var metrics = this.nodeCache(magnet); + if (metrics.geometryShape === undefined) metrics.geometryShape = V(magnet).toGeometryShape(); + return metrics.geometryShape.clone(); + }, - /** - * @private - */ - _updatePorts: function() { + getNodeMatrix: function(magnet) { - // layout ports without group - this._updatePortGroup(undefined); - // layout ports with explicit group - var groupsNames = Object.keys(this.model._portSettingsData.groups); - groupsNames.forEach(this._updatePortGroup, this); - }, + var metrics = this.nodeCache(magnet); + if (metrics.magnetMatrix === undefined) { + var target = this.rotatableNode || this.el; + metrics.magnetMatrix = V(magnet).getTransformToElement(target); + } + return V.createSVGMatrix(metrics.magnetMatrix); + }, - /** - * @private - */ - _removePorts: function() { - util.invoke(this._portElementsCache, 'portElement.remove'); - }, + getRootTranslateMatrix: function() { - /** - * @param {Port} port - * @returns {V} - * @private - */ - _createPortElement: function(port) { + var model = this.model; + var position = model.position(); + var mt = V.createSVGMatrix().translate(position.x, position.y); + return mt; + }, - var portContentElement = V(this._getPortMarkup(port)); - var portLabelContentElement = V(this._getPortLabelMarkup(port.label)); + getRootRotateMatrix: function() { - if (portContentElement && portContentElement.length > 1) { - throw new Error('ElementView: Invalid port markup - multiple roots.'); - } + var mr = V.createSVGMatrix(); + var model = this.model; + var angle = model.angle(); + if (angle) { + var bbox = model.getBBox(); + var cx = bbox.width / 2; + var cy = bbox.height / 2; + mr = mr.translate(cx, cy).rotate(angle).translate(-cx, -cy); + } + return mr; + }, - portContentElement.attr({ - 'port': port.id, - 'port-group': port.group - }); + // Rotatable & Scalable Group + // always slower, kept mainly for backwards compatibility - var portElement = V(this.portContainerMarkup) - .append(portContentElement) - .append(portLabelContentElement); + rgRotate: function() { - this._portElementsCache[port.id] = { - portElement: portElement, - portLabelElement: portLabelContentElement - }; + this.rotatableNode.attr('transform', this.getRotateString()); + }, - return portElement; - }, + rgTranslate: function() { - /** + this.vel.attr('transform', this.getTranslateString()); + }, + + sgResize: function(cell, changed, opt) { + + var model = this.model; + var angle = model.get('angle') || 0; + var size = model.get('size') || { width: 1, height: 1 }; + var scalable = this.scalableNode; + + // Getting scalable group's bbox. + // Due to a bug in webkit's native SVG .getBBox implementation, the bbox of groups with path children includes the paths' control points. + // To work around the issue, we need to check whether there are any path elements inside the scalable group. + var recursive = false; + if (scalable.node.getElementsByTagName('path').length > 0) { + // If scalable has at least one descendant that is a path, we need to switch to recursive bbox calculation. + // If there are no path descendants, group bbox calculation works and so we can use the (faster) native function directly. + recursive = true; + } + var scalableBBox = scalable.getBBox({ recursive: recursive }); + + // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making + // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. + var sx = (size.width / (scalableBBox.width || 1)); + var sy = (size.height / (scalableBBox.height || 1)); + scalable.attr('transform', 'scale(' + sx + ',' + sy + ')'); + + // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` + // Order of transformations is significant but we want to reconstruct the object always in the order: + // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, + // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the + // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation + // around the center of the resized object (which is a different origin then the origin of the previous rotation) + // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. + + // Cancel the rotation but now around a different origin, which is the center of the scaled object. + var rotatable = this.rotatableNode; + var rotation = rotatable && rotatable.attr('transform'); + if (rotation && rotation !== null) { + + rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); + var rotatableBBox = scalable.getBBox({ target: this.paper.viewport }); + + // Store new x, y and perform rotate() again against the new rotation origin. + model.set('position', { x: rotatableBBox.x, y: rotatableBBox.y }, opt); + this.rotate(); + } + + // Update must always be called on non-rotated element. Otherwise, relative positioning + // would work with wrong (rotated) bounding boxes. + this.update(); + }, + + // Embedding mode methods. + // ----------------------- + + prepareEmbedding: function(data) { + + data || (data = {}); + + var model = data.model || this.model; + var paper = data.paper || this.paper; + var graph = paper.model; + + model.startBatch('to-front'); + + // Bring the model to the front with all his embeds. + model.toFront({ deep: true, ui: true }); + + // Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see + // the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z. + var maxZ = graph.get('cells').max('z').get('z'); + var connectedLinks = graph.getConnectedLinks(model, { deep: true }); + + // Move to front also all the inbound and outbound links that are connected + // to any of the element descendant. If we bring to front only embedded elements, + // links connected to them would stay in the background. + joint.util.invoke(connectedLinks, 'set', 'z', maxZ + 1, { ui: true }); + + model.stopBatch('to-front'); + + // Before we start looking for suitable parent we remove the current one. + var parentId = model.parent(); + parentId && graph.getCell(parentId).unembed(model, { ui: true }); + }, + + processEmbedding: function(data) { + + data || (data = {}); + + var model = data.model || this.model; + var paper = data.paper || this.paper; + var paperOptions = paper.options; + + var candidates = []; + if (joint.util.isFunction(paperOptions.findParentBy)) { + var parents = joint.util.toArray(paperOptions.findParentBy.call(paper.model, this)); + candidates = parents.filter(function(el) { + return el instanceof joint.dia.Cell && this.model.id !== el.id && !el.isEmbeddedIn(this.model); + }.bind(this)); + } else { + candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); + } + + if (paperOptions.frontParentOnly) { + // pick the element with the highest `z` index + candidates = candidates.slice(-1); + } + + var newCandidateView = null; + var prevCandidateView = data.candidateEmbedView; + + // iterate over all candidates starting from the last one (has the highest z-index). + for (var i = candidates.length - 1; i >= 0; i--) { + + var candidate = candidates[i]; + + if (prevCandidateView && prevCandidateView.model.id == candidate.id) { + + // candidate remains the same + newCandidateView = prevCandidateView; + break; + + } else { + + var view = candidate.findView(paper); + if (paperOptions.validateEmbedding.call(paper, this, view)) { + + // flip to the new candidate + newCandidateView = view; + break; + } + } + } + + if (newCandidateView && newCandidateView != prevCandidateView) { + // A new candidate view found. Highlight the new one. + this.clearEmbedding(data); + data.candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); + } + + if (!newCandidateView && prevCandidateView) { + // No candidate view found. Unhighlight the previous candidate. + this.clearEmbedding(data); + } + }, + + clearEmbedding: function(data) { + + data || (data = {}); + + var candidateView = data.candidateEmbedView; + if (candidateView) { + // No candidate view found. Unhighlight the previous candidate. + candidateView.unhighlight(null, { embedding: true }); + data.candidateEmbedView = null; + } + }, + + finalizeEmbedding: function(data) { + + data || (data = {}); + + var candidateView = data.candidateEmbedView; + var model = data.model || this.model; + var paper = data.paper || this.paper; + + if (candidateView) { + + // We finished embedding. Candidate view is chosen to become the parent of the model. + candidateView.model.embed(model, { ui: true }); + candidateView.unhighlight(null, { embedding: true }); + + data.candidateEmbedView = null; + } + + joint.util.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); + }, + + // Interaction. The controller part. + // --------------------------------- + + pointerdblclick: function(evt, x, y) { + + joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments); + this.notify('element:pointerdblclick', evt, x, y); + }, + + pointerclick: function(evt, x, y) { + + joint.dia.CellView.prototype.pointerclick.apply(this, arguments); + this.notify('element:pointerclick', evt, x, y); + }, + + contextmenu: function(evt, x, y) { + + joint.dia.CellView.prototype.contextmenu.apply(this, arguments); + this.notify('element:contextmenu', evt, x, y); + }, + + pointerdown: function(evt, x, y) { + + joint.dia.CellView.prototype.pointerdown.apply(this, arguments); + this.notify('element:pointerdown', evt, x, y); + + this.dragStart(evt, x, y); + }, + + pointermove: function(evt, x, y) { + + var data = this.eventData(evt); + switch (data.action) { + case 'move': + this.drag(evt, x, y); + break; + case 'magnet': + this.dragMagnet(evt, x, y); + break; + } + + if (!data.stopPropagation) { + joint.dia.CellView.prototype.pointermove.apply(this, arguments); + this.notify('element:pointermove', evt, x, y); + } + + // Make sure the element view data is passed along. + // It could have been wiped out in the handlers above. + this.eventData(evt, data); + }, + + pointerup: function(evt, x, y) { + + var data = this.eventData(evt); + switch (data.action) { + case 'move': + this.dragEnd(evt, x, y); + break; + case 'magnet': + this.dragMagnetEnd(evt, x, y); + return; + } + + if (!data.stopPropagation) { + this.notify('element:pointerup', evt, x, y); + joint.dia.CellView.prototype.pointerup.apply(this, arguments); + } + }, + + mouseover: function(evt) { + + joint.dia.CellView.prototype.mouseover.apply(this, arguments); + this.notify('element:mouseover', evt); + }, + + mouseout: function(evt) { + + joint.dia.CellView.prototype.mouseout.apply(this, arguments); + this.notify('element:mouseout', evt); + }, + + mouseenter: function(evt) { + + joint.dia.CellView.prototype.mouseenter.apply(this, arguments); + this.notify('element:mouseenter', evt); + }, + + mouseleave: function(evt) { + + joint.dia.CellView.prototype.mouseleave.apply(this, arguments); + this.notify('element:mouseleave', evt); + }, + + mousewheel: function(evt, x, y, delta) { + + joint.dia.CellView.prototype.mousewheel.apply(this, arguments); + this.notify('element:mousewheel', evt, x, y, delta); + }, + + onmagnet: function(evt, x, y) { + + this.dragMagnetStart(evt, x, y); + + var stopPropagation = this.eventData(evt).stopPropagation; + if (stopPropagation) evt.stopPropagation(); + }, + + // Drag Start Handlers + + dragStart: function(evt, x, y) { + + if (!this.can('elementMove')) return; + + this.eventData(evt, { + action: 'move', + x: x, + y: y, + restrictedArea: this.paper.getRestrictedArea(this) + }); + }, + + dragMagnetStart: function(evt, x, y) { + + if (!this.can('addLinkFromMagnet')) return; + + this.model.startBatch('add-link'); + + var paper = this.paper; + var graph = paper.model; + var magnet = evt.target; + var link = paper.getDefaultLink(this, magnet); + var sourceEnd = this.getLinkEnd(magnet, x, y, link, 'source'); + var targetEnd = { x: x, y: y }; + + link.set({ source: sourceEnd, target: targetEnd }); + link.addTo(graph, { async: false, ui: true }); + + var linkView = link.findView(paper); + joint.dia.CellView.prototype.pointerdown.apply(linkView, arguments); + linkView.notify('link:pointerdown', evt, x, y); + var data = linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); + linkView.eventData(evt, data); + + this.eventData(evt, { + action: 'magnet', + linkView: linkView, + stopPropagation: true + }); + + this.paper.delegateDragEvents(this, evt.data); + }, + + // Drag Handlers + + drag: function(evt, x, y) { + + var paper = this.paper; + var grid = paper.options.gridSize; + var element = this.model; + var position = element.position(); + var data = this.eventData(evt); + + // Make sure the new element's position always snaps to the current grid after + // translate as the previous one could be calculated with a different grid size. + var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - data.x, grid); + var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - data.y, grid); + + element.translate(tx, ty, { restrictedArea: data.restrictedArea, ui: true }); + + var embedding = !!data.embedding; + if (paper.options.embeddingMode) { + if (!embedding) { + // Prepare the element for embedding only if the pointer moves. + // We don't want to do unnecessary action with the element + // if an user only clicks/dblclicks on it. + this.prepareEmbedding(data); + embedding = true; + } + this.processEmbedding(data); + } + + this.eventData(evt, { + x: g.snapToGrid(x, grid), + y: g.snapToGrid(y, grid), + embedding: embedding + }); + }, + + dragMagnet: function(evt, x, y) { + + var data = this.eventData(evt); + var linkView = data.linkView; + if (linkView) linkView.pointermove(evt, x, y); + }, + + // Drag End Handlers + + dragEnd: function(evt, x, y) { + + var data = this.eventData(evt); + if (data.embedding) this.finalizeEmbedding(data); + }, + + dragMagnetEnd: function(evt, x, y) { + + var data = this.eventData(evt); + var linkView = data.linkView; + if (linkView) linkView.pointerup(evt, x, y); + + this.model.stopBatch('add-link'); + } + +}); + + +// joint.dia.Link base model. +// -------------------------- + +joint.dia.Link = joint.dia.Cell.extend({ + + // The default markup for links. + markup: [ + '<path class="connection" stroke="black" d="M 0 0 0 0"/>', + '<path class="marker-source" fill="black" stroke="black" d="M 0 0 0 0"/>', + '<path class="marker-target" fill="black" stroke="black" d="M 0 0 0 0"/>', + '<path class="connection-wrap" d="M 0 0 0 0"/>', + '<g class="labels"/>', + '<g class="marker-vertices"/>', + '<g class="marker-arrowheads"/>', + '<g class="link-tools"/>' + ].join(''), + + toolMarkup: [ + '<g class="link-tool">', + '<g class="tool-remove" event="remove">', + '<circle r="11" />', + '<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z" />', + '<title>Remove link.', + '', + '', + '', + '', + 'Link options.', + '', + '' + ].join(''), + + doubleToolMarkup: undefined, + + // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). + // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for + // dragging vertices (changin their position). The latter is used for removing vertices. + vertexMarkup: [ + '', + '', + '', + '', + 'Remove vertex.', + '', + '' + ].join(''), + + arrowheadMarkup: [ + '', + '', + '' + ].join(''), + + // may be overwritten by user to change default label (its markup, attrs, position) + defaultLabel: undefined, + + // deprecated + // may be overwritten by user to change default label markup + // lower priority than defaultLabel.markup + labelMarkup: undefined, + + // private + _builtins: { + defaultLabel: { + // builtin default markup: + // used if neither defaultLabel.markup + // nor label.markup is set + markup: [ + { + tagName: 'rect', + selector: 'rect' // faster than tagName CSS selector + }, { + tagName: 'text', + selector: 'text' // faster than tagName CSS selector + } + ], + // builtin default attributes: + // applied only if builtin default markup is used + attrs: { + text: { + fill: '#000000', + fontSize: 14, + textAnchor: 'middle', + yAlignment: 'middle', + pointerEvents: 'none' + }, + rect: { + ref: 'text', + fill: '#ffffff', + rx: 3, + ry: 3, + refWidth: 1, + refHeight: 1, + refX: 0, + refY: 0 + } + }, + // builtin default position: + // used if neither defaultLabel.position + // nor label.position is set + position: { + distance: 0.5 + } + } + }, + + defaults: { + type: 'link', + source: {}, + target: {} + }, + + isLink: function() { + + return true; + }, + + disconnect: function(opt) { + + return this.set({ + source: { x: 0, y: 0 }, + target: { x: 0, y: 0 } + }, opt); + }, + + source: function(source, args, opt) { + + // getter + if (source === undefined) { + return joint.util.clone(this.get('source')); + } + + // setter + var localSource; + var localOpt; + + // `source` is a cell + // take only its `id` and combine with `args` + var isCellProvided = source instanceof joint.dia.Cell; + if (isCellProvided) { // three arguments + localSource = joint.util.clone(args) || {}; + localSource.id = source.id; + localOpt = opt; + return this.set('source', localSource, localOpt); + } + + // `source` is a g.Point + // take only its `x` and `y` and combine with `args` + var isPointProvided = source instanceof g.Point; + if (isPointProvided) { // three arguments + localSource = joint.util.clone(args) || {}; + localSource.x = source.x; + localSource.y = source.y; + localOpt = opt; + return this.set('source', localSource, localOpt); + } + + // `source` is an object + // no checking + // two arguments + localSource = source; + localOpt = args; + return this.set('source', localSource, localOpt); + }, + + target: function(target, args, opt) { + + // getter + if (target === undefined) { + return joint.util.clone(this.get('target')); + } + + // setter + var localTarget; + var localOpt; + + // `target` is a cell + // take only its `id` argument and combine with `args` + var isCellProvided = target instanceof joint.dia.Cell; + if (isCellProvided) { // three arguments + localTarget = joint.util.clone(args) || {}; + localTarget.id = target.id; + localOpt = opt; + return this.set('target', localTarget, localOpt); + } + + // `target` is a g.Point + // take only its `x` and `y` and combine with `args` + var isPointProvided = target instanceof g.Point; + if (isPointProvided) { // three arguments + localTarget = joint.util.clone(args) || {}; + localTarget.x = target.x; + localTarget.y = target.y; + localOpt = opt; + return this.set('target', localTarget, localOpt); + } + + // `target` is an object + // no checking + // two arguments + localTarget = target; + localOpt = args; + return this.set('target', localTarget, localOpt); + }, + + router: function(name, args, opt) { + + // getter + if (name === undefined) { + router = this.get('router'); + if (!router) { + if (this.get('manhattan')) return { name: 'orthogonal' }; // backwards compatibility + return null; + } + if (typeof router === 'object') return joint.util.clone(router); + return router; // e.g. a function + } + + // setter + var isRouterProvided = ((typeof name === 'object') || (typeof name === 'function')); + var localRouter = isRouterProvided ? name : { name: name, args: args }; + var localOpt = isRouterProvided ? args : opt; + + return this.set('router', localRouter, localOpt); + }, + + connector: function(name, args, opt) { + + // getter + if (name === undefined) { + connector = this.get('connector'); + if (!connector) { + if (this.get('smooth')) return { name: 'smooth' }; // backwards compatibility + return null; + } + if (typeof connector === 'object') return joint.util.clone(connector); + return connector; // e.g. a function + } + + // setter + var isConnectorProvided = ((typeof name === 'object' || typeof name === 'function')); + var localConnector = isConnectorProvided ? name : { name: name, args: args }; + var localOpt = isConnectorProvided ? args : opt; + + return this.set('connector', localConnector, localOpt); + }, + + // Labels API + + // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. + label: function(idx, label, opt) { + + var labels = this.labels(); + + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : 0; + if (idx < 0) idx = labels.length + idx; + + // getter + if (arguments.length <= 1) return this.prop(['labels', idx]); + // setter + return this.prop(['labels', idx], label, opt); + }, + + labels: function(labels, opt) { + + // getter + if (arguments.length === 0) { + labels = this.get('labels'); + if (!Array.isArray(labels)) return []; + return labels.slice(); + } + // setter + if (!Array.isArray(labels)) labels = []; + return this.set('labels', labels, opt); + }, + + insertLabel: function(idx, label, opt) { + + if (!label) throw new Error('dia.Link: no label provided'); + + var labels = this.labels(); + var n = labels.length; + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : n; + if (idx < 0) idx = n + idx + 1; + + labels.splice(idx, 0, label); + return this.labels(labels, opt); + }, + + // convenience function + // add label to end of labels array + appendLabel: function(label, opt) { + + return this.insertLabel(-1, label, opt); + }, + + removeLabel: function(idx, opt) { + + var labels = this.labels(); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : -1; + + labels.splice(idx, 1); + return this.labels(labels, opt); + }, + + // Vertices API + + vertex: function(idx, vertex, opt) { + + var vertices = this.vertices(); + + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : 0; + if (idx < 0) idx = vertices.length + idx; + + // getter + if (arguments.length <= 1) return this.prop(['vertices', idx]); + // setter + return this.prop(['vertices', idx], vertex, opt); + }, + + vertices: function(vertices, opt) { + + // getter + if (arguments.length === 0) { + vertices = this.get('vertices'); + if (!Array.isArray(vertices)) return []; + return vertices.slice(); + } + // setter + if (!Array.isArray(vertices)) vertices = []; + return this.set('vertices', vertices, opt); + }, + + insertVertex: function(idx, vertex, opt) { + + if (!vertex) throw new Error('dia.Link: no vertex provided'); + + var vertices = this.vertices(); + var n = vertices.length; + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : n; + if (idx < 0) idx = n + idx + 1; + + vertices.splice(idx, 0, vertex); + return this.vertices(vertices, opt); + }, + + removeVertex: function(idx, opt) { + + var vertices = this.vertices(); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : -1; + + vertices.splice(idx, 1); + return this.vertices(vertices, opt); + }, + + // Transformations + + translate: function(tx, ty, opt) { + + // enrich the option object + opt = opt || {}; + opt.translateBy = opt.translateBy || this.id; + opt.tx = tx; + opt.ty = ty; + + return this.applyToPoints(function(p) { + return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; + }, opt); + }, + + scale: function(sx, sy, origin, opt) { + + return this.applyToPoints(function(p) { + return g.point(p).scale(sx, sy, origin).toJSON(); + }, opt); + }, + + applyToPoints: function(fn, opt) { + + if (!joint.util.isFunction(fn)) { + throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); + } + + var attrs = {}; + + var source = this.source(); + if (!source.id) { + attrs.source = fn(source); + } + + var target = this.target(); + if (!target.id) { + attrs.target = fn(target); + } + + var vertices = this.vertices(); + if (vertices.length > 0) { + attrs.vertices = vertices.map(fn); + } + + return this.set(attrs, opt); + }, + + reparent: function(opt) { + + var newParent; + + if (this.graph) { + + var source = this.getSourceElement(); + var target = this.getTargetElement(); + var prevParent = this.getParentCell(); + + if (source && target) { + newParent = this.graph.getCommonAncestor(source, target); + } + + if (prevParent && (!newParent || newParent.id !== prevParent.id)) { + // Unembed the link if source and target has no common ancestor + // or common ancestor changed + prevParent.unembed(this, opt); + } + + if (newParent) { + newParent.embed(this, opt); + } + } + + return newParent; + }, + + hasLoop: function(opt) { + + opt = opt || {}; + + var sourceId = this.source().id; + var targetId = this.target().id; + + if (!sourceId || !targetId) { + // Link "pinned" to the paper does not have a loop. + return false; + } + + var loop = sourceId === targetId; + + // Note that there in the deep mode a link can have a loop, + // even if it connects only a parent and its embed. + // A loop "target equals source" is valid in both shallow and deep mode. + if (!loop && opt.deep && this.graph) { + + var sourceElement = this.getSourceElement(); + var targetElement = this.getTargetElement(); + + loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); + } + + return loop; + }, + + // unlike source(), this method returns null if source is a point + getSourceElement: function() { + + var source = this.source(); + var graph = this.graph; + + return (source && source.id && graph && graph.getCell(source.id)) || null; + }, + + // unlike target(), this method returns null if target is a point + getTargetElement: function() { + + var target = this.target(); + var graph = this.graph; + + return (target && target.id && graph && graph.getCell(target.id)) || null; + }, + + // Returns the common ancestor for the source element, + // target element and the link itself. + getRelationshipAncestor: function() { + + var connectionAncestor; + + if (this.graph) { + + var cells = [ + this, + this.getSourceElement(), // null if source is a point + this.getTargetElement() // null if target is a point + ].filter(function(item) { + return !!item; + }); + + connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); + } + + return connectionAncestor || null; + }, + + // Is source, target and the link itself embedded in a given cell? + isRelationshipEmbeddedIn: function(cell) { + + var cellId = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : cell.id; + var ancestor = this.getRelationshipAncestor(); + + return !!ancestor && (ancestor.id === cellId || ancestor.isEmbeddedIn(cellId)); + }, + + // Get resolved default label. + _getDefaultLabel: function() { + + var defaultLabel = this.get('defaultLabel') || this.defaultLabel || {}; + + var label = {}; + label.markup = defaultLabel.markup || this.get('labelMarkup') || this.labelMarkup; + label.position = defaultLabel.position; + label.attrs = defaultLabel.attrs; + label.size = defaultLabel.size; + + return label; + } +}, + { + endsEqual: function(a, b) { + + var portsEqual = a.port === b.port || !a.port && !b.port; + return a.id === b.id && portsEqual; + } + }); + + +// joint.dia.Link base view and controller. +// ---------------------------------------- + +joint.dia.LinkView = joint.dia.CellView.extend({ + + className: function() { + + var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); + + classNames.push('link'); + + return classNames.join(' '); + }, + + options: { + + shortLinkLength: 105, + doubleLinkTools: false, + longLinkLength: 155, + linkToolsOffset: 40, + doubleLinkToolsOffset: 65, + sampleInterval: 50, + }, + + _labelCache: null, + _labelSelectors: null, + _markerCache: null, + _V: null, + _dragData: null, // deprecated + + metrics: null, + decimalsRounding: 2, + + initialize: function(options) { + + joint.dia.CellView.prototype.initialize.apply(this, arguments); + + // create methods in prototype, so they can be accessed from any instance and + // don't need to be create over and over + if (typeof this.constructor.prototype.watchSource !== 'function') { + this.constructor.prototype.watchSource = this.createWatcher('source'); + this.constructor.prototype.watchTarget = this.createWatcher('target'); + } + + // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to + // `` nodes wrapped by Vectorizer. This allows for quick access to the + // nodes in `updateLabelPosition()` in order to update the label positions. + this._labelCache = {}; + + // a cache of label selectors + this._labelSelectors = {}; + + // keeps markers bboxes and positions again for quicker access + this._markerCache = {}; + + // cache of default markup nodes + this._V = {}, + + // connection path metrics + this.metrics = {}, + + // bind events + this.startListening(); + }, + + startListening: function() { + + var model = this.model; + + this.listenTo(model, 'change:markup', this.render); + this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); + this.listenTo(model, 'change:toolMarkup', this.onToolsChange); + this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); + this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); + this.listenTo(model, 'change:source', this.onSourceChange); + this.listenTo(model, 'change:target', this.onTargetChange); + }, + + onSourceChange: function(cell, source, opt) { + + // Start watching the new source model. + this.watchSource(cell, source); + // This handler is called when the source attribute is changed. + // This can happen either when someone reconnects the link (or moves arrowhead), + // or when an embedded link is translated by its ancestor. + // 1. Always do update. + // 2. Do update only if the opposite end ('target') is also a point. + var model = this.model; + if (!opt.translateBy || !model.get('target').id || !source.id) { + this.update(model, null, opt); + } + }, + + onTargetChange: function(cell, target, opt) { + + // Start watching the new target model. + this.watchTarget(cell, target); + // See `onSourceChange` method. + var model = this.model; + if (!opt.translateBy || (model.get('source').id && !target.id && joint.util.isEmpty(model.get('vertices')))) { + this.update(model, null, opt); + } + }, + + onVerticesChange: function(cell, changed, opt) { + + this.renderVertexMarkers(); + + // If the vertices have been changed by a translation we do update only if the link was + // the only link that was translated. If the link was translated via another element which the link + // is embedded in, this element will be translated as well and that triggers an update. + // Note that all embeds in a model are sorted - first comes links, then elements. + if (!opt.translateBy || opt.translateBy === this.model.id) { + // Vertices were changed (not as a reaction on translate) + // or link.translate() was called or + this.update(cell, null, opt); + } + }, + + onToolsChange: function() { + + this.renderTools().updateToolsPosition(); + }, + + onLabelsChange: function(link, labels, opt) { + + var requireRender = true; + + var previousLabels = this.model.previous('labels'); + + if (previousLabels) { + // Here is an optimalization for cases when we know, that change does + // not require rerendering of all labels. + if (('propertyPathArray' in opt) && ('propertyValue' in opt)) { + // The label is setting by `prop()` method + var pathArray = opt.propertyPathArray || []; + var pathLength = pathArray.length; + if (pathLength > 1) { + // We are changing a single label here e.g. 'labels/0/position' + var labelExists = !!previousLabels[pathArray[1]]; + if (labelExists) { + if (pathLength === 2) { + // We are changing the entire label. Need to check if the + // markup is also being changed. + requireRender = ('markup' in Object(opt.propertyValue)); + } else if (pathArray[2] !== 'markup') { + // We are changing a label property but not the markup + requireRender = false; + } + } + } + } + } + + if (requireRender) { + this.renderLabels(); + } else { + this.updateLabels(); + } + + this.updateLabelPositions(); + }, + + // Rendering. + // ---------- + + render: function() { + + this.vel.empty(); + this._V = {}; + this.renderMarkup(); + // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox + // returns zero values) + this.renderLabels(); + // start watching the ends of the link for changes + var model = this.model; + this.watchSource(model, model.source()) + .watchTarget(model, model.target()) + .update(); + + return this; + }, + + renderMarkup: function() { + + var link = this.model; + var markup = link.get('markup') || link.markup; + if (!markup) throw new Error('dia.LinkView: markup required'); + if (Array.isArray(markup)) return this.renderJSONMarkup(markup); + if (typeof markup === 'string') return this.renderStringMarkup(markup); + throw new Error('dia.LinkView: invalid markup'); + }, + + renderJSONMarkup: function(markup) { + + var doc = joint.util.parseDOMJSON(markup); + // Selectors + var selectors = this.selectors = doc.selectors; + var rootSelector = this.selector; + if (selectors[rootSelector]) throw new Error('dia.LinkView: ambiguous root selector.'); + selectors[rootSelector] = this.el; + // Fragment + this.vel.append(doc.fragment); + }, + + renderStringMarkup: function(markup) { + + // A special markup can be given in the `properties.markup` property. This might be handy + // if e.g. arrowhead markers should be `` elements or any other element than ``s. + // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors + // of elements with special meaning though. Therefore, those classes should be preserved in any + // special markup passed in `properties.markup`. + var children = V(markup); + // custom markup may contain only one children + if (!Array.isArray(children)) children = [children]; + // Cache all children elements for quicker access. + var cache = this._V; // vectorized markup; + for (var i = 0, n = children.length; i < n; i++) { + var child = children[i]; + var className = child.attr('class'); + if (className) { + // Strip the joint class name prefix, if there is one. + className = joint.util.removeClassNamePrefix(className); + cache[$.camelCase(className)] = child; + } + } + // partial rendering + this.renderTools(); + this.renderVertexMarkers(); + this.renderArrowheadMarkers(); + this.vel.append(children); + }, + + _getLabelMarkup: function(labelMarkup) { + + if (!labelMarkup) return undefined; + + if (Array.isArray(labelMarkup)) return this._getLabelJSONMarkup(labelMarkup); + if (typeof labelMarkup === 'string') return this._getLabelStringMarkup(labelMarkup); + throw new Error('dia.linkView: invalid label markup'); + }, + + _getLabelJSONMarkup: function(labelMarkup) { + + return joint.util.parseDOMJSON(labelMarkup); // fragment and selectors + }, + + _getLabelStringMarkup: function(labelMarkup) { + + var children = V(labelMarkup); + var fragment = document.createDocumentFragment(); + + if (!Array.isArray(children)) { + fragment.append(children.node); + + } else { + for (var i = 0, n = children.length; i < n; i++) { + var currentChild = children[i].node; + fragment.appendChild(currentChild); + } + } + + return { fragment: fragment, selectors: {} }; // no selectors + }, + + // Label markup fragment may come wrapped in , or not. + // If it doesn't, add the container here. + _normalizeLabelMarkup: function(markup) { + + if (!markup) return undefined; + + var fragment = markup.fragment; + if (!(markup.fragment instanceof DocumentFragment) || !markup.fragment.hasChildNodes()) throw new Error('dia.LinkView: invalid label markup.'); + + var vNode; + var childNodes = fragment.childNodes; + + if ((childNodes.length > 1) || childNodes[0].nodeName.toUpperCase() !== 'G') { + // default markup fragment is not wrapped in + // add a container + + vNode = V('g'); + vNode.append(fragment); + vNode.addClass('label'); + + } else { + vNode = V(childNodes[0]); + vNode.addClass('label'); + } + + return { node: vNode.node, selectors: markup.selectors }; + }, + + renderLabels: function() { + + var cache = this._V; + var vLabels = cache.labels; + var labelCache = this._labelCache = {}; + var labelSelectors = this._labelSelectors = {}; + + if (vLabels) vLabels.empty(); + + var model = this.model; + var labels = model.get('labels') || []; + var labelsCount = labels.length; + if (labelsCount === 0) return this; + + if (!vLabels) { + // there is no label container in the markup but some labels are defined + // add a container + vLabels = cache.labels = V('g').addClass('labels').appendTo(this.el); + } + + for (var i = 0; i < labelsCount; i++) { + + var label = labels[i]; + var labelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(label.markup)); + + var node; + var selectors; + if (labelMarkup) { + node = labelMarkup.node; + selectors = labelMarkup.selectors; + + } else { + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(builtinDefaultLabel.markup)); + + var defaultLabel = model._getDefaultLabel(); + var defaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(defaultLabel.markup)); + + var defaultMarkup = defaultLabelMarkup || builtinDefaultLabelMarkup; + + node = defaultMarkup.node; + selectors = defaultMarkup.selectors; + } + + var vLabel = V(node); + vLabel.attr('label-idx', i); // assign label-idx + vLabel.appendTo(vLabels); + labelCache[i] = vLabel; // cache node for `updateLabels()` so it can just update label node positions + + selectors[this.selector] = vLabel.node; + labelSelectors[i] = selectors; // cache label selectors for `updateLabels()` + } + + this.updateLabels(); + + return this; + }, + + // merge default label attrs into label attrs + // keep `undefined` or `null` because `{}` means something else + _mergeLabelAttrs: function(hasCustomMarkup, labelAttrs, defaultLabelAttrs, builtinDefaultLabelAttrs) { + + if (labelAttrs === null) return null; + if (labelAttrs === undefined) { + + if (defaultLabelAttrs === null) return null; + if (defaultLabelAttrs === undefined) { + + if (hasCustomMarkup) return undefined; + return builtinDefaultLabelAttrs; + } + + if (hasCustomMarkup) return defaultLabelAttrs; + return joint.util.merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs); + } + + if (hasCustomMarkup) return joint.util.merge({}, defaultLabelAttrs, labelAttrs); + return joint.util.merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs, labelAttrs); + }, + + updateLabels: function() { + + if (!this._V.labels) return this; + + var model = this.model; + var labels = model.get('labels') || []; + var canLabelMove = this.can('labelMove'); + + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelAttrs = builtinDefaultLabel.attrs; + + var defaultLabel = model._getDefaultLabel(); + var defaultLabelMarkup = defaultLabel.markup; + var defaultLabelAttrs = defaultLabel.attrs; + + for (var i = 0, n = labels.length; i < n; i++) { + + var vLabel = this._labelCache[i]; + vLabel.attr('cursor', (canLabelMove ? 'move' : 'default')); + + var selectors = this._labelSelectors[i]; + + var label = labels[i]; + var labelMarkup = label.markup; + var labelAttrs = label.attrs; + + var attrs = this._mergeLabelAttrs( + (labelMarkup || defaultLabelMarkup), + labelAttrs, + defaultLabelAttrs, + builtinDefaultLabelAttrs + ); + + this.updateDOMSubtreeAttributes(vLabel.node, attrs, { + rootBBox: new g.Rect(label.size), + selectors: selectors + }); + } + + return this; + }, + + renderTools: function() { + + if (!this._V.linkTools) return this; + + // Tools are a group of clickable elements that manipulate the whole link. + // A good example of this is the remove tool that removes the whole link. + // Tools appear after hovering the link close to the `source` element/point of the link + // but are offset a bit so that they don't cover the `marker-arrowhead`. + + var $tools = $(this._V.linkTools.node).empty(); + var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); + var tool = V(toolTemplate()); + + $tools.append(tool.node); + + // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. + this._toolCache = tool; + + // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the + // link as well but only if the link is longer than `longLinkLength`. + if (this.options.doubleLinkTools) { + + var tool2; + if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { + toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); + tool2 = V(toolTemplate()); + } else { + tool2 = tool.clone(); + } + + $tools.append(tool2.node); + this._tool2Cache = tool2; + } + + return this; + }, + + renderVertexMarkers: function() { + + if (!this._V.markerVertices) return this; + + var $markerVertices = $(this._V.markerVertices.node).empty(); + + // A special markup can be given in the `properties.vertexMarkup` property. This might be handy + // if default styling (elements) are not desired. This makes it possible to use any + // SVG elements for .marker-vertex and .marker-vertex-remove tools. + var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); + + this.model.vertices().forEach(function(vertex, idx) { + + $markerVertices.append(V(markupTemplate(joint.util.assign({ idx: idx }, vertex))).node); + }); + + return this; + }, + + renderArrowheadMarkers: function() { + + // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. + if (!this._V.markerArrowheads) return this; + + var $markerArrowheads = $(this._V.markerArrowheads.node); + + $markerArrowheads.empty(); + + // A special markup can be given in the `properties.vertexMarkup` property. This might be handy + // if default styling (elements) are not desired. This makes it possible to use any + // SVG elements for .marker-vertex and .marker-vertex-remove tools. + var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); + + this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); + this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); + + $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); + + return this; + }, + + // Updating. + // --------- + + // Default is to process the `attrs` object and set attributes on subelements based on the selectors. + update: function(model, attributes, opt) { + + opt || (opt = {}); + + // update the link path + this.updateConnection(opt); + + // update SVG attributes defined by 'attrs/'. + this.updateDOMSubtreeAttributes(this.el, this.model.attr(), { selectors: this.selectors }); + + this.updateDefaultConnectionPath(); + + // update the label position etc. + this.updateLabelPositions(); + this.updateToolsPosition(); + this.updateArrowheadMarkers(); + + this.updateTools(opt); + // Local perpendicular flag (as opposed to one defined on paper). + // Could be enabled inside a connector/router. It's valid only + // during the update execution. + this.options.perpendicular = null; + // Mark that postponed update has been already executed. + this.updatePostponed = false; + + return this; + }, + + removeRedundantLinearVertices: function(opt) { + var link = this.model; + var vertices = link.vertices(); + var conciseVertices = []; + var n = vertices.length; + var m = 0; + for (var i = 0; i < n; i++) { + var current = new g.Point(vertices[i]).round(); + var prev = new g.Point(conciseVertices[m - 1] || this.sourceAnchor).round(); + if (prev.equals(current)) continue; + var next = new g.Point(vertices[i + 1] || this.targetAnchor).round(); + if (prev.equals(next)) continue; + var line = new g.Line(prev, next); + if (line.pointOffset(current) === 0) continue; + conciseVertices.push(vertices[i]); + m++; + } + if (n === m) return 0; + link.vertices(conciseVertices, opt); + return (n - m); + }, + + updateDefaultConnectionPath: function() { + + var cache = this._V; + + if (cache.connection) { + cache.connection.attr('d', this.getSerializedConnection()); + } + + if (cache.connectionWrap) { + cache.connectionWrap.attr('d', this.getSerializedConnection()); + } + + if (cache.markerSource && cache.markerTarget) { + this._translateAndAutoOrientArrows(cache.markerSource, cache.markerTarget); + } + }, + + getEndView: function(type) { + switch (type) { + case 'source': + return this.sourceView || null; + case 'target': + return this.targetView || null; + default: + throw new Error('dia.LinkView: type parameter required.'); + } + }, + + getEndAnchor: function(type) { + switch (type) { + case 'source': + return new g.Point(this.sourceAnchor); + case 'target': + return new g.Point(this.targetAnchor); + default: + throw new Error('dia.LinkView: type parameter required.'); + } + }, + + getEndMagnet: function(type) { + switch (type) { + case 'source': + var sourceView = this.sourceView; + if (!sourceView) break; + return this.sourceMagnet || sourceView.el; + case 'target': + var targetView = this.targetView; + if (!targetView) break; + return this.targetMagnet || targetView.el; + default: + throw new Error('dia.LinkView: type parameter required.'); + } + return null; + }, + + updateConnection: function(opt) { + + opt = opt || {}; + + var model = this.model; + var route, path; + + if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { + // The link is being translated by an ancestor that will + // shift source point, target point and all vertices + // by an equal distance. + var tx = opt.tx || 0; + var ty = opt.ty || 0; + + route = (new g.Polyline(this.route)).translate(tx, ty).points; + + // translate source and target connection and marker points. + this._translateConnectionPoints(tx, ty); + + // translate the path itself + path = this.path; + path.translate(tx, ty); + + } else { + + var vertices = model.vertices(); + // 1. Find Anchors + + var anchors = this.findAnchors(vertices); + var sourceAnchor = this.sourceAnchor = anchors.source; + var targetAnchor = this.targetAnchor = anchors.target; + + // 2. Find Route + route = this.findRoute(vertices, opt); + + // 3. Find Connection Points + var connectionPoints = this.findConnectionPoints(route, sourceAnchor, targetAnchor); + var sourcePoint = this.sourcePoint = connectionPoints.source; + var targetPoint = this.targetPoint = connectionPoints.target; + + // 3b. Find Marker Connection Point - Backwards Compatibility + var markerPoints = this.findMarkerPoints(route, sourcePoint, targetPoint); + + // 4. Find Connection + path = this.findPath(route, markerPoints.source || sourcePoint, markerPoints.target || targetPoint); + } + + this.route = route; + this.path = path; + this.metrics = {}; + }, + + findMarkerPoints: function(route, sourcePoint, targetPoint) { + + var firstWaypoint = route[0]; + var lastWaypoint = route[route.length - 1]; + + // Move the source point by the width of the marker taking into account + // its scale around x-axis. Note that scale is the only transform that + // makes sense to be set in `.marker-source` attributes object + // as all other transforms (translate/rotate) will be replaced + // by the `translateAndAutoOrient()` function. + var cache = this._markerCache; + // cache source and target points + var sourceMarkerPoint, targetMarkerPoint; + + if (this._V.markerSource) { + + cache.sourceBBox = cache.sourceBBox || this._V.markerSource.getBBox(); + sourceMarkerPoint = g.point(sourcePoint).move( + firstWaypoint || targetPoint, + cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 + ).round(); + } + + if (this._V.markerTarget) { + + cache.targetBBox = cache.targetBBox || this._V.markerTarget.getBBox(); + targetMarkerPoint = g.point(targetPoint).move( + lastWaypoint || sourcePoint, + cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 + ).round(); + } + + // if there was no markup for the marker, use the connection point. + cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); + cache.targetPoint = targetMarkerPoint || targetPoint.clone(); + + return { + source: sourceMarkerPoint, + target: targetMarkerPoint + } + }, + + findAnchors: function(vertices) { + + var model = this.model; + var firstVertex = vertices[0]; + var lastVertex = vertices[vertices.length - 1]; + var sourceDef = model.get('source'); + var targetDef = model.get('target'); + var sourceView = this.sourceView; + var targetView = this.targetView; + var sourceMagnet, targetMagnet; + + // Anchor Source + var sourceAnchor; + if (sourceView) { + sourceMagnet = (this.sourceMagnet || sourceView.el); + var sourceAnchorRef; + if (firstVertex) { + sourceAnchorRef = new g.Point(firstVertex); + } else if (targetView) { + // TODO: the source anchor reference is not a point, how to deal with this? + sourceAnchorRef = this.targetMagnet || targetView.el; + } else { + sourceAnchorRef = new g.Point(targetDef); + } + sourceAnchor = this.getAnchor(sourceDef.anchor, sourceView, sourceMagnet, sourceAnchorRef, 'source'); + } else { + sourceAnchor = new g.Point(sourceDef); + } + + // Anchor Target + var targetAnchor; + if (targetView) { + targetMagnet = (this.targetMagnet || targetView.el); + var targetAnchorRef = new g.Point(lastVertex || sourceAnchor); + targetAnchor = this.getAnchor(targetDef.anchor, targetView, targetMagnet, targetAnchorRef, 'target'); + } else { + targetAnchor = new g.Point(targetDef); + } + + // Con + return { + source: sourceAnchor, + target: targetAnchor + } + }, + + findConnectionPoints: function(route, sourceAnchor, targetAnchor) { + + var firstWaypoint = route[0]; + var lastWaypoint = route[route.length - 1]; + var model = this.model; + var sourceDef = model.get('source'); + var targetDef = model.get('target'); + var sourceView = this.sourceView; + var targetView = this.targetView; + var paperOptions = this.paper.options; + var sourceMagnet, targetMagnet; + + // Connection Point Source + var sourcePoint; + if (sourceView) { + sourceMagnet = (this.sourceMagnet || sourceView.el); + var sourceConnectionPointDef = sourceDef.connectionPoint || paperOptions.defaultConnectionPoint; + var sourcePointRef = firstWaypoint || targetAnchor; + var sourceLine = new g.Line(sourcePointRef, sourceAnchor); + sourcePoint = this.getConnectionPoint(sourceConnectionPointDef, sourceView, sourceMagnet, sourceLine, 'source'); + } else { + sourcePoint = sourceAnchor; + } + // Connection Point Target + var targetPoint; + if (targetView) { + targetMagnet = (this.targetMagnet || targetView.el); + var targetConnectionPointDef = targetDef.connectionPoint || paperOptions.defaultConnectionPoint; + var targetPointRef = lastWaypoint || sourceAnchor; + var targetLine = new g.Line(targetPointRef, targetAnchor); + targetPoint = this.getConnectionPoint(targetConnectionPointDef, targetView, targetMagnet, targetLine, 'target'); + } else { + targetPoint = targetAnchor; + } + + return { + source: sourcePoint, + target: targetPoint + } + }, + + getAnchor: function(anchorDef, cellView, magnet, ref, endType) { + + if (!anchorDef) { + var paperOptions = this.paper.options; + if (paperOptions.perpendicularLinks || this.options.perpendicular) { + // Backwards compatibility + // If `perpendicularLinks` flag is set on the paper and there are vertices + // on the link, then try to find a connection point that makes the link perpendicular + // even though the link won't point to the center of the targeted object. + anchorDef = { name: 'perpendicular' }; + } else { + anchorDef = paperOptions.defaultAnchor; + } + } + + if (!anchorDef) throw new Error('Anchor required.'); + var anchorFn; + if (typeof anchorDef === 'function') { + anchorFn = anchorDef; + } else { + var anchorName = anchorDef.name; + anchorFn = joint.anchors[anchorName]; + if (typeof anchorFn !== 'function') throw new Error('Unknown anchor: ' + anchorName); + } + var anchor = anchorFn.call(this, cellView, magnet, ref, anchorDef.args || {}, endType, this); + if (anchor) return anchor.round(this.decimalsRounding); + return new g.Point() + }, + + + getConnectionPoint: function(connectionPointDef, view, magnet, line, endType) { + + var connectionPoint; + var anchor = line.end; + // Backwards compatibility + var paperOptions = this.paper.options; + if (typeof paperOptions.linkConnectionPoint === 'function') { + connectionPoint = paperOptions.linkConnectionPoint(this, view, magnet, line.start, endType); + if (connectionPoint) return connectionPoint; + } + + if (!connectionPointDef) return anchor; + var connectionPointFn; + if (typeof connectionPointDef === 'function') { + connectionPointFn = connectionPointDef; + } else { + var connectionPointName = connectionPointDef.name; + connectionPointFn = joint.connectionPoints[connectionPointName]; + if (typeof connectionPointFn !== 'function') throw new Error('Unknown connection point: ' + connectionPointName); + } + connectionPoint = connectionPointFn.call(this, line, view, magnet, connectionPointDef.args || {}, endType, this); + if (connectionPoint) return connectionPoint.round(this.decimalsRounding); + return anchor; + }, + + _translateConnectionPoints: function(tx, ty) { + + var cache = this._markerCache; + + cache.sourcePoint.offset(tx, ty); + cache.targetPoint.offset(tx, ty); + this.sourcePoint.offset(tx, ty); + this.targetPoint.offset(tx, ty); + this.sourceAnchor.offset(tx, ty); + this.targetAnchor.offset(tx, ty); + }, + + // if label position is a number, normalize it to a position object + // this makes sure that label positions can be merged properly + _normalizeLabelPosition: function(labelPosition) { + + if (typeof labelPosition === 'number') return { distance: labelPosition, offset: null, args: null }; + return labelPosition; + }, + + updateLabelPositions: function() { + + if (!this._V.labels) return this; + + var path = this.path; + if (!path) return this; + + // This method assumes all the label nodes are stored in the `this._labelCache` hash table + // by their indices in the `this.get('labels')` array. This is done in the `renderLabels()` method. + + var model = this.model; + var labels = model.get('labels') || []; + if (!labels.length) return this; + + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelPosition = builtinDefaultLabel.position; + + var defaultLabel = model._getDefaultLabel(); + var defaultLabelPosition = this._normalizeLabelPosition(defaultLabel.position); + + var defaultPosition = joint.util.merge({}, builtinDefaultLabelPosition, defaultLabelPosition); + + for (var idx = 0, n = labels.length; idx < n; idx++) { + + var label = labels[idx]; + var labelPosition = this._normalizeLabelPosition(label.position); + + var position = joint.util.merge({}, defaultPosition, labelPosition); + + var labelPoint = this.getLabelCoordinates(position); + this._labelCache[idx].attr('transform', 'translate(' + labelPoint.x + ', ' + labelPoint.y + ')'); + } + + return this; + }, + + updateToolsPosition: function() { + + if (!this._V.linkTools) return this; + + // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. + // Note that the offset is hardcoded here. The offset should be always + // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking + // this up all the time would be slow. + + var scale = ''; + var offset = this.options.linkToolsOffset; + var connectionLength = this.getConnectionLength(); + + // Firefox returns connectionLength=NaN in odd cases (for bezier curves). + // In that case we won't update tools position at all. + if (!Number.isNaN(connectionLength)) { + + // If the link is too short, make the tools half the size and the offset twice as low. + if (connectionLength < this.options.shortLinkLength) { + scale = 'scale(.5)'; + offset /= 2; + } + + var toolPosition = this.getPointAtLength(offset); + + this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + + if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { + + var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; + + toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); + this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + this._tool2Cache.attr('visibility', 'visible'); + + } else if (this.options.doubleLinkTools) { + + this._tool2Cache.attr('visibility', 'hidden'); + } + } + + return this; + }, + + updateArrowheadMarkers: function() { + + if (!this._V.markerArrowheads) return this; + + // getting bbox of an element with `display="none"` in IE9 ends up with access violation + if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; + + var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; + this._V.sourceArrowhead.scale(sx); + this._V.targetArrowhead.scale(sx); + + this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); + + return this; + }, + + // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, + // it stops listening on the previous one and starts listening to the new one. + createWatcher: function(endType) { + + // create handler for specific end type (source|target). + var onModelChange = function(endModel, opt) { + this.onEndModelChange(endType, endModel, opt); + }; + + function watchEndModel(link, end) { + + end = end || {}; + + var endModel = null; + var previousEnd = link.previous(endType) || {}; + + if (previousEnd.id) { + this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); + } + + if (end.id) { + // If the observed model changes, it caches a new bbox and do the link update. + endModel = this.paper.getModelById(end.id); + this.listenTo(endModel, 'change', onModelChange); + } + + onModelChange.call(this, endModel, { cacheOnly: true }); + + return this; + } + + return watchEndModel; + }, + + onEndModelChange: function(endType, endModel, opt) { + + var doUpdate = !opt.cacheOnly; + var model = this.model; + var end = model.get(endType) || {}; + + if (endModel) { + + var selector = this.constructor.makeSelector(end); + var oppositeEndType = endType == 'source' ? 'target' : 'source'; + var oppositeEnd = model.get(oppositeEndType) || {}; + var endId = end.id; + var oppositeEndId = oppositeEnd.id; + var oppositeSelector = oppositeEndId && this.constructor.makeSelector(oppositeEnd); + + // Caching end models bounding boxes. + // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached + // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed + // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). + if (opt.handleBy === this.cid && (endId === oppositeEndId) && selector == oppositeSelector) { + + // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the + // second time now. There is no need to calculate bbox and find magnet element again. + // It was calculated already for opposite link end. + this[endType + 'View'] = this[oppositeEndType + 'View']; + this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; + + } else if (opt.translateBy) { + // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. + // If `opt.translateBy` is an ID of the element that was originally translated. + + // Noop + + } else { + // The slowest path, source/target could have been rotated or resized or any attribute + // that affects the bounding box of the view might have been changed. + + var connectedModel = this.paper.model.getCell(endId); + if (!connectedModel) throw new Error('LinkView: invalid ' + endType + ' cell.'); + var connectedView = connectedModel.findView(this.paper); + if (connectedView) { + var connectedMagnet = connectedView.getMagnetFromLinkEnd(end); + if (connectedMagnet === connectedView.el) connectedMagnet = null; + this[endType + 'View'] = connectedView; + this[endType + 'Magnet'] = connectedMagnet; + } else { + // the view is not rendered yet + this[endType + 'View'] = this[endType + 'Magnet'] = null; + } + } + + if (opt.handleBy === this.cid && opt.translateBy && + model.isEmbeddedIn(endModel) && + !joint.util.isEmpty(model.get('vertices'))) { + // Loop link whose element was translated and that has vertices (that need to be translated with + // the parent in which my element is embedded). + // If the link is embedded, has a loop and vertices and the end model + // has been translated, do not update yet. There are vertices still to be updated (change:vertices + // event will come in the next turn). + doUpdate = false; + } + + if (!this.updatePostponed && oppositeEndId) { + // The update was not postponed (that can happen e.g. on the first change event) and the opposite + // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if + // we're reacting on change:source event, the oppositeEnd is the target model). + + var oppositeEndModel = this.paper.getModelById(oppositeEndId); + + // Passing `handleBy` flag via event option. + // Note that if we are listening to the same model for event 'change' twice. + // The same event will be handled by this method also twice. + if (end.id === oppositeEnd.id) { + // We're dealing with a loop link. Tell the handlers in the next turn that they should update + // the link instead of me. (We know for sure there will be a next turn because + // loop links react on at least two events: change on the source model followed by a change on + // the target model). + opt.handleBy = this.cid; + } + + if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { + + // Here are two options: + // - Source and target are connected to the same model (not necessarily the same port). + // - Both end models are translated by the same ancestor. We know that opposite end + // model will be translated in the next turn as well. + // In both situations there will be more changes on the model that trigger an + // update. So there is no need to update the linkView yet. + this.updatePostponed = true; + doUpdate = false; + } + } + + } else { + + // the link end is a point ~ rect 1x1 + this[endType + 'View'] = this[endType + 'Magnet'] = null; + } + + if (doUpdate) { + this.update(model, null, opt); + } + }, + + _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { + + // Make the markers "point" to their sticky points being auto-oriented towards + // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. + var route = joint.util.toArray(this.route); + if (sourceArrow) { + sourceArrow.translateAndAutoOrient( + this.sourcePoint, + route[0] || this.targetPoint, + this.paper.viewport + ); + } + + if (targetArrow) { + targetArrow.translateAndAutoOrient( + this.targetPoint, + route[route.length - 1] || this.sourcePoint, + this.paper.viewport + ); + } + }, + + _getDefaultLabelPositionArgs: function() { + + var defaultLabel = this.model._getDefaultLabel(); + var defaultLabelPosition = defaultLabel.position || {}; + return defaultLabelPosition.args; + }, + + _getLabelPositionArgs: function(idx) { + + var labelPosition = this.model.label(idx).position || {}; + return labelPosition.args; + }, + + // merge default label position args into label position args + // keep `undefined` or `null` because `{}` means something else + _mergeLabelPositionArgs: function(labelPositionArgs, defaultLabelPositionArgs) { + + if (labelPositionArgs === null) return null; + if (labelPositionArgs === undefined) { + + if (defaultLabelPositionArgs === null) return null; + return defaultLabelPositionArgs; + } + + return joint.util.merge({}, defaultLabelPositionArgs, labelPositionArgs); + }, + + // Add default label at given position at end of `labels` array. + // Assigns relative coordinates by default. + // `opt.absoluteDistance` forces absolute coordinates. + // `opt.reverseDistance` forces reverse absolute coordinates (if absoluteDistance = true). + // `opt.absoluteOffset` forces absolute coordinates for offset. + addLabel: function(x, y, opt) { + + // accept input in form `{ x, y }, opt` or `x, y, opt` + var isPointProvided = (typeof x !== 'number'); + var localX = isPointProvided ? x.x : x; + var localY = isPointProvided ? x.y : y; + var localOpt = isPointProvided ? y : opt; + + var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs(); + var labelPositionArgs = localOpt; + var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); + + var label = { position: this.getLabelPosition(localX, localY, positionArgs) }; + var idx = -1; + this.model.insertLabel(idx, label, localOpt); + return idx; + }, + + // Add a new vertex at calculated index to the `vertices` array. + addVertex: function(x, y, opt) { + + // accept input in form `{ x, y }, opt` or `x, y, opt` + var isPointProvided = (typeof x !== 'number'); + var localX = isPointProvided ? x.x : x; + var localY = isPointProvided ? x.y : y; + var localOpt = isPointProvided ? y : opt; + + var vertex = { x: localX, y: localY }; + var idx = this.getVertexIndex(localX, localY); + this.model.insertVertex(idx, vertex, localOpt); + return idx; + }, + + // Send a token (an SVG element, usually a circle) along the connection path. + // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)` + // `opt.duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. + // `opt.directon` is optional and it determines whether the token goes from source to target or other way round (`reverse`) + // `opt.connection` is an optional selector to the connection path. + // `callback` is optional and is a function to be called once the token reaches the target. + sendToken: function(token, opt, callback) { + + function onAnimationEnd(vToken, callback) { + return function() { + vToken.remove(); + if (typeof callback === 'function') { + callback(); + } + }; + } + + var duration, isReversed, selector; + if (joint.util.isObject(opt)) { + duration = opt.duration; + isReversed = (opt.direction === 'reverse'); + selector = opt.connection; + } else { + // Backwards compatibility + duration = opt; + isReversed = false; + selector = null; + } + + duration = duration || 1000; + + var animationAttributes = { + dur: duration + 'ms', + repeatCount: 1, + calcMode: 'linear', + fill: 'freeze' + }; + + if (isReversed) { + animationAttributes.keyPoints = '1;0'; + animationAttributes.keyTimes = '0;1'; + } + + var vToken = V(token); + var connection; + if (typeof selector === 'string') { + // Use custom connection path. + connection = this.findBySelector(selector, this.el, this.selectors)[0]; + } else { + // Select connection path automatically. + var cache = this._V; + connection = (cache.connection) ? cache.connection.node : this.el.querySelector('path'); + } + + if (!(connection instanceof SVGPathElement)) { + throw new Error('dia.LinkView: token animation requires a valid connection path.'); + } + + vToken + .appendTo(this.paper.viewport) + .animateAlongPath(animationAttributes, connection); + + setTimeout(onAnimationEnd(vToken, callback), duration); + }, + + findRoute: function(vertices) { + + vertices || (vertices = []); + + var namespace = joint.routers; + var router = this.model.router(); + var defaultRouter = this.paper.options.defaultRouter; + + if (!router) { + if (defaultRouter) router = defaultRouter; + else return vertices.map(g.Point, g); // no router specified + } + + var routerFn = joint.util.isFunction(router) ? router : namespace[router.name]; + if (!joint.util.isFunction(routerFn)) { + throw new Error('dia.LinkView: unknown router: "' + router.name + '".'); + } + + var args = router.args || {}; + + var route = routerFn.call( + this, // context + vertices, // vertices + args, // options + this // linkView + ); + + if (!route) return vertices.map(g.Point, g); + return route; + }, + + // Return the `d` attribute value of the `` element representing the link + // between `source` and `target`. + findPath: function(route, sourcePoint, targetPoint) { + + var namespace = joint.connectors; + var connector = this.model.connector(); + var defaultConnector = this.paper.options.defaultConnector; + + if (!connector) { + connector = defaultConnector || {}; + } + + var connectorFn = joint.util.isFunction(connector) ? connector : namespace[connector.name]; + if (!joint.util.isFunction(connectorFn)) { + throw new Error('dia.LinkView: unknown connector: "' + connector.name + '".'); + } + + var args = joint.util.clone(connector.args || {}); + args.raw = true; // Request raw g.Path as the result. + + var path = connectorFn.call( + this, // context + sourcePoint, // start point + targetPoint, // end point + route, // vertices + args, // options + this // linkView + ); + + if (typeof path === 'string') { + // Backwards compatibility for connectors not supporting `raw` option. + path = new g.Path(V.normalizePathData(path)); + } + + return path; + }, + + // Public API. + // ----------- + + getConnection: function() { + + var path = this.path; + if (!path) return null; + + return path.clone(); + }, + + getSerializedConnection: function() { + + var path = this.path; + if (!path) return null; + + var metrics = this.metrics; + if (metrics.hasOwnProperty('data')) return metrics.data; + var data = path.serialize(); + metrics.data = data; + return data; + }, + + getConnectionSubdivisions: function() { + + var path = this.path; + if (!path) return null; + + var metrics = this.metrics; + if (metrics.hasOwnProperty('segmentSubdivisions')) return metrics.segmentSubdivisions; + var subdivisions = path.getSegmentSubdivisions(); + metrics.segmentSubdivisions = subdivisions; + return subdivisions; + }, + + getConnectionLength: function() { + + var path = this.path; + if (!path) return 0; + + var metrics = this.metrics; + if (metrics.hasOwnProperty('length')) return metrics.length; + var length = path.length({ segmentSubdivisions: this.getConnectionSubdivisions() }); + metrics.length = length; + return length; + }, + + getPointAtLength: function(length) { + + var path = this.path; + if (!path) return null; + + return path.pointAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getPointAtRatio: function(ratio) { + + var path = this.path; + if (!path) return null; + + return path.pointAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getTangentAtLength: function(length) { + + var path = this.path; + if (!path) return null; + + return path.tangentAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getTangentAtRatio: function(ratio) { + + var path = this.path; + if (!path) return null; + + return path.tangentAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getClosestPoint: function(point) { + + var path = this.path; + if (!path) return null; + + return path.closestPoint(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getClosestPointLength: function(point) { + + var path = this.path; + if (!path) return null; + + return path.closestPointLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getClosestPointRatio: function(point) { + + var path = this.path; + if (!path) return null; + + return path.closestPointNormalizedLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + // accepts options `absoluteDistance: boolean`, `reverseDistance: boolean`, `absoluteOffset: boolean` + // to move beyond connection endpoints, absoluteOffset has to be set + getLabelPosition: function(x, y, opt) { + + var position = {}; + + var localOpt = opt || {}; + if (opt) position.args = opt; + + var isDistanceRelative = !localOpt.absoluteDistance; // relative by default + var isDistanceAbsoluteReverse = (localOpt.absoluteDistance && localOpt.reverseDistance); // non-reverse by default + var isOffsetAbsolute = localOpt.absoluteOffset; // offset is non-absolute by default + + var path = this.path; + var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }; + + var labelPoint = new g.Point(x, y); + var t = path.closestPointT(labelPoint, pathOpt); + + // GET DISTANCE: + + var labelDistance = path.lengthAtT(t, pathOpt); + if (isDistanceRelative) labelDistance = (labelDistance / this.getConnectionLength()) || 0; // fix to prevent NaN for 0 length + if (isDistanceAbsoluteReverse) labelDistance = (-1 * (this.getConnectionLength() - labelDistance)) || 1; // fix for end point (-0 => 1) + + position.distance = labelDistance; + + // GET OFFSET: + // use absolute offset if: + // - opt.absoluteOffset is true, + // - opt.absoluteOffset is not true but there is no tangent + + var tangent; + if (!isOffsetAbsolute) tangent = path.tangentAtT(t); + + var labelOffset; + if (tangent) { + labelOffset = tangent.pointOffset(labelPoint); + + } else { + var closestPoint = path.pointAtT(t); + var labelOffsetDiff = labelPoint.difference(closestPoint); + labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y }; + } + + position.offset = labelOffset; + + return position; + }, + + getLabelCoordinates: function(labelPosition) { + + var labelDistance; + if (typeof labelPosition === 'number') labelDistance = labelPosition; + else if (typeof labelPosition.distance === 'number') labelDistance = labelPosition.distance; + else throw new Error('dia.LinkView: invalid label position distance.'); + + var isDistanceRelative = ((labelDistance > 0) && (labelDistance <= 1)); + + var labelOffset = 0; + var labelOffsetCoordinates = { x: 0, y: 0 }; + if (labelPosition.offset) { + var positionOffset = labelPosition.offset; + if (typeof positionOffset === 'number') labelOffset = positionOffset; + if (positionOffset.x) labelOffsetCoordinates.x = positionOffset.x; + if (positionOffset.y) labelOffsetCoordinates.y = positionOffset.y; + } + + var isOffsetAbsolute = ((labelOffsetCoordinates.x !== 0) || (labelOffsetCoordinates.y !== 0) || labelOffset === 0); + + var path = this.path; + var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }; + + var distance = isDistanceRelative ? (labelDistance * this.getConnectionLength()) : labelDistance; + + var point; + + if (isOffsetAbsolute) { + point = path.pointAtLength(distance, pathOpt); + point.offset(labelOffsetCoordinates); + + } else { + var tangent = path.tangentAtLength(distance, pathOpt); + + if (tangent) { + tangent.rotate(tangent.start, -90); + tangent.setLength(labelOffset); + point = tangent.end; + + } else { + // fallback - the connection has zero length + point = path.start; + } + } + + return point; + }, + + getVertexIndex: function(x, y) { + + var model = this.model; + var vertices = model.vertices(); + + var vertexLength = this.getClosestPointLength(new g.Point(x, y)); + + var idx = 0; + for (var n = vertices.length; idx < n; idx++) { + var currentVertex = vertices[idx]; + var currentVertexLength = this.getClosestPointLength(currentVertex); + if (vertexLength < currentVertexLength) break; + } + + return idx; + }, + + // Interaction. The controller part. + // --------------------------------- + + pointerdblclick: function(evt, x, y) { + + joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments); + this.notify('link:pointerdblclick', evt, x, y); + }, + + pointerclick: function(evt, x, y) { + + joint.dia.CellView.prototype.pointerclick.apply(this, arguments); + this.notify('link:pointerclick', evt, x, y); + }, + + contextmenu: function(evt, x, y) { + + joint.dia.CellView.prototype.contextmenu.apply(this, arguments); + this.notify('link:contextmenu', evt, x, y); + }, + + pointerdown: function(evt, x, y) { + + joint.dia.CellView.prototype.pointerdown.apply(this, arguments); + this.notify('link:pointerdown', evt, x, y); + + // Backwards compatibility for the default markup + var className = evt.target.getAttribute('class'); + switch (className) { + + case 'marker-vertex': + this.dragVertexStart(evt, x, y); + return; + + case 'marker-vertex-remove': + case 'marker-vertex-remove-area': + this.dragVertexRemoveStart(evt, x, y); + return; + + case 'marker-arrowhead': + this.dragArrowheadStart(evt, x, y); + return; + + case 'connection': + case 'connection-wrap': + this.dragConnectionStart(evt, x, y); + return; + + case 'marker-source': + case 'marker-target': + return; + } + + this.dragStart(evt, x, y); + }, + + pointermove: function(evt, x, y) { + + // Backwards compatibility + var dragData = this._dragData; + if (dragData) this.eventData(evt, dragData); + + var data = this.eventData(evt); + switch (data.action) { + + case 'vertex-move': + this.dragVertex(evt, x, y); + break; + + case 'label-move': + this.dragLabel(evt, x, y); + break; + + case 'arrowhead-move': + this.dragArrowhead(evt, x, y); + break; + + case 'move': + this.drag(evt, x, y); + break; + } + + // Backwards compatibility + if (dragData) joint.util.assign(dragData, this.eventData(evt)); + + joint.dia.CellView.prototype.pointermove.apply(this, arguments); + this.notify('link:pointermove', evt, x, y); + }, + + pointerup: function(evt, x, y) { + + // Backwards compatibility + var dragData = this._dragData; + if (dragData) { + this.eventData(evt, dragData); + this._dragData = null; + } + + var data = this.eventData(evt); + switch (data.action) { + + case 'vertex-move': + this.dragVertexEnd(evt, x, y); + break; + + case 'label-move': + this.dragLabelEnd(evt, x, y); + break; + + case 'arrowhead-move': + this.dragArrowheadEnd(evt, x, y); + break; + + case 'move': + this.dragEnd(evt, x, y); + } + + this.notify('link:pointerup', evt, x, y); + joint.dia.CellView.prototype.pointerup.apply(this, arguments); + }, + + mouseover: function(evt) { + + joint.dia.CellView.prototype.mouseover.apply(this, arguments); + this.notify('link:mouseover', evt); + }, + + mouseout: function(evt) { + + joint.dia.CellView.prototype.mouseout.apply(this, arguments); + this.notify('link:mouseout', evt); + }, + + mouseenter: function(evt) { + + joint.dia.CellView.prototype.mouseenter.apply(this, arguments); + this.notify('link:mouseenter', evt); + }, + + mouseleave: function(evt) { + + joint.dia.CellView.prototype.mouseleave.apply(this, arguments); + this.notify('link:mouseleave', evt); + }, + + mousewheel: function(evt, x, y, delta) { + + joint.dia.CellView.prototype.mousewheel.apply(this, arguments); + this.notify('link:mousewheel', evt, x, y, delta); + }, + + onevent: function(evt, eventName, x, y) { + + // Backwards compatibility + var linkTool = V(evt.target).findParentByClass('link-tool', this.el); + if (linkTool) { + // No further action to be executed + evt.stopPropagation(); + + // Allow `interactive.useLinkTools=false` + if (this.can('useLinkTools')) { + if (eventName === 'remove') { + // Built-in remove event + this.model.remove({ ui: true }); + + } else { + // link:options and other custom events inside the link tools + this.notify(eventName, evt, x, y); + } + } + + } else { + joint.dia.CellView.prototype.onevent.apply(this, arguments); + } + }, + + onlabel: function(evt, x, y) { + + this.dragLabelStart(evt, x, y); + + var stopPropagation = this.eventData(evt).stopPropagation; + if (stopPropagation) evt.stopPropagation(); + }, + + // Drag Start Handlers + + dragConnectionStart: function(evt, x, y) { + + if (!this.can('vertexAdd')) return; + + // Store the index at which the new vertex has just been placed. + // We'll be update the very same vertex position in `pointermove()`. + var vertexIdx = this.addVertex({ x: x, y: y }, { ui: true }); + this.eventData(evt, { + action: 'vertex-move', + vertexIdx: vertexIdx + }); + }, + + dragLabelStart: function(evt, x, y) { + + if (!this.can('labelMove')) { + // Backwards compatibility: + // If labels can't be dragged no default action is triggered. + this.eventData(evt, { stopPropagation: true }); + return; + } + + var labelNode = evt.currentTarget; + var labelIdx = parseInt(labelNode.getAttribute('label-idx'), 10); + + var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs(); + var labelPositionArgs = this._getLabelPositionArgs(labelIdx); + var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); + + this.eventData(evt, { + action: 'label-move', + labelIdx: labelIdx, + positionArgs: positionArgs, + stopPropagation: true + }); + + this.paper.delegateDragEvents(this, evt.data); + }, + + dragVertexStart: function(evt, x, y) { + + if (!this.can('vertexMove')) return; + + var vertexNode = evt.target; + var vertexIdx = parseInt(vertexNode.getAttribute('idx'), 10); + this.eventData(evt, { + action: 'vertex-move', + vertexIdx: vertexIdx + }); + }, + + dragVertexRemoveStart: function(evt, x, y) { + + if (!this.can('vertexRemove')) return; + + var removeNode = evt.target; + var vertexIdx = parseInt(removeNode.getAttribute('idx'), 10); + this.model.removeVertex(vertexIdx); + }, + + dragArrowheadStart: function(evt, x, y) { + + if (!this.can('arrowheadMove')) return; + + var arrowheadNode = evt.target; + var arrowheadType = arrowheadNode.getAttribute('end'); + var data = this.startArrowheadMove(arrowheadType, { ignoreBackwardsCompatibility: true }); + + this.eventData(evt, data); + }, + + dragStart: function(evt, x, y) { + + if (!this.can('linkMove')) return; + + this.eventData(evt, { + action: 'move', + dx: x, + dy: y + }); + }, + + // Drag Handlers + + dragLabel: function(evt, x, y) { + + var data = this.eventData(evt); + var label = { position: this.getLabelPosition(x, y, data.positionArgs) }; + this.model.label(data.labelIdx, label); + }, + + dragVertex: function(evt, x, y) { + + var data = this.eventData(evt); + this.model.vertex(data.vertexIdx, { x: x, y: y }, { ui: true }); + }, + + dragArrowhead: function(evt, x, y) { + + var data = this.eventData(evt); + + if (this.paper.options.snapLinks) { + + this._snapArrowhead(x, y, data); + + } else { + // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. + // It holds the element when a touchstart triggered. + var target = (evt.type === 'mousemove') + ? evt.target + : document.elementFromPoint(evt.clientX, evt.clientY); + + this._connectArrowhead(target, x, y, data); + } + }, + + drag: function(evt, x, y) { + + var data = this.eventData(evt); + this.model.translate(x - data.dx, y - data.dy, { ui: true }); + this.eventData(evt, { + dx: x, + dy: y + }); + }, + + // Drag End Handlers + + dragLabelEnd: function() { + // noop + }, + + dragVertexEnd: function() { + // noop + }, + + dragArrowheadEnd: function(evt, x, y) { + + var data = this.eventData(evt); + var paper = this.paper; + + if (paper.options.snapLinks) { + this._snapArrowheadEnd(data); + } else { + this._connectArrowheadEnd(data, x, y); + } + + if (!paper.linkAllowed(this)) { + // If the changed link is not allowed, revert to its previous state. + this._disallow(data); + } else { + this._finishEmbedding(data); + this._notifyConnectEvent(data, evt); + } + + this._afterArrowheadMove(data); + + // mouseleave event is not triggered due to changing pointer-events to `none`. + if (!this.vel.contains(evt.target)) { + this.mouseleave(evt); + } + }, + + dragEnd: function() { + // noop + }, + + _disallow: function(data) { + + switch (data.whenNotAllowed) { + + case 'remove': + this.model.remove({ ui: true }); + break; + + case 'revert': + default: + this.model.set(data.arrowhead, data.initialEnd, { ui: true }); + break; + } + }, + + _finishEmbedding: function(data) { + + // Reparent the link if embedding is enabled + if (this.paper.options.embeddingMode && this.model.reparent()) { + // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). + data.z = null; + } + }, + + _notifyConnectEvent: function(data, evt) { + + var arrowhead = data.arrowhead; + var initialEnd = data.initialEnd; + var currentEnd = this.model.prop(arrowhead); + var endChanged = currentEnd && !joint.dia.Link.endsEqual(initialEnd, currentEnd); + if (endChanged) { + var paper = this.paper; + if (initialEnd.id) { + this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), data.initialMagnet, arrowhead); + } + if (currentEnd.id) { + this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), data.magnetUnderPointer, arrowhead); + } + } + }, + + _snapArrowhead: function(x, y, data) { + + // checking view in close area of the pointer + + var r = this.paper.options.snapLinks.radius || 50; + var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); + + if (data.closestView) { + data.closestView.unhighlight(data.closestMagnet, { + connecting: true, + snapping: true + }); + } + data.closestView = data.closestMagnet = null; + + var distance; + var minDistance = Number.MAX_VALUE; + var pointer = g.point(x, y); + var paper = this.paper; + + viewsInArea.forEach(function(view) { + + // skip connecting to the element in case '.': { magnet: false } attribute present + if (view.el.getAttribute('magnet') !== 'false') { + + // find distance from the center of the model to pointer coordinates + distance = view.model.getBBox().center().distance(pointer); + + // the connection is looked up in a circle area by `distance < r` + if (distance < r && distance < minDistance) { + + if (paper.options.validateConnection.apply( + paper, data.validateConnectionArgs(view, null) + )) { + minDistance = distance; + data.closestView = view; + data.closestMagnet = view.el; + } + } + } + + view.$('[magnet]').each(function(index, magnet) { + + var bbox = view.getNodeBBox(magnet); + + distance = pointer.distance({ + x: bbox.x + bbox.width / 2, + y: bbox.y + bbox.height / 2 + }); + + if (distance < r && distance < minDistance) { + + if (paper.options.validateConnection.apply( + paper, data.validateConnectionArgs(view, magnet) + )) { + minDistance = distance; + data.closestView = view; + data.closestMagnet = magnet; + } + } + + }.bind(this)); + + }, this); + + var end; + var closestView = data.closestView; + var closestMagnet = data.closestMagnet; + var endType = data.arrowhead; + if (closestView) { + closestView.highlight(closestMagnet, { + connecting: true, + snapping: true + }); + end = closestView.getLinkEnd(closestMagnet, x, y, this.model, endType); + } else { + end = { x: x, y: y }; + } + + this.model.set(endType, end || { x: x, y: y }, { ui: true }); + }, + + _snapArrowheadEnd: function(data) { + + // Finish off link snapping. + // Everything except view unhighlighting was already done on pointermove. + var closestView = data.closestView; + var closestMagnet = data.closestMagnet; + if (closestView && closestMagnet) { + + closestView.unhighlight(closestMagnet, { connecting: true, snapping: true }); + data.magnetUnderPointer = closestView.findMagnet(closestMagnet); + } + + data.closestView = data.closestMagnet = null; + }, + + _connectArrowhead: function(target, x, y, data) { + + // checking views right under the pointer + + if (data.eventTarget !== target) { + // Unhighlight the previous view under pointer if there was one. + if (data.magnetUnderPointer) { + data.viewUnderPointer.unhighlight(data.magnetUnderPointer, { + connecting: true + }); + } + + data.viewUnderPointer = this.paper.findView(target); + if (data.viewUnderPointer) { + // If we found a view that is under the pointer, we need to find the closest + // magnet based on the real target element of the event. + data.magnetUnderPointer = data.viewUnderPointer.findMagnet(target); + + if (data.magnetUnderPointer && this.paper.options.validateConnection.apply( + this.paper, + data.validateConnectionArgs(data.viewUnderPointer, data.magnetUnderPointer) + )) { + // If there was no magnet found, do not highlight anything and assume there + // is no view under pointer we're interested in reconnecting to. + // This can only happen if the overall element has the attribute `'.': { magnet: false }`. + if (data.magnetUnderPointer) { + data.viewUnderPointer.highlight(data.magnetUnderPointer, { + connecting: true + }); + } + } else { + // This type of connection is not valid. Disregard this magnet. + data.magnetUnderPointer = null; + } + } else { + // Make sure we'll unset previous magnet. + data.magnetUnderPointer = null; + } + } + + data.eventTarget = target; + + this.model.set(data.arrowhead, { x: x, y: y }, { ui: true }); + }, + + _connectArrowheadEnd: function(data, x, y) { + + var view = data.viewUnderPointer; + var magnet = data.magnetUnderPointer; + if (!magnet || !view) return; + + view.unhighlight(magnet, { connecting: true }); + + var endType = data.arrowhead; + var end = view.getLinkEnd(magnet, x, y, this.model, endType); + this.model.set(endType, end, { ui: true }); + }, + + _beforeArrowheadMove: function(data) { + + data.z = this.model.get('z'); + this.model.toFront(); + + // Let the pointer propagate throught the link view elements so that + // the `evt.target` is another element under the pointer, not the link itself. + this.el.style.pointerEvents = 'none'; + + if (this.paper.options.markAvailable) { + this._markAvailableMagnets(data); + } + }, + + _afterArrowheadMove: function(data) { + + if (data.z !== null) { + this.model.set('z', data.z, { ui: true }); + data.z = null; + } + + // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation. + // Value `auto` doesn't work in IE9. We force to use `visiblePainted` instead. + // See `https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events`. + this.el.style.pointerEvents = 'visiblePainted'; + + if (this.paper.options.markAvailable) { + this._unmarkAvailableMagnets(data); + } + }, + + _createValidateConnectionArgs: function(arrowhead) { + // It makes sure the arguments for validateConnection have the following form: + // (source view, source magnet, target view, target magnet and link view) + var args = []; + + args[4] = arrowhead; + args[5] = this; + + var oppositeArrowhead; + var i = 0; + var j = 0; + + if (arrowhead === 'source') { + i = 2; + oppositeArrowhead = 'target'; + } else { + j = 2; + oppositeArrowhead = 'source'; + } + + var end = this.model.get(oppositeArrowhead); + + if (end.id) { + args[i] = this.paper.findViewByModel(end.id); + args[i + 1] = end.selector && args[i].el.querySelector(end.selector); + } + + function validateConnectionArgs(cellView, magnet) { + args[j] = cellView; + args[j + 1] = cellView.el === magnet ? undefined : magnet; + return args; + } + + return validateConnectionArgs; + }, + + _markAvailableMagnets: function(data) { + + function isMagnetAvailable(view, magnet) { + var paper = view.paper; + var validate = paper.options.validateConnection; + return validate.apply(paper, this.validateConnectionArgs(view, magnet)); + } + + var paper = this.paper; + var elements = paper.model.getElements(); + data.marked = {}; + + for (var i = 0, n = elements.length; i < n; i++) { + var view = elements[i].findView(paper); + + if (!view) { + continue; + } + + var magnets = Array.prototype.slice.call(view.el.querySelectorAll('[magnet]')); + if (view.el.getAttribute('magnet') !== 'false') { + // Element wrapping group is also a magnet + magnets.push(view.el); + } + + var availableMagnets = magnets.filter(isMagnetAvailable.bind(data, view)); + + if (availableMagnets.length > 0) { + // highlight all available magnets + for (var j = 0, m = availableMagnets.length; j < m; j++) { + view.highlight(availableMagnets[j], { magnetAvailability: true }); + } + // highlight the entire view + view.highlight(null, { elementAvailability: true }); + + data.marked[view.model.id] = availableMagnets; + } + } + }, + + _unmarkAvailableMagnets: function(data) { + + var markedKeys = Object.keys(data.marked); + var id; + var markedMagnets; + + for (var i = 0, n = markedKeys.length; i < n; i++) { + id = markedKeys[i]; + markedMagnets = data.marked[id]; + + var view = this.paper.findViewByModel(id); + if (view) { + for (var j = 0, m = markedMagnets.length; j < m; j++) { + view.unhighlight(markedMagnets[j], { magnetAvailability: true }); + } + view.unhighlight(null, { elementAvailability: true }); + } + } + + data.marked = null; + }, + + startArrowheadMove: function(end, opt) { + + opt || (opt = {}); + + // Allow to delegate events from an another view to this linkView in order to trigger arrowhead + // move without need to click on the actual arrowhead dom element. + var data = { + action: 'arrowhead-move', + arrowhead: end, + whenNotAllowed: opt.whenNotAllowed || 'revert', + initialMagnet: this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null), + initialEnd: joint.util.clone(this.model.get(end)), + validateConnectionArgs: this._createValidateConnectionArgs(end) + }; + + this._beforeArrowheadMove(data); + + if (opt.ignoreBackwardsCompatibility !== true) { + this._dragData = data; + } + + return data; + } +}, { + + makeSelector: function(end) { + + var selector = ''; + // `port` has a higher precendence over `selector`. This is because the selector to the magnet + // might change while the name of the port can stay the same. + if (end.port) { + selector += '[port="' + end.port + '"]'; + } else if (end.selector) { + selector += end.selector; + } + + return selector; + } + +}); + + +Object.defineProperty(joint.dia.LinkView.prototype, 'sourceBBox', { + + enumerable: true, + + get: function() { + var sourceView = this.sourceView; + var sourceMagnet = this.sourceMagnet; + if (sourceView) { + if (!sourceMagnet) sourceMagnet = sourceView.el; + return sourceView.getNodeBBox(sourceMagnet); + } + var sourceDef = this.model.source(); + return new g.Rect(sourceDef.x, sourceDef.y, 1, 1); + } + +}); + +Object.defineProperty(joint.dia.LinkView.prototype, 'targetBBox', { + + enumerable: true, + + get: function() { + var targetView = this.targetView; + var targetMagnet = this.targetMagnet; + if (targetView) { + if (!targetMagnet) targetMagnet = targetView.el; + return targetView.getNodeBBox(targetMagnet); + } + var targetDef = this.model.target(); + return new g.Rect(targetDef.x, targetDef.y, 1, 1); + } +}); + + +joint.dia.Paper = joint.mvc.View.extend({ + + className: 'paper', + + options: { + + width: 800, + height: 600, + origin: { x: 0, y: 0 }, // x,y coordinates in top-left corner + gridSize: 1, + + // Whether or not to draw the grid lines on the paper's DOM element. + // e.g drawGrid: true, drawGrid: { color: 'red', thickness: 2 } + drawGrid: false, + + // Whether or not to draw the background on the paper's DOM element. + // e.g. background: { color: 'lightblue', image: '/paper-background.png', repeat: 'flip-xy' } + background: false, + + perpendicularLinks: false, + elementView: joint.dia.ElementView, + linkView: joint.dia.LinkView, + snapLinks: false, // false, true, { radius: value } + + // When set to FALSE, an element may not have more than 1 link with the same source and target element. + multiLinks: true, + + // For adding custom guard logic. + guard: function(evt, view) { + + // FALSE means the event isn't guarded. + return false; + }, + + highlighting: { + 'default': { + name: 'stroke', + options: { + padding: 3 + } + }, + magnetAvailability: { + name: 'addClass', + options: { + className: 'available-magnet' + } + }, + elementAvailability: { + name: 'addClass', + options: { + className: 'available-cell' + } + } + }, + + // Prevent the default context menu from being displayed. + preventContextMenu: true, + + // Prevent the default action for blank:pointer. + preventDefaultBlankAction: true, + + // Restrict the translation of elements by given bounding box. + // Option accepts a boolean: + // true - the translation is restricted to the paper area + // false - no restrictions + // A method: + // restrictTranslate: function(elementView) { + // var parentId = elementView.model.get('parent'); + // return parentId && this.model.getCell(parentId).getBBox(); + // }, + // Or a bounding box: + // restrictTranslate: { x: 10, y: 10, width: 790, height: 590 } + restrictTranslate: false, + + // Marks all available magnets with 'available-magnet' class name and all available cells with + // 'available-cell' class name. Marks them when dragging a link is started and unmark + // when the dragging is stopped. + markAvailable: false, + + // Defines what link model is added to the graph after an user clicks on an active magnet. + // Value could be the Backbone.model or a function returning the Backbone.model + // defaultLink: function(elementView, magnet) { return condition ? new customLink1() : new customLink2() } + defaultLink: new joint.dia.Link, + + // A connector that is used by links with no connector defined on the model. + // e.g. { name: 'rounded', args: { radius: 5 }} or a function + defaultConnector: { name: 'normal' }, + + // A router that is used by links with no router defined on the model. + // e.g. { name: 'oneSide', args: { padding: 10 }} or a function + defaultRouter: { name: 'normal' }, + + defaultAnchor: { name: 'center' }, + + defaultConnectionPoint: { name: 'bbox' }, + + /* CONNECTING */ + + connectionStrategy: null, + + // Check whether to add a new link to the graph when user clicks on an a magnet. + validateMagnet: function(cellView, magnet) { + return magnet.getAttribute('magnet') !== 'passive'; + }, + + // Check whether to allow or disallow the link connection while an arrowhead end (source/target) + // being changed. + validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { + return (end === 'target' ? cellViewT : cellViewS) instanceof joint.dia.ElementView; + }, + + /* EMBEDDING */ + + // Enables embedding. Reparents the dragged element with elements under it and makes sure that + // all links and elements are visible taken the level of embedding into account. + embeddingMode: false, + + // Check whether to allow or disallow the element embedding while an element being translated. + validateEmbedding: function(childView, parentView) { + // by default all elements can be in relation child-parent + return true; + }, + + // Determines the way how a cell finds a suitable parent when it's dragged over the paper. + // The cell with the highest z-index (visually on the top) will be chosen. + findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft' + + // If enabled only the element on the very front is taken into account for the embedding. + // If disabled the elements under the dragged view are tested one by one + // (from front to back) until a valid parent found. + frontParentOnly: true, + + // Interactive flags. See online docs for the complete list of interactive flags. + interactive: { + labelMove: false + }, + + // When set to true the links can be pinned to the paper. + // i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 }; + linkPinning: true, + + // Custom validation after an interaction with a link ends. + // Recognizes a function. If `false` is returned, the link is disallowed (removed or reverted) + // (linkView, paper) => boolean + allowLink: null, + + // Allowed number of mousemove events after which the pointerclick event will be still triggered. + clickThreshold: 0, + + // Number of required mousemove events before the first pointermove event will be triggered. + moveThreshold: 0, + + // The namespace, where all the cell views are defined. + cellViewNamespace: joint.shapes, + + // The namespace, where all the cell views are defined. + highlighterNamespace: joint.highlighters + }, + + events: { + 'dblclick': 'pointerdblclick', + 'click': 'pointerclick', // triggered alongside pointerdown and pointerup if no movement + 'touchend': 'pointerclick', // triggered alongside pointerdown and pointerup if no movement + 'contextmenu': 'contextmenu', + 'mousedown': 'pointerdown', + 'touchstart': 'pointerdown', + 'mouseover': 'mouseover', + 'mouseout': 'mouseout', + 'mouseenter': 'mouseenter', + 'mouseleave': 'mouseleave', + 'mousewheel': 'mousewheel', + 'DOMMouseScroll': 'mousewheel', + 'mouseenter .joint-cell': 'mouseenter', + 'mouseleave .joint-cell': 'mouseleave', + 'mouseenter .joint-tools': 'mouseenter', + 'mouseleave .joint-tools': 'mouseleave', + 'mousedown .joint-cell [event]': 'onevent', // interaction with cell with `event` attribute set + 'touchstart .joint-cell [event]': 'onevent', + 'mousedown .joint-cell [magnet]': 'onmagnet', // interaction with cell with `magnet` attribute set + 'touchstart .joint-cell [magnet]': 'onmagnet', + 'mousedown .joint-link .label': 'onlabel', // interaction with link label + 'touchstart .joint-link .label': 'onlabel', + 'dragstart .joint-cell image': 'onImageDragStart' // firefox fix + }, + + documentEvents: { + 'mousemove': 'pointermove', + 'touchmove': 'pointermove', + 'mouseup': 'pointerup', + 'touchend': 'pointerup', + 'touchcancel': 'pointerup' + }, + + _highlights: {}, + + init: function() { + + joint.util.bindAll(this, 'pointerup'); + + var model = this.model = this.options.model || new joint.dia.Graph; + + this.setGrid(this.options.drawGrid); + this.cloneOptions(); + this.render(); + this.setDimensions(); + + this.listenTo(model, 'add', this.onCellAdded) + .listenTo(model, 'remove', this.removeView) + .listenTo(model, 'reset', this.resetViews) + .listenTo(model, 'sort', this._onSort) + .listenTo(model, 'batch:stop', this._onBatchStop); + + this.on('cell:highlight', this.onCellHighlight) + .on('cell:unhighlight', this.onCellUnhighlight) + .on('scale translate', this.update); + + // Hold the value when mouse has been moved: when mouse moved, no click event will be triggered. + this._mousemoved = 0; + // Hash of all cell views. + this._views = {}; + // Reference to the paper owner document + this.$document = $(this.el.ownerDocument); + }, + + cloneOptions: function() { + + var options = this.options; + + // This is a fix for the case where two papers share the same options. + // Changing origin.x for one paper would change the value of origin.x for the other. + // This prevents that behavior. + options.origin = joint.util.assign({}, options.origin); + options.defaultConnector = joint.util.assign({}, options.defaultConnector); + // Return the default highlighting options into the user specified options. + options.highlighting = joint.util.defaultsDeep( + {}, + options.highlighting, + this.constructor.prototype.options.highlighting + ); + }, + + render: function() { + + this.$el.empty(); + + this.svg = V('svg').attr({ width: '100%', height: '100%' }).node; + this.viewport = V('g').addClass(joint.util.addClassNamePrefix('viewport')).node; + this.defs = V('defs').node; + this.tools = V('g').addClass(joint.util.addClassNamePrefix('tools-container')).node; + // Append `` element to the SVG document. This is useful for filters and gradients. + // It's desired to have the defs defined before the viewport (e.g. to make a PDF document pick up defs properly). + V(this.svg).append([this.defs, this.viewport, this.tools]); + + this.$background = $('
').addClass(joint.util.addClassNamePrefix('paper-background')); + if (this.options.background) { + this.drawBackground(this.options.background); + } + + this.$grid = $('
').addClass(joint.util.addClassNamePrefix('paper-grid')); + if (this.options.drawGrid) { + this.drawGrid(); + } + + this.$el.append(this.$background, this.$grid, this.svg); + + return this; + }, + + update: function() { + + if (this.options.drawGrid) { + this.drawGrid(); + } + + if (this._background) { + this.updateBackgroundImage(this._background); + } + + return this; + }, + + // For storing the current transformation matrix (CTM) of the paper's viewport. + _viewportMatrix: null, + + // For verifying whether the CTM is up-to-date. The viewport transform attribute + // could have been manipulated directly. + _viewportTransformString: null, + + matrix: function(ctm) { + + var viewport = this.viewport; + + // Getter: + if (ctm === undefined) { + + var transformString = viewport.getAttribute('transform'); + + if ((this._viewportTransformString || null) === transformString) { + // It's ok to return the cached matrix. The transform attribute has not changed since + // the matrix was stored. + ctm = this._viewportMatrix; + } else { + // The viewport transform attribute has changed. Measure the matrix and cache again. + ctm = viewport.getCTM(); + this._viewportMatrix = ctm; + this._viewportTransformString = transformString; + } + + // Clone the cached current transformation matrix. + // If no matrix previously stored the identity matrix is returned. + return V.createSVGMatrix(ctm); + } + + // Setter: + ctm = V.createSVGMatrix(ctm); + ctmString = V.matrixToTransformString(ctm); + viewport.setAttribute('transform', ctmString); + this.tools.setAttribute('transform', ctmString); + + this._viewportMatrix = ctm; + this._viewportTransformString = viewport.getAttribute('transform'); + + return this; + }, + + clientMatrix: function() { + + return V.createSVGMatrix(this.viewport.getScreenCTM()); + }, + + _sortDelayingBatches: ['add', 'to-front', 'to-back'], + + _onSort: function() { + if (!this.model.hasActiveBatch(this._sortDelayingBatches)) { + this.sortViews(); + } + }, + + _onBatchStop: function(data) { + var name = data && data.batchName; + if (this._sortDelayingBatches.includes(name) && + !this.model.hasActiveBatch(this._sortDelayingBatches)) { + this.sortViews(); + } + }, + + onRemove: function() { + + //clean up all DOM elements/views to prevent memory leaks + this.removeViews(); + }, + + setDimensions: function(width, height) { + + width = this.options.width = width || this.options.width; + height = this.options.height = height || this.options.height; + + this.$el.css({ + width: Math.round(width), + height: Math.round(height) + }); + + this.trigger('resize', width, height); + }, + + setOrigin: function(ox, oy) { + + return this.translate(ox || 0, oy || 0, { absolute: true }); + }, + + // Expand/shrink the paper to fit the content. Snap the width/height to the grid + // defined in `gridWidth`, `gridHeight`. `padding` adds to the resulting width/height of the paper. + // When options { fitNegative: true } it also translates the viewport in order to make all + // the content visible. + fitToContent: function(gridWidth, gridHeight, padding, opt) { // alternatively function(opt) + + if (joint.util.isObject(gridWidth)) { + // first parameter is an option object + opt = gridWidth; + gridWidth = opt.gridWidth || 1; + gridHeight = opt.gridHeight || 1; + padding = opt.padding || 0; + + } else { + + opt = opt || {}; + gridWidth = gridWidth || 1; + gridHeight = gridHeight || 1; + padding = padding || 0; + } + + padding = joint.util.normalizeSides(padding); + + // Calculate the paper size to accomodate all the graph's elements. + var bbox = V(this.viewport).getBBox(); + + var currentScale = this.scale(); + var currentTranslate = this.translate(); + + bbox.x *= currentScale.sx; + bbox.y *= currentScale.sy; + bbox.width *= currentScale.sx; + bbox.height *= currentScale.sy; + + var calcWidth = Math.max(Math.ceil((bbox.width + bbox.x) / gridWidth), 1) * gridWidth; + var calcHeight = Math.max(Math.ceil((bbox.height + bbox.y) / gridHeight), 1) * gridHeight; + + var tx = 0; + var ty = 0; + + if ((opt.allowNewOrigin == 'negative' && bbox.x < 0) || (opt.allowNewOrigin == 'positive' && bbox.x >= 0) || opt.allowNewOrigin == 'any') { + tx = Math.ceil(-bbox.x / gridWidth) * gridWidth; + tx += padding.left; + calcWidth += tx; + } + + if ((opt.allowNewOrigin == 'negative' && bbox.y < 0) || (opt.allowNewOrigin == 'positive' && bbox.y >= 0) || opt.allowNewOrigin == 'any') { + ty = Math.ceil(-bbox.y / gridHeight) * gridHeight; + ty += padding.top; + calcHeight += ty; + } + + calcWidth += padding.right; + calcHeight += padding.bottom; + + // Make sure the resulting width and height are greater than minimum. + calcWidth = Math.max(calcWidth, opt.minWidth || 0); + calcHeight = Math.max(calcHeight, opt.minHeight || 0); + + // Make sure the resulting width and height are lesser than maximum. + calcWidth = Math.min(calcWidth, opt.maxWidth || Number.MAX_VALUE); + calcHeight = Math.min(calcHeight, opt.maxHeight || Number.MAX_VALUE); + + var dimensionChange = calcWidth != this.options.width || calcHeight != this.options.height; + var originChange = tx != currentTranslate.tx || ty != currentTranslate.ty; + + // Change the dimensions only if there is a size discrepency or an origin change + if (originChange) { + this.translate(tx, ty); + } + if (dimensionChange) { + this.setDimensions(calcWidth, calcHeight); + } + }, + + scaleContentToFit: function(opt) { + + var contentBBox = this.getContentBBox(); + + if (!contentBBox.width || !contentBBox.height) return; + + opt = opt || {}; + + joint.util.defaults(opt, { + padding: 0, + preserveAspectRatio: true, + scaleGrid: null, + minScale: 0, + maxScale: Number.MAX_VALUE + //minScaleX + //minScaleY + //maxScaleX + //maxScaleY + //fittingBBox + }); + + var padding = opt.padding; + + var minScaleX = opt.minScaleX || opt.minScale; + var maxScaleX = opt.maxScaleX || opt.maxScale; + var minScaleY = opt.minScaleY || opt.minScale; + var maxScaleY = opt.maxScaleY || opt.maxScale; + + var fittingBBox; + if (opt.fittingBBox) { + fittingBBox = opt.fittingBBox; + } else { + var currentTranslate = this.translate(); + fittingBBox = { + x: currentTranslate.tx, + y: currentTranslate.ty, + width: this.options.width, + height: this.options.height + }; + } + + fittingBBox = g.rect(fittingBBox).moveAndExpand({ + x: padding, + y: padding, + width: -2 * padding, + height: -2 * padding + }); + + var currentScale = this.scale(); + + var newSx = fittingBBox.width / contentBBox.width * currentScale.sx; + var newSy = fittingBBox.height / contentBBox.height * currentScale.sy; + + if (opt.preserveAspectRatio) { + newSx = newSy = Math.min(newSx, newSy); + } + + // snap scale to a grid + if (opt.scaleGrid) { + + var gridSize = opt.scaleGrid; + + newSx = gridSize * Math.floor(newSx / gridSize); + newSy = gridSize * Math.floor(newSy / gridSize); + } + + // scale min/max boundaries + newSx = Math.min(maxScaleX, Math.max(minScaleX, newSx)); + newSy = Math.min(maxScaleY, Math.max(minScaleY, newSy)); + + this.scale(newSx, newSy); + + var contentTranslation = this.getContentBBox(); + + var newOx = fittingBBox.x - contentTranslation.x; + var newOy = fittingBBox.y - contentTranslation.y; + + this.translate(newOx, newOy); + }, + + // Return the dimensions of the content area in local units (without transformations). + getContentArea: function() { + + return V(this.viewport).getBBox(); + }, + + // Return the dimensions of the content bbox in client units (as it appears on screen). + getContentBBox: function() { + + var crect = this.viewport.getBoundingClientRect(); + + // Using Screen CTM was the only way to get the real viewport bounding box working in both + // Google Chrome and Firefox. + var clientCTM = this.clientMatrix(); + + // for non-default origin we need to take the viewport translation into account + var currentTranslate = this.translate(); + + return g.rect({ + x: crect.left - clientCTM.e + currentTranslate.tx, + y: crect.top - clientCTM.f + currentTranslate.ty, + width: crect.width, + height: crect.height + }); + }, + + // Returns a geometry rectangle represeting the entire + // paper area (coordinates from the left paper border to the right one + // and the top border to the bottom one). + getArea: function() { + + return this.paperToLocalRect({ + x: 0, + y: 0, + width: this.options.width, + height: this.options.height + }); + }, + + getRestrictedArea: function() { + + var restrictedArea; + + if (joint.util.isFunction(this.options.restrictTranslate)) { + // A method returning a bounding box + restrictedArea = this.options.restrictTranslate.apply(this, arguments); + } else if (this.options.restrictTranslate === true) { + // The paper area + restrictedArea = this.getArea(); + } else { + // Either false or a bounding box + restrictedArea = this.options.restrictTranslate || null; + } + + return restrictedArea; + }, + + createViewForModel: function(cell) { + + // A class taken from the paper options. + var optionalViewClass; + + // A default basic class (either dia.ElementView or dia.LinkView) + var defaultViewClass; + + // A special class defined for this model in the corresponding namespace. + // e.g. joint.shapes.basic.Rect searches for joint.shapes.basic.RectView + var namespace = this.options.cellViewNamespace; + var type = cell.get('type') + 'View'; + var namespaceViewClass = joint.util.getByPath(namespace, type, '.'); + + if (cell.isLink()) { + optionalViewClass = this.options.linkView; + defaultViewClass = joint.dia.LinkView; + } else { + optionalViewClass = this.options.elementView; + defaultViewClass = joint.dia.ElementView; + } + + // a) the paper options view is a class (deprecated) + // 1. search the namespace for a view + // 2. if no view was found, use view from the paper options + // b) the paper options view is a function + // 1. call the function from the paper options + // 2. if no view was return, search the namespace for a view + // 3. if no view was found, use the default + var ViewClass = (optionalViewClass.prototype instanceof Backbone.View) + ? namespaceViewClass || optionalViewClass + : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass; + + return new ViewClass({ + model: cell, + interactive: this.options.interactive + }); + }, + + onCellAdded: function(cell, graph, opt) { + + if (this.options.async && opt.async !== false && joint.util.isNumber(opt.position)) { + + this._asyncCells = this._asyncCells || []; + this._asyncCells.push(cell); + + if (opt.position == 0) { + + if (this._frameId) throw new Error('another asynchronous rendering in progress'); + + this.asyncRenderViews(this._asyncCells, opt); + delete this._asyncCells; + } + + } else { + + this.renderView(cell); + } + }, + + removeView: function(cell) { + + var view = this._views[cell.id]; + + if (view) { + view.remove(); + delete this._views[cell.id]; + } + + return view; + }, + + renderView: function(cell) { + + var view = this._views[cell.id] = this.createViewForModel(cell); + + V(this.viewport).append(view.el); + view.paper = this; + view.render(); + + return view; + }, + + onImageDragStart: function() { + // This is the only way to prevent image dragging in Firefox that works. + // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help. + + return false; + }, + + beforeRenderViews: function(cells) { + + // Make sure links are always added AFTER elements. + // They wouldn't find their sources/targets in the DOM otherwise. + cells.sort(function(a) { return (a.isLink()) ? 1 : -1; }); + + return cells; + }, + + afterRenderViews: function() { + + this.sortViews(); + }, + + resetViews: function(cellsCollection, opt) { + + // clearing views removes any event listeners + this.removeViews(); + + var cells = cellsCollection.models.slice(); + + // `beforeRenderViews()` can return changed cells array (e.g sorted). + cells = this.beforeRenderViews(cells, opt) || cells; + + this.cancelRenderViews(); + + if (this.options.async) { + + this.asyncRenderViews(cells, opt); + // Sort the cells once all elements rendered (see asyncRenderViews()). + + } else { + + for (var i = 0, n = cells.length; i < n; i++) { + this.renderView(cells[i]); + } + + // Sort the cells in the DOM manually as we might have changed the order they + // were added to the DOM (see above). + this.sortViews(); + } + }, + + cancelRenderViews: function() { + if (this._frameId) { + joint.util.cancelFrame(this._frameId); + delete this._frameId; + } + }, + + removeViews: function() { + + joint.util.invoke(this._views, 'remove'); + + this._views = {}; + }, + + asyncBatchAdded: joint.util.noop, + + asyncRenderViews: function(cells, opt) { + + if (this._frameId) { + + var batchSize = (this.options.async && this.options.async.batchSize) || 50; + var batchCells = cells.splice(0, batchSize); + + batchCells.forEach(function(cell) { + + // The cell has to be part of the graph. + // There is a chance in asynchronous rendering + // that a cell was removed before it's rendered to the paper. + if (cell.graph === this.model) this.renderView(cell); + + }, this); + + this.asyncBatchAdded(); + } + + if (!cells.length) { + + // No cells left to render. + delete this._frameId; + this.afterRenderViews(opt); + this.trigger('render:done', opt); + + } else { + + // Schedule a next batch to render. + this._frameId = joint.util.nextFrame(function() { + this.asyncRenderViews(cells, opt); + }, this); + } + }, + + sortViews: function() { + + // Run insertion sort algorithm in order to efficiently sort DOM elements according to their + // associated model `z` attribute. + + var $cells = $(this.viewport).children('[model-id]'); + var cells = this.model.get('cells'); + + joint.util.sortElements($cells, function(a, b) { + + var cellA = cells.get($(a).attr('model-id')); + var cellB = cells.get($(b).attr('model-id')); + + return (cellA.get('z') || 0) > (cellB.get('z') || 0) ? 1 : -1; + }); + }, + + scale: function(sx, sy, ox, oy) { + + // getter + if (sx === undefined) { + return V.matrixToScale(this.matrix()); + } + + // setter + if (sy === undefined) { + sy = sx; + } + if (ox === undefined) { + ox = 0; + oy = 0; + } + + var translate = this.translate(); + + if (ox || oy || translate.tx || translate.ty) { + var newTx = translate.tx - ox * (sx - 1); + var newTy = translate.ty - oy * (sy - 1); + this.translate(newTx, newTy); + } + + var ctm = this.matrix(); + ctm.a = sx || 0; + ctm.d = sy || 0; + + this.matrix(ctm); + + this.trigger('scale', sx, sy, ox, oy); + + return this; + }, + + // Experimental - do not use in production. + rotate: function(angle, cx, cy) { + + // getter + if (angle === undefined) { + return V.matrixToRotate(this.matrix()); + } + + // setter + + // If the origin is not set explicitely, rotate around the center. Note that + // we must use the plain bounding box (`this.el.getBBox()` instead of the one that gives us + // the real bounding box (`bbox()`) including transformations). + if (cx === undefined) { + var bbox = this.viewport.getBBox(); + cx = bbox.width / 2; + cy = bbox.height / 2; + } + + var ctm = this.matrix().translate(cx,cy).rotate(angle).translate(-cx,-cy); + this.matrix(ctm); + + return this; + }, + + translate: function(tx, ty) { + + // getter + if (tx === undefined) { + return V.matrixToTranslate(this.matrix()); + } + + // setter + + var ctm = this.matrix(); + ctm.e = tx || 0; + ctm.f = ty || 0; + + this.matrix(ctm); + + var newTranslate = this.translate(); + var origin = this.options.origin; + origin.x = newTranslate.tx; + origin.y = newTranslate.ty; + + this.trigger('translate', newTranslate.tx, newTranslate.ty); + + if (this.options.drawGrid) { + this.drawGrid(); + } + + return this; + }, + + // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also + // be a selector or a jQuery object. + findView: function($el) { + + var el = joint.util.isString($el) + ? this.viewport.querySelector($el) + : $el instanceof $ ? $el[0] : $el; + + while (el && el !== this.el && el !== document) { + + var id = el.getAttribute('model-id'); + if (id) return this._views[id]; + + el = el.parentNode; + } + + return undefined; + }, + + // Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`. + findViewByModel: function(cell) { + + var id = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : (cell && cell.id); + + return this._views[id]; + }, + + // Find all views at given point + findViewsFromPoint: function(p) { + + p = g.point(p); + + var views = this.model.getElements().map(this.findViewByModel, this); + + return views.filter(function(view) { + return view && view.vel.getBBox({ target: this.viewport }).containsPoint(p); + }, this); + }, + + // Find all views in given area + findViewsInArea: function(rect, opt) { + + opt = joint.util.defaults(opt || {}, { strict: false }); + rect = g.rect(rect); + + var views = this.model.getElements().map(this.findViewByModel, this); + var method = opt.strict ? 'containsRect' : 'intersect'; + + return views.filter(function(view) { + return view && rect[method](view.vel.getBBox({ target: this.viewport })); + }, this); + }, + + removeTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'remove'); + return this; + }, + + hideTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'hide'); + return this; + }, + + showTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'show'); + return this; + }, + + getModelById: function(id) { + + return this.model.getCell(id); + }, + + snapToGrid: function(x, y) { + + // Convert global coordinates to the local ones of the `viewport`. Otherwise, + // improper transformation would be applied when the viewport gets transformed (scaled/rotated). + return this.clientToLocalPoint(x, y).snapToGrid(this.options.gridSize); + }, + + localToPaperPoint: function(x, y) { + // allow `x` to be a point and `y` undefined + var localPoint = g.Point(x, y); + var paperPoint = V.transformPoint(localPoint, this.matrix()); + return g.Point(paperPoint); + }, + + localToPaperRect: function(x, y, width, height) { + // allow `x` to be a rectangle and rest arguments undefined + var localRect = g.Rect(x, y); + var paperRect = V.transformRect(localRect, this.matrix()); + return g.Rect(paperRect); + }, + + paperToLocalPoint: function(x, y) { + // allow `x` to be a point and `y` undefined + var paperPoint = g.Point(x, y); + var localPoint = V.transformPoint(paperPoint, this.matrix().inverse()); + return g.Point(localPoint); + }, + + paperToLocalRect: function(x, y, width, height) { + // allow `x` to be a rectangle and rest arguments undefined + var paperRect = g.Rect(x, y, width, height); + var localRect = V.transformRect(paperRect, this.matrix().inverse()); + return g.Rect(localRect); + }, + + localToClientPoint: function(x, y) { + // allow `x` to be a point and `y` undefined + var localPoint = g.Point(x, y); + var clientPoint = V.transformPoint(localPoint, this.clientMatrix()); + return g.Point(clientPoint); + }, + + localToClientRect: function(x, y, width, height) { + // allow `x` to be a point and `y` undefined + var localRect = g.Rect(x, y, width, height); + var clientRect = V.transformRect(localRect, this.clientMatrix()); + return g.Rect(clientRect); + }, + + // Transform client coordinates to the paper local coordinates. + // Useful when you have a mouse event object and you'd like to get coordinates + // inside the paper that correspond to `evt.clientX` and `evt.clientY` point. + // Example: var localPoint = paper.clientToLocalPoint({ x: evt.clientX, y: evt.clientY }); + clientToLocalPoint: function(x, y) { + // allow `x` to be a point and `y` undefined + var clientPoint = g.Point(x, y); + var localPoint = V.transformPoint(clientPoint, this.clientMatrix().inverse()); + return g.Point(localPoint); + }, + + clientToLocalRect: function(x, y, width, height) { + // allow `x` to be a point and `y` undefined + var clientRect = g.Rect(x, y, width, height); + var localRect = V.transformRect(clientRect, this.clientMatrix().inverse()); + return g.Rect(localRect); + }, + + localToPagePoint: function(x, y) { + + return this.localToPaperPoint(x, y).offset(this.pageOffset()); + }, + + localToPageRect: function(x, y, width, height) { + + return this.localToPaperRect(x, y, width, height).moveAndExpand(this.pageOffset()); + }, + + pageToLocalPoint: function(x, y) { + + var pagePoint = g.Point(x, y); + var paperPoint = pagePoint.difference(this.pageOffset()); + return this.paperToLocalPoint(paperPoint); + }, + + pageToLocalRect: function(x, y, width, height) { + + var pageOffset = this.pageOffset(); + var paperRect = g.Rect(x, y, width, height); + paperRect.x -= pageOffset.x; + paperRect.y -= pageOffset.y; + return this.paperToLocalRect(paperRect); + }, + + clientOffset: function() { + + var clientRect = this.svg.getBoundingClientRect(); + return g.Point(clientRect.left, clientRect.top); + }, + + pageOffset: function() { + + return this.clientOffset().offset(window.scrollX, window.scrollY); + }, + + linkAllowed: function(linkView) { + + if (!(linkView instanceof joint.dia.LinkView)) { + throw new Error('Must provide a linkView.'); + } + + var link = linkView.model; + var paperOptions = this.options; + var graph = this.model; + var ns = graph.constructor.validations; + + if (!paperOptions.multiLinks) { + if (!ns.multiLinks.call(this, graph, link)) return false; + } + + if (!paperOptions.linkPinning) { + // Link pinning is not allowed and the link is not connected to the target. + if (!ns.linkPinning.call(this, graph, link)) return false; + } + + if (typeof paperOptions.allowLink === 'function') { + if (!paperOptions.allowLink.call(this, linkView, this)) return false; + } + + return true; + }, + + getDefaultLink: function(cellView, magnet) { + + return joint.util.isFunction(this.options.defaultLink) + // default link is a function producing link model + ? this.options.defaultLink.call(this, cellView, magnet) + // default link is the Backbone model + : this.options.defaultLink.clone(); + }, + + // Cell highlighting. + // ------------------ + + resolveHighlighter: function(opt) { + + opt = opt || {}; + var highlighterDef = opt.highlighter; + var paperOpt = this.options; + + /* + Expecting opt.highlighter to have the following structure: + { + name: 'highlighter-name', + options: { + some: 'value' + } + } + */ + if (highlighterDef === undefined) { + + // check for built-in types + var type = ['embedding', 'connecting', 'magnetAvailability', 'elementAvailability'].find(function(type) { + return !!opt[type]; + }); + + highlighterDef = (type && paperOpt.highlighting[type]) || paperOpt.highlighting['default']; + } + + // Do nothing if opt.highlighter is falsey. + // This allows the case to not highlight cell(s) in certain cases. + // For example, if you want to NOT highlight when embedding elements. + if (!highlighterDef) return false; + + // Allow specifying a highlighter by name. + if (joint.util.isString(highlighterDef)) { + highlighterDef = { + name: highlighterDef + }; + } + + var name = highlighterDef.name; + var highlighter = paperOpt.highlighterNamespace[name]; + + // Highlighter validation + if (!highlighter) { + throw new Error('Unknown highlighter ("' + name + '")'); + } + if (typeof highlighter.highlight !== 'function') { + throw new Error('Highlighter ("' + name + '") is missing required highlight() method'); + } + if (typeof highlighter.unhighlight !== 'function') { + throw new Error('Highlighter ("' + name + '") is missing required unhighlight() method'); + } + + return { + highlighter: highlighter, + options: highlighterDef.options || {}, + name: name + }; + }, + + onCellHighlight: function(cellView, magnetEl, opt) { + + opt = this.resolveHighlighter(opt); + if (!opt) return; + if (!magnetEl.id) { + magnetEl.id = V.uniqueId(); + } + + var key = opt.name + magnetEl.id + JSON.stringify(opt.options); + if (!this._highlights[key]) { + + var highlighter = opt.highlighter; + highlighter.highlight(cellView, magnetEl, joint.util.assign({}, opt.options)); + + this._highlights[key] = { + cellView: cellView, + magnetEl: magnetEl, + opt: opt.options, + highlighter: highlighter + }; + } + }, + + onCellUnhighlight: function(cellView, magnetEl, opt) { + + opt = this.resolveHighlighter(opt); + if (!opt) return; + + var key = opt.name + magnetEl.id + JSON.stringify(opt.options); + var highlight = this._highlights[key]; + if (highlight) { + + // Use the cellView and magnetEl that were used by the highlighter.highlight() method. + highlight.highlighter.unhighlight(highlight.cellView, highlight.magnetEl, highlight.opt); + + this._highlights[key] = null; + } + }, + + // Interaction. + // ------------ + + pointerdblclick: function(evt) { + + evt.preventDefault(); + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + + if (view) { + view.pointerdblclick(evt, localPoint.x, localPoint.y); + + } else { + this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y); + } + }, + + pointerclick: function(evt) { + + // Trigger event only if mouse has not moved. + if (this._mousemoved <= this.options.clickThreshold) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + + if (view) { + view.pointerclick(evt, localPoint.x, localPoint.y); + + } else { + this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y); + } + } + }, + + contextmenu: function(evt) { + + if (this.options.preventContextMenu) evt.preventDefault(); + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + + if (view) { + view.contextmenu(evt, localPoint.x, localPoint.y); + + } else { + this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y); + } + }, + + pointerdown: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + + if (view) { + + evt.preventDefault(); + view.pointerdown(evt, localPoint.x, localPoint.y); + + } else { + + if (this.options.preventDefaultBlankAction) evt.preventDefault(); + + this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y); + } + + this.delegateDragEvents(view, evt.data); + }, + + pointermove: function(evt) { + + evt.preventDefault(); + + // mouse moved counter + var data = this.eventData(evt); + data.mousemoved || (data.mousemoved = 0); + var mousemoved = ++data.mousemoved; + if (mousemoved <= this.options.moveThreshold) return; + + evt = joint.util.normalizeEvent(evt); + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + + var view = data.sourceView; + if (view) { + view.pointermove(evt, localPoint.x, localPoint.y); + } else { + this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y); + } + + this.eventData(evt, data); + }, + + pointerup: function(evt) { + + this.undelegateDocumentEvents(); + + evt = joint.util.normalizeEvent(evt); + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + + var view = this.eventData(evt).sourceView; + if (view) { + view.pointerup(evt, localPoint.x, localPoint.y); + } else { + this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); + } + + this.delegateEvents(); + }, + + mouseover: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + if (view) { + view.mouseover(evt); + + } else { + if (this.el === evt.target) return; // prevent border of paper from triggering this + this.trigger('blank:mouseover', evt); + } + }, + + mouseout: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + if (view) { + view.mouseout(evt); + + } else { + if (this.el === evt.target) return; // prevent border of paper from triggering this + this.trigger('blank:mouseout', evt); + } + }, + + mouseenter: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + var relatedView = this.findView(evt.relatedTarget); + if (view) { + // mouse moved from tool over view? + if (relatedView === view) return; + view.mouseenter(evt); + } else { + if (relatedView) return; + // `paper` (more descriptive), not `blank` + this.trigger('paper:mouseenter', evt); + } + }, + + mouseleave: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + var relatedView = this.findView(evt.relatedTarget); + if (view) { + // mouse moved from view over tool? + if (relatedView === view) return; + view.mouseleave(evt); + } else { + if (relatedView) return; + // `paper` (more descriptive), not `blank` + this.trigger('paper:mouseleave', evt); + } + }, + + mousewheel: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + var originalEvent = evt.originalEvent; + var localPoint = this.snapToGrid({ x: originalEvent.clientX, y: originalEvent.clientY }); + var delta = Math.max(-1, Math.min(1, (originalEvent.wheelDelta || -originalEvent.detail))); + + if (view) { + view.mousewheel(evt, localPoint.x, localPoint.y, delta); + + } else { + this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta); + } + }, + + onevent: function(evt) { + + var eventNode = evt.currentTarget; + var eventName = eventNode.getAttribute('event'); + if (eventName) { + var view = this.findView(eventNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + view.onevent(evt, eventName, localPoint.x, localPoint.y); + } + } + }, + + onmagnet: function(evt) { + + var magnetNode = evt.currentTarget; + var magnetValue = magnetNode.getAttribute('magnet'); + if (magnetValue) { + var view = this.findView(magnetNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + if (!this.options.validateMagnet(view, magnetNode)) return; + + var localPoint = this.snapToGrid(evt.clientX, evt.clientY); + view.onmagnet(evt, localPoint.x, localPoint.y); + } + } + }, + + onlabel: function(evt) { + + var labelNode = evt.currentTarget; + var view = this.findView(labelNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid(evt.clientX, evt.clientY); + view.onlabel(evt, localPoint.x, localPoint.y); + } + }, + + delegateDragEvents: function(view, data) { + + data || (data = {}); + this.eventData({ data: data }, { sourceView: view || null, mousemoved: 0 }); + this.delegateDocumentEvents(null, data); + this.undelegateEvents(); + }, + + // Guard the specified event. If the event is not interesting, guard returns `true`. + // Otherwise, it returns `false`. + guard: function(evt, view) { + + if (this.options.guard && this.options.guard(evt, view)) { + return true; + } + + if (evt.data && evt.data.guarded !== undefined) { + return evt.data.guarded; + } + + if (view && view.model && (view.model instanceof joint.dia.Cell)) { + return false; + } + + if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { + return false; + } + + return true; // Event guarded. Paper should not react on it in any way. + }, + + setGridSize: function(gridSize) { + + this.options.gridSize = gridSize; + + if (this.options.drawGrid) { + this.drawGrid(); + } + + return this; + }, + + clearGrid: function() { + + if (this.$grid) { + this.$grid.css('backgroundImage', 'none'); + } + return this; + }, + + _getGriRefs: function() { + + if (!this._gridCache) { + + this._gridCache = { + root: V('svg', { width: '100%', height: '100%' }, V('defs')), + patterns: {}, + add: function(id, vel) { + V(this.root.node.childNodes[0]).append(vel); + this.patterns[id] = vel; + this.root.append(V('rect', { width: "100%", height: "100%", fill: 'url(#' + id + ')' })); + }, + get: function(id) { + return this.patterns[id] + }, + exist: function(id) { + return this.patterns[id] !== undefined; + } + } + } + + return this._gridCache; + }, + + setGrid: function(drawGrid) { + + this.clearGrid(); + + this._gridCache = null; + this._gridSettings = []; + + var optionsList = Array.isArray(drawGrid) ? drawGrid : [drawGrid || {}]; + optionsList.forEach(function(item) { + this._gridSettings.push.apply(this._gridSettings, this._resolveDrawGridOption(item)); + }, this); + return this; + }, + + _resolveDrawGridOption: function(opt) { + + var namespace = this.constructor.gridPatterns; + if (joint.util.isString(opt) && Array.isArray(namespace[opt])) { + return namespace[opt].map(function(item) { + return joint.util.assign({}, item); + }); + } + + var options = opt || { args: [{}] }; + var isArray = Array.isArray(options); + var name = options.name; + + if (!isArray && !name && !options.markup ) { + name = 'dot'; + } + + if (name && Array.isArray(namespace[name])) { + var pattern = namespace[name].map(function(item) { + return joint.util.assign({}, item); + }); + + var args = Array.isArray(options.args) ? options.args : [options.args || {}]; + + joint.util.defaults(args[0], joint.util.omit(opt, 'args')); + for (var i = 0; i < args.length; i++) { + if (pattern[i]) { + joint.util.assign(pattern[i], args[i]); + } + } + return pattern; + } + + return isArray ? options : [options]; + }, + + drawGrid: function(opt) { + + var gridSize = this.options.gridSize; + if (gridSize <= 1) { + return this.clearGrid(); + } + + var localOptions = Array.isArray(opt) ? opt : [opt]; + + var ctm = this.matrix(); + var refs = this._getGriRefs(); + + this._gridSettings.forEach(function(gridLayerSetting, index) { + + var id = 'pattern_' + index; + var options = joint.util.merge(gridLayerSetting, localOptions[index], { + sx: ctm.a || 1, + sy: ctm.d || 1, + ox: ctm.e || 0, + oy: ctm.f || 0 + }); + + options.width = gridSize * (ctm.a || 1) * (options.scaleFactor || 1); + options.height = gridSize * (ctm.d || 1) * (options.scaleFactor || 1); + + if (!refs.exist(id)) { + refs.add(id, V('pattern', { id: id, patternUnits: 'userSpaceOnUse' }, V(options.markup))); + } + + var patternDefVel = refs.get(id); + + if (joint.util.isFunction(options.update)) { + options.update(patternDefVel.node.childNodes[0], options); + } + + var x = options.ox % options.width; + if (x < 0) x += options.width; + + var y = options.oy % options.height; + if (y < 0) y += options.height; + + patternDefVel.attr({ + x: x, + y: y, + width: options.width, + height: options.height + }); + }); + + var patternUri = new XMLSerializer().serializeToString(refs.root.node); + patternUri = 'url(data:image/svg+xml;base64,' + btoa(patternUri) + ')'; + + this.$grid.css('backgroundImage', patternUri); + + return this; + }, + + updateBackgroundImage: function(opt) { + + opt = opt || {}; + + var backgroundPosition = opt.position || 'center'; + var backgroundSize = opt.size || 'auto auto'; + + var currentScale = this.scale(); + var currentTranslate = this.translate(); + + // backgroundPosition + if (joint.util.isObject(backgroundPosition)) { + var x = currentTranslate.tx + (currentScale.sx * (backgroundPosition.x || 0)); + var y = currentTranslate.ty + (currentScale.sy * (backgroundPosition.y || 0)); + backgroundPosition = x + 'px ' + y + 'px'; + } + + // backgroundSize + if (joint.util.isObject(backgroundSize)) { + backgroundSize = g.rect(backgroundSize).scale(currentScale.sx, currentScale.sy); + backgroundSize = backgroundSize.width + 'px ' + backgroundSize.height + 'px'; + } + + this.$background.css({ + backgroundSize: backgroundSize, + backgroundPosition: backgroundPosition + }); + }, + + drawBackgroundImage: function(img, opt) { + + // Clear the background image if no image provided + if (!(img instanceof HTMLImageElement)) { + this.$background.css('backgroundImage', ''); + return; + } + + opt = opt || {}; + + var backgroundImage; + var backgroundSize = opt.size; + var backgroundRepeat = opt.repeat || 'no-repeat'; + var backgroundOpacity = opt.opacity || 1; + var backgroundQuality = Math.abs(opt.quality) || 1; + var backgroundPattern = this.constructor.backgroundPatterns[joint.util.camelCase(backgroundRepeat)]; + + if (joint.util.isFunction(backgroundPattern)) { + // 'flip-x', 'flip-y', 'flip-xy', 'watermark' and custom + img.width *= backgroundQuality; + img.height *= backgroundQuality; + var canvas = backgroundPattern(img, opt); + if (!(canvas instanceof HTMLCanvasElement)) { + throw new Error('dia.Paper: background pattern must return an HTML Canvas instance'); + } + + backgroundImage = canvas.toDataURL('image/png'); + backgroundRepeat = 'repeat'; + if (joint.util.isObject(backgroundSize)) { + // recalculate the tile size if an object passed in + backgroundSize.width *= canvas.width / img.width; + backgroundSize.height *= canvas.height / img.height; + } else if (backgroundSize === undefined) { + // calcule the tile size if no provided + opt.size = { + width: canvas.width / backgroundQuality, + height: canvas.height / backgroundQuality + }; + } + } else { + // backgroundRepeat: + // no-repeat', 'round', 'space', 'repeat', 'repeat-x', 'repeat-y' + backgroundImage = img.src; + if (backgroundSize === undefined) { + // pass the image size for the backgroundSize if no size provided + opt.size = { + width: img.width, + height: img.height + }; + } + } + + this.$background.css({ + opacity: backgroundOpacity, + backgroundRepeat: backgroundRepeat, + backgroundImage: 'url(' + backgroundImage + ')' + }); + + this.updateBackgroundImage(opt); + }, + + updateBackgroundColor: function(color) { + + this.$el.css('backgroundColor', color || ''); + }, + + drawBackground: function(opt) { + + opt = opt || {}; + + this.updateBackgroundColor(opt.color); + + if (opt.image) { + opt = this._background = joint.util.cloneDeep(opt); + var img = document.createElement('img'); + img.onload = this.drawBackgroundImage.bind(this, img, opt); + img.src = opt.image; + } else { + this.drawBackgroundImage(null); + this._background = null; + } + + return this; + }, + + setInteractivity: function(value) { + + this.options.interactive = value; + + joint.util.invoke(this._views, 'setInteractivity', value); + }, + + // Paper definitions. + // ------------------ + + isDefined: function(defId) { + + return !!this.svg.getElementById(defId); + }, + + defineFilter: function(filter) { + + if (!joint.util.isObject(filter)) { + throw new TypeError('dia.Paper: defineFilter() requires 1. argument to be an object.'); + } + + var filterId = filter.id; + var name = filter.name; + // Generate a hash code from the stringified filter definition. This gives us + // a unique filter ID for different definitions. + if (!filterId) { + filterId = name + this.svg.id + joint.util.hashCode(JSON.stringify(filter)); + } + // If the filter already exists in the document, + // we're done and we can just use it (reference it using `url()`). + // If not, create one. + if (!this.isDefined(filterId)) { + + var namespace = joint.util.filter; + var filterSVGString = namespace[name] && namespace[name](filter.args || {}); + if (!filterSVGString) { + throw new Error('Non-existing filter ' + name); + } + + // Set the filter area to be 3x the bounding box of the cell + // and center the filter around the cell. + var filterAttrs = joint.util.assign({ + filterUnits: 'objectBoundingBox', + x: -1, + y: -1, + width: 3, + height: 3 + }, filter.attrs, { + id: filterId + }); + + V(filterSVGString, filterAttrs).appendTo(this.defs); + } + + return filterId; + }, + + defineGradient: function(gradient) { + + if (!joint.util.isObject(gradient)) { + throw new TypeError('dia.Paper: defineGradient() requires 1. argument to be an object.'); + } + + var gradientId = gradient.id; + var type = gradient.type; + var stops = gradient.stops; + // Generate a hash code from the stringified filter definition. This gives us + // a unique filter ID for different definitions. + if (!gradientId) { + gradientId = type + this.svg.id + joint.util.hashCode(JSON.stringify(gradient)); + } + // If the gradient already exists in the document, + // we're done and we can just use it (reference it using `url()`). + // If not, create one. + if (!this.isDefined(gradientId)) { + + var stopTemplate = joint.util.template(''); + var gradientStopsStrings = joint.util.toArray(stops).map(function(stop) { + return stopTemplate({ + offset: stop.offset, + color: stop.color, + opacity: Number.isFinite(stop.opacity) ? stop.opacity : 1 + }); + }); + + var gradientSVGString = [ + '<' + type + '>', + gradientStopsStrings.join(''), + '' + ].join(''); + + var gradientAttrs = joint.util.assign({ id: gradientId }, gradient.attrs); + + V(gradientSVGString, gradientAttrs).appendTo(this.defs); + } + + return gradientId; + }, + + defineMarker: function(marker) { + + if (!joint.util.isObject(marker)) { + throw new TypeError('dia.Paper: defineMarker() requires 1. argument to be an object.'); + } + + var markerId = marker.id; + + // Generate a hash code from the stringified filter definition. This gives us + // a unique filter ID for different definitions. + if (!markerId) { + markerId = this.svg.id + joint.util.hashCode(JSON.stringify(marker)); + } + + if (!this.isDefined(markerId)) { + + var attrs = joint.util.omit(marker, 'type', 'userSpaceOnUse'); + var pathMarker = V('marker', { + id: markerId, + orient: 'auto', + overflow: 'visible', + markerUnits: marker.markerUnits || 'userSpaceOnUse' + }, [ + V(marker.type || 'path', attrs) + ]); + + pathMarker.appendTo(this.defs); + } + + return markerId; + } +}, { + + backgroundPatterns: { + + flipXy: function(img) { + // d b + // q p + + var canvas = document.createElement('canvas'); + var imgWidth = img.width; + var imgHeight = img.height; + + canvas.width = 2 * imgWidth; + canvas.height = 2 * imgHeight; + + var ctx = canvas.getContext('2d'); + // top-left image + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + // xy-flipped bottom-right image + ctx.setTransform(-1, 0, 0, -1, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + // x-flipped top-right image + ctx.setTransform(-1, 0, 0, 1, canvas.width, 0); + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + // y-flipped bottom-left image + ctx.setTransform(1, 0, 0, -1, 0, canvas.height); + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + + return canvas; + }, + + flipX: function(img) { + // d b + // d b + + var canvas = document.createElement('canvas'); + var imgWidth = img.width; + var imgHeight = img.height; + + canvas.width = imgWidth * 2; + canvas.height = imgHeight; + + var ctx = canvas.getContext('2d'); + // left image + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + // flipped right image + ctx.translate(2 * imgWidth, 0); + ctx.scale(-1, 1); + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + + return canvas; + }, + + flipY: function(img) { + // d d + // q q + + var canvas = document.createElement('canvas'); + var imgWidth = img.width; + var imgHeight = img.height; + + canvas.width = imgWidth; + canvas.height = imgHeight * 2; + + var ctx = canvas.getContext('2d'); + // top image + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + // flipped bottom image + ctx.translate(0, 2 * imgHeight); + ctx.scale(1, -1); + ctx.drawImage(img, 0, 0, imgWidth, imgHeight); + + return canvas; + }, + + watermark: function(img, opt) { + // d + // d + + opt = opt || {}; + + var imgWidth = img.width; + var imgHeight = img.height; + + var canvas = document.createElement('canvas'); + canvas.width = imgWidth * 3; + canvas.height = imgHeight * 3; + + var ctx = canvas.getContext('2d'); + var angle = joint.util.isNumber(opt.watermarkAngle) ? -opt.watermarkAngle : -20; + var radians = g.toRad(angle); + var stepX = canvas.width / 4; + var stepY = canvas.height / 4; + + for (var i = 0; i < 4; i ++) { + for (var j = 0; j < 4; j++) { + if ((i + j) % 2 > 0) { + // reset the current transformations + ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY); + ctx.rotate(radians); + ctx.drawImage(img, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight); + } + } + } + + return canvas; + } + }, + + gridPatterns: { + dot: [{ + color: '#AAAAAA', + thickness: 1, + markup: 'rect', + update: function(el, opt) { + V(el).attr({ + width: opt.thickness * opt.sx, + height: opt.thickness * opt.sy, + fill: opt.color + }); + } + }], + fixedDot: [{ + color: '#AAAAAA', + thickness: 1, + markup: 'rect', + update: function(el, opt) { + var size = opt.sx <= 1 ? opt.thickness * opt.sx : opt.thickness; + V(el).attr({ width: size, height: size, fill: opt.color }); + } + }], + mesh: [{ + color: '#AAAAAA', + thickness: 1, + markup: 'path', + update: function(el, opt) { + + var d; + var width = opt.width; + var height = opt.height; + var thickness = opt.thickness; + + if (width - thickness >= 0 && height - thickness >= 0) { + d = ['M', width, 0, 'H0 M0 0 V0', height].join(' '); + } else { + d = 'M 0 0 0 0'; + } + + V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness }); + } + }], + doubleMesh: [{ + color: '#AAAAAA', + thickness: 1, + markup: 'path', + update: function(el, opt) { + + var d; + var width = opt.width; + var height = opt.height; + var thickness = opt.thickness; + + if (width - thickness >= 0 && height - thickness >= 0) { + d = ['M', width, 0, 'H0 M0 0 V0', height].join(' '); + } else { + d = 'M 0 0 0 0'; + } + + V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness }); + } + }, { + color: '#000000', + thickness: 3, + scaleFactor: 4, + markup: 'path', + update: function(el, opt) { + + var d; + var width = opt.width; + var height = opt.height; + var thickness = opt.thickness; + + if (width - thickness >= 0 && height - thickness >= 0) { + d = ['M', width, 0, 'H0 M0 0 V0', height].join(' '); + } else { + d = 'M 0 0 0 0'; + } + + V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness }); + } + }] + } +}); + +(function(joint, _, util) { + + var PortData = function(data) { + + var clonedData = util.cloneDeep(data) || {}; + this.ports = []; + this.groups = {}; + this.portLayoutNamespace = joint.layout.Port; + this.portLabelLayoutNamespace = joint.layout.PortLabel; + + this._init(clonedData); + }; + + PortData.prototype = { + + getPorts: function() { + return this.ports; + }, + + getGroup: function(name) { + return this.groups[name] || {}; + }, + + getPortsByGroup: function(groupName) { + + return this.ports.filter(function(port) { + return port.group === groupName; + }); + }, + + getGroupPortsMetrics: function(groupName, elBBox) { + + var group = this.getGroup(groupName); + var ports = this.getPortsByGroup(groupName); + + var groupPosition = group.position || {}; + var groupPositionName = groupPosition.name; + var namespace = this.portLayoutNamespace; + if (!namespace[groupPositionName]) { + groupPositionName = 'left'; + } + + var groupArgs = groupPosition.args || {}; + var portsArgs = ports.map(function(port) { + return port && port.position && port.position.args; + }); + var groupPortTransformations = namespace[groupPositionName](portsArgs, elBBox, groupArgs); + + var accumulator = { + ports: ports, + result: [] + }; + + util.toArray(groupPortTransformations).reduce(function(res, portTransformation, index) { + var port = res.ports[index]; + res.result.push({ + portId: port.id, + portTransformation: portTransformation, + labelTransformation: this._getPortLabelLayout(port, g.Point(portTransformation), elBBox), + portAttrs: port.attrs, + portSize: port.size, + labelSize: port.label.size + }); + return res; + }.bind(this), accumulator); + + return accumulator.result; + }, + + _getPortLabelLayout: function(port, portPosition, elBBox) { + + var namespace = this.portLabelLayoutNamespace; + var labelPosition = port.label.position.name || 'left'; + + if (namespace[labelPosition]) { + return namespace[labelPosition](portPosition, elBBox, port.label.position.args); + } + + return null; + }, + + _init: function(data) { + + // prepare groups + if (util.isObject(data.groups)) { + var groups = Object.keys(data.groups); + for (var i = 0, n = groups.length; i < n; i++) { + var key = groups[i]; + this.groups[key] = this._evaluateGroup(data.groups[key]); + } + } + + // prepare ports + var ports = util.toArray(data.items); + for (var j = 0, m = ports.length; j < m; j++) { + this.ports.push(this._evaluatePort(ports[j])); + } + }, + + _evaluateGroup: function(group) { + + return util.merge(group, { + position: this._getPosition(group.position, true), + label: this._getLabel(group, true) + }); + }, + + _evaluatePort: function(port) { + + var evaluated = util.assign({}, port); + + var group = this.getGroup(port.group); + + evaluated.markup = evaluated.markup || group.markup; + evaluated.attrs = util.merge({}, group.attrs, evaluated.attrs); + evaluated.position = this._createPositionNode(group, evaluated); + evaluated.label = util.merge({}, group.label, this._getLabel(evaluated)); + evaluated.z = this._getZIndex(group, evaluated); + evaluated.size = util.assign({}, group.size, evaluated.size); + + return evaluated; + }, + + _getZIndex: function(group, port) { + + if (util.isNumber(port.z)) { + return port.z; + } + if (util.isNumber(group.z) || group.z === 'auto') { + return group.z; + } + return 'auto'; + }, + + _createPositionNode: function(group, port) { + + return util.merge({ + name: 'left', + args: {} + }, group.position, { args: port.args }); + }, + + _getPosition: function(position, setDefault) { + + var args = {}; + var positionName; + + if (util.isFunction(position)) { + positionName = 'fn'; + args.fn = position; + } else if (util.isString(position)) { + positionName = position; + } else if (position === undefined) { + positionName = setDefault ? 'left' : null; + } else if (Array.isArray(position)) { + positionName = 'absolute'; + args.x = position[0]; + args.y = position[1]; + } else if (util.isObject(position)) { + positionName = position.name; + util.assign(args, position.args); + } + + var result = { args: args }; + + if (positionName) { + result.name = positionName; + } + return result; + }, + + _getLabel: function(item, setDefaults) { + + var label = item.label || {}; + + var ret = label; + ret.position = this._getPosition(label.position, setDefaults); + + return ret; + } + }; + + util.assign(joint.dia.Element.prototype, { + + _initializePorts: function() { + + this._createPortData(); + this.on('change:ports', function() { + + this._processRemovedPort(); + this._createPortData(); + }, this); + }, + + /** + * remove links tied wiht just removed element + * @private + */ + _processRemovedPort: function() { + + var current = this.get('ports') || {}; + var currentItemsMap = {}; + + util.toArray(current.items).forEach(function(item) { + currentItemsMap[item.id] = true; + }); + + var previous = this.previous('ports') || {}; + var removed = {}; + + util.toArray(previous.items).forEach(function(item) { + if (!currentItemsMap[item.id]) { + removed[item.id] = true; + } + }); + + var graph = this.graph; + if (graph && !util.isEmpty(removed)) { + + var inboundLinks = graph.getConnectedLinks(this, { inbound: true }); + inboundLinks.forEach(function(link) { + + if (removed[link.get('target').port]) link.remove(); + }); + + var outboundLinks = graph.getConnectedLinks(this, { outbound: true }); + outboundLinks.forEach(function(link) { + + if (removed[link.get('source').port]) link.remove(); + }); + } + }, + + /** + * @returns {boolean} + */ + hasPorts: function() { + + return this.prop('ports/items').length > 0; + }, + + /** + * @param {string} id + * @returns {boolean} + */ + hasPort: function(id) { + + return this.getPortIndex(id) !== -1; + }, + + /** + * @returns {Array} + */ + getPorts: function() { + + return util.cloneDeep(this.prop('ports/items')) || []; + }, + + /** + * @param {string} id + * @returns {object} + */ + getPort: function(id) { + + return util.cloneDeep(util.toArray(this.prop('ports/items')).find( function(port) { + return port.id && port.id === id; + })); + }, + + /** + * @param {string} groupName + * @returns {Object} + */ + getPortsPositions: function(groupName) { + + var portsMetrics = this._portSettingsData.getGroupPortsMetrics(groupName, g.Rect(this.size())); + + return portsMetrics.reduce(function(positions, metrics) { + var transformation = metrics.portTransformation; + positions[metrics.portId] = { + x: transformation.x, + y: transformation.y, + angle: transformation.angle + }; + return positions; + }, {}); + }, + + /** + * @param {string|Port} port port id or port + * @returns {number} port index + */ + getPortIndex: function(port) { + + var id = util.isObject(port) ? port.id : port; + + if (!this._isValidPortId(id)) { + return -1; + } + + return util.toArray(this.prop('ports/items')).findIndex(function(item) { + return item.id === id; + }); + }, + + /** + * @param {object} port + * @param {object} [opt] + * @returns {joint.dia.Element} + */ + addPort: function(port, opt) { + + if (!util.isObject(port) || Array.isArray(port)) { + throw new Error('Element: addPort requires an object.'); + } + + var ports = util.assign([], this.prop('ports/items')); + ports.push(port); + this.prop('ports/items', ports, opt); + + return this; + }, + + /** + * @param {string} portId + * @param {string|object=} path + * @param {*=} value + * @param {object=} opt + * @returns {joint.dia.Element} + */ + portProp: function(portId, path, value, opt) { + + var index = this.getPortIndex(portId); + + if (index === -1) { + throw new Error('Element: unable to find port with id ' + portId); + } + + var args = Array.prototype.slice.call(arguments, 1); + if (Array.isArray(path)) { + args[0] = ['ports', 'items', index].concat(path); + } else if (util.isString(path)) { + + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. + args[0] = ['ports/items/', index, '/', path].join(''); + + } else { + + args = ['ports/items/' + index]; + if (util.isPlainObject(path)) { + args.push(path); + args.push(value); + } + } + + return this.prop.apply(this, args); + }, + + _validatePorts: function() { + + var portsAttr = this.get('ports') || {}; + + var errorMessages = []; + portsAttr = portsAttr || {}; + var ports = util.toArray(portsAttr.items); + + ports.forEach(function(p) { + + if (typeof p !== 'object') { + errorMessages.push('Element: invalid port ', p); + } + + if (!this._isValidPortId(p.id)) { + p.id = util.uuid(); + } + }, this); + + if (joint.util.uniq(ports, 'id').length !== ports.length) { + errorMessages.push('Element: found id duplicities in ports.'); + } + + return errorMessages; + }, + + /** + * @param {string} id port id + * @returns {boolean} + * @private + */ + _isValidPortId: function(id) { + + return id !== null && id !== undefined && !util.isObject(id); + }, + + addPorts: function(ports, opt) { + + if (ports.length) { + this.prop('ports/items', util.assign([], this.prop('ports/items')).concat(ports), opt); + } + + return this; + }, + + removePort: function(port, opt) { + + var options = opt || {}; + var ports = util.assign([], this.prop('ports/items')); + + var index = this.getPortIndex(port); + + if (index !== -1) { + ports.splice(index, 1); + options.rewrite = true; + this.prop('ports/items', ports, options); + } + + return this; + }, + + /** + * @private + */ + _createPortData: function() { + + var err = this._validatePorts(); + + if (err.length > 0) { + this.set('ports', this.previous('ports')); + throw new Error(err.join(' ')); + } + + var prevPortData; + + if (this._portSettingsData) { + + prevPortData = this._portSettingsData.getPorts(); + } + + this._portSettingsData = new PortData(this.get('ports')); + + var curPortData = this._portSettingsData.getPorts(); + + if (prevPortData) { + + var added = curPortData.filter(function(item) { + if (!prevPortData.find(function(prevPort) { return prevPort.id === item.id;})) { + return item; + } + }); + + var removed = prevPortData.filter(function(item) { + if (!curPortData.find(function(curPort) { return curPort.id === item.id;})) { + return item; + } + }); + + if (removed.length > 0) { + this.trigger('ports:remove', this, removed); + } + + if (added.length > 0) { + this.trigger('ports:add', this, added); + } + } + } + }); + + util.assign(joint.dia.ElementView.prototype, { + + portContainerMarkup: 'g', + portMarkup: [{ + tagName: 'circle', + selector: 'circle', + attributes: { + 'r': 10, + 'fill': '#FFFFFF', + 'stroke': '#000000' + } + }], + portLabelMarkup: [{ + tagName: 'text', + selector: 'text', + attributes: { + 'fill': '#000000' + } + }], + /** @type {Object} */ + _portElementsCache: null, + + /** + * @private + */ + _initializePorts: function() { + + this._portElementsCache = {}; + + this.listenTo(this.model, 'change:ports', function() { + + this._refreshPorts(); + }); + }, + + /** + * @typedef {Object} Port + * + * @property {string} id + * @property {Object} position + * @property {Object} label + * @property {Object} attrs + * @property {string} markup + * @property {string} group + */ + + /** + * @private + */ + _refreshPorts: function() { + + this._removePorts(); + this._portElementsCache = {}; + + this._renderPorts(); + }, + + /** + * @private + */ + _renderPorts: function() { + + // references to rendered elements without z-index + var elementReferences = []; + var elem = this._getContainerElement(); + + for (var i = 0, count = elem.node.childNodes.length; i < count; i++) { + elementReferences.push(elem.node.childNodes[i]); + } + + var portsGropsByZ = util.groupBy(this.model._portSettingsData.getPorts(), 'z'); + var withoutZKey = 'auto'; + + // render non-z first + util.toArray(portsGropsByZ[withoutZKey]).forEach(function(port) { + var portElement = this._getPortElement(port); + elem.append(portElement); + elementReferences.push(portElement); + }, this); + + var groupNames = Object.keys(portsGropsByZ); + for (var k = 0; k < groupNames.length; k++) { + var groupName = groupNames[k]; + if (groupName !== withoutZKey) { + var z = parseInt(groupName, 10); + this._appendPorts(portsGropsByZ[groupName], z, elementReferences); + } + } + + this._updatePorts(); + }, + + /** + * @returns {V} + * @private + */ + _getContainerElement: function() { + + return this.rotatableNode || this.vel; + }, + + /** + * @param {Array}ports + * @param {number} z + * @param refs + * @private + */ + _appendPorts: function(ports, z, refs) { + + var containerElement = this._getContainerElement(); + var portElements = util.toArray(ports).map(this._getPortElement, this); + + if (refs[z] || z < 0) { + V(refs[Math.max(z, 0)]).before(portElements); + } else { + containerElement.append(portElements); + } + }, + + /** + * Try to get element from cache, + * @param port + * @returns {*} + * @private + */ + _getPortElement: function(port) { + + if (this._portElementsCache[port.id]) { + return this._portElementsCache[port.id].portElement; + } + return this._createPortElement(port); + }, + + findPortNode: function(portId, selector) { + var portCache = this._portElementsCache[portId]; + if (!portCache) return null; + var portRoot = portCache.portContentElement.node; + var portSelectors = portCache.portContentSelectors; + return this.findBySelector(selector, portRoot, portSelectors)[0]; + }, + + /** + * @private + */ + _updatePorts: function() { + + // layout ports without group + this._updatePortGroup(undefined); + // layout ports with explicit group + var groupsNames = Object.keys(this.model._portSettingsData.groups); + groupsNames.forEach(this._updatePortGroup, this); + }, + + /** + * @private + */ + _removePorts: function() { + util.invoke(this._portElementsCache, 'portElement.remove'); + }, + + /** + * @param {Port} port + * @returns {V} + * @private + */ + _createPortElement: function(port) { + + + var portElement; + var labelElement; + + var portMarkup = this._getPortMarkup(port); + var portSelectors; + if (Array.isArray(portMarkup)) { + var portDoc = util.parseDOMJSON(portMarkup); + var portFragment = portDoc.fragment; + if (portFragment.childNodes.length > 1) { + portElement = V('g').append(portFragment); + } else { + portElement = V(portFragment.firstChild); + } + portSelectors = portDoc.selectors; + } else { + portElement = V(portMarkup); + if (Array.isArray(portElement)) { + portElement = V('g').append(portElement); + } + } + + if (!portElement) { + throw new Error('ElementView: Invalid port markup.'); + } + + portElement.attr({ + 'port': port.id, + 'port-group': port.group + }); + + var labelMarkup = this._getPortLabelMarkup(port.label); + var labelSelectors; + if (Array.isArray(labelMarkup)) { + var labelDoc = util.parseDOMJSON(labelMarkup); + var labelFragment = labelDoc.fragment; + if (labelFragment.childNodes.length > 1) { + labelElement = V('g').append(labelFragment); + } else { + labelElement = V(labelFragment.firstChild); + } + labelSelectors = labelDoc.selectors; + } else { + labelElement = V(labelMarkup); + if (Array.isArray(labelElement)) { + labelElement = V('g').append(labelElement); + } + } + + if (!labelElement) { + throw new Error('ElementView: Invalid port label markup.'); + } + + var portContainerSelectors; + if (portSelectors && labelSelectors) { + for (var key in labelSelectors) { + if (portSelectors[key]) throw new Error('ElementView: selectors within port must be unique.'); + } + portContainerSelectors = util.assign({}, portSelectors, labelSelectors); + } else { + portContainerSelectors = portSelectors || labelSelectors; + } + + var portContainerElement = V(this.portContainerMarkup) + .addClass('joint-port') + .append([ + portElement.addClass('joint-port-body'), + labelElement.addClass('joint-port-label') + ]); + + this._portElementsCache[port.id] = { + portElement: portContainerElement, + portLabelElement: labelElement, + portSelectors: portContainerSelectors, + portLabelSelectors: labelSelectors, + portContentElement: portElement, + portContentSelectors: portSelectors + }; + + return portContainerElement; + }, + + /** * @param {string=} groupName * @private */ - _updatePortGroup: function(groupName) { + _updatePortGroup: function(groupName) { + + var elementBBox = g.Rect(this.model.size()); + var portsMetrics = this.model._portSettingsData.getGroupPortsMetrics(groupName, elementBBox); + + for (var i = 0, n = portsMetrics.length; i < n; i++) { + var metrics = portsMetrics[i]; + var portId = metrics.portId; + var cached = this._portElementsCache[portId] || {}; + var portTransformation = metrics.portTransformation; + this.applyPortTransform(cached.portElement, portTransformation); + this.updateDOMSubtreeAttributes(cached.portElement.node, metrics.portAttrs, { + rootBBox: new g.Rect(metrics.portSize), + selectors: cached.portSelectors + }); + + var labelTransformation = metrics.labelTransformation; + if (labelTransformation) { + this.applyPortTransform(cached.portLabelElement, labelTransformation, (-portTransformation.angle || 0)); + this.updateDOMSubtreeAttributes(cached.portLabelElement.node, labelTransformation.attrs, { + rootBBox: new g.Rect(metrics.labelSize), + selectors: cached.portLabelSelectors + }); + } + } + }, + + /** + * @param {Vectorizer} element + * @param {{dx:number, dy:number, angle: number, attrs: Object, x:number: y:number}} transformData + * @param {number=} initialAngle + * @constructor + */ + applyPortTransform: function(element, transformData, initialAngle) { + + var matrix = V.createSVGMatrix() + .rotate(initialAngle || 0) + .translate(transformData.x || 0, transformData.y || 0) + .rotate(transformData.angle || 0); + + element.transform(matrix, { absolute: true }); + }, + + /** + * @param {Port} port + * @returns {string} + * @private + */ + _getPortMarkup: function(port) { + + return port.markup || this.model.get('portMarkup') || this.model.portMarkup || this.portMarkup; + }, + + /** + * @param {Object} label + * @returns {string} + * @private + */ + _getPortLabelMarkup: function(label) { + + return label.markup || this.model.get('portLabelMarkup') || this.model.portLabelMarkup || this.portLabelMarkup; + } + + }); +}(joint, _, joint.util)); + +joint.dia.Element.define('basic.Generic', { + attrs: { + '.': { fill: '#ffffff', stroke: 'none' } + } +}); + +joint.shapes.basic.Generic.define('basic.Rect', { + attrs: { + 'rect': { + fill: '#ffffff', + stroke: '#000000', + width: 100, + height: 60 + }, + 'text': { + fill: '#000000', + text: '', + 'font-size': 14, + 'ref-x': .5, + 'ref-y': .5, + 'text-anchor': 'middle', + 'y-alignment': 'middle', + 'font-family': 'Arial, helvetica, sans-serif' + } + } +}, { + markup: '' +}); + +joint.shapes.basic.TextView = joint.dia.ElementView.extend({ + + initialize: function() { + joint.dia.ElementView.prototype.initialize.apply(this, arguments); + // The element view is not automatically rescaled to fit the model size + // when the attribute 'attrs' is changed. + this.listenTo(this.model, 'change:attrs', this.resize); + } +}); + +joint.shapes.basic.Generic.define('basic.Text', { + attrs: { + 'text': { + 'font-size': 18, + fill: '#000000' + } + } +}, { + markup: '', +}); + +joint.shapes.basic.Generic.define('basic.Circle', { + size: { width: 60, height: 60 }, + attrs: { + 'circle': { + fill: '#ffffff', + stroke: '#000000', + r: 30, + cx: 30, + cy: 30 + }, + 'text': { + 'font-size': 14, + text: '', + 'text-anchor': 'middle', + 'ref-x': .5, + 'ref-y': .5, + 'y-alignment': 'middle', + fill: '#000000', + 'font-family': 'Arial, helvetica, sans-serif' + } + } +}, { + markup: '', +}); + +joint.shapes.basic.Generic.define('basic.Ellipse', { + size: { width: 60, height: 40 }, + attrs: { + 'ellipse': { + fill: '#ffffff', + stroke: '#000000', + rx: 30, + ry: 20, + cx: 30, + cy: 20 + }, + 'text': { + 'font-size': 14, + text: '', + 'text-anchor': 'middle', + 'ref-x': .5, + 'ref-y': .5, + 'y-alignment': 'middle', + fill: '#000000', + 'font-family': 'Arial, helvetica, sans-serif' + } + } +}, { + markup: '', +}); + +joint.shapes.basic.Generic.define('basic.Polygon', { + size: { width: 60, height: 40 }, + attrs: { + 'polygon': { + fill: '#ffffff', + stroke: '#000000' + }, + 'text': { + 'font-size': 14, + text: '', + 'text-anchor': 'middle', + 'ref-x': .5, + 'ref-dy': 20, + 'y-alignment': 'middle', + fill: '#000000', + 'font-family': 'Arial, helvetica, sans-serif' + } + } +}, { + markup: '', +}); + +joint.shapes.basic.Generic.define('basic.Polyline', { + size: { width: 60, height: 40 }, + attrs: { + 'polyline': { + fill: '#ffffff', + stroke: '#000000' + }, + 'text': { + 'font-size': 14, + text: '', + 'text-anchor': 'middle', + 'ref-x': .5, + 'ref-dy': 20, + 'y-alignment': 'middle', + fill: '#000000', + 'font-family': 'Arial, helvetica, sans-serif' + } + } +}, { + markup: '', +}); + +joint.shapes.basic.Generic.define('basic.Image', { + attrs: { + 'text': { + 'font-size': 14, + text: '', + 'text-anchor': 'middle', + 'ref-x': .5, + 'ref-dy': 20, + 'y-alignment': 'middle', + fill: '#000000', + 'font-family': 'Arial, helvetica, sans-serif' + } + } +}, { + markup: '', +}); + +joint.shapes.basic.Generic.define('basic.Path', { + size: { width: 60, height: 60 }, + attrs: { + 'path': { + fill: '#ffffff', + stroke: '#000000' + }, + 'text': { + 'font-size': 14, + text: '', + 'text-anchor': 'middle', + 'ref': 'path', + 'ref-x': .5, + 'ref-dy': 10, + fill: '#000000', + 'font-family': 'Arial, helvetica, sans-serif' + } + } + +}, { + markup: '', +}); + +joint.shapes.basic.Path.define('basic.Rhombus', { + attrs: { + 'path': { + d: 'M 30 0 L 60 30 30 60 0 30 z' + }, + 'text': { + 'ref-y': .5, + 'ref-dy': null, + 'y-alignment': 'middle' + } + } +}); + + +/** + * @deprecated use the port api instead + */ +// PortsModelInterface is a common interface for shapes that have ports. This interface makes it easy +// to create new shapes with ports functionality. It is assumed that the new shapes have +// `inPorts` and `outPorts` array properties. Only these properties should be used to set ports. +// In other words, using this interface, it is no longer recommended to set ports directly through the +// `attrs` object. + +// Usage: +// joint.shapes.custom.MyElementWithPorts = joint.shapes.basic.Path.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, { +// getPortAttrs: function(portName, index, total, selector, type) { +// var attrs = {}; +// var portClass = 'port' + index; +// var portSelector = selector + '>.' + portClass; +// var portTextSelector = portSelector + '>text'; +// var portBodySelector = portSelector + '>.port-body'; +// +// attrs[portTextSelector] = { text: portName }; +// attrs[portBodySelector] = { port: { id: portName || _.uniqueId(type) , type: type } }; +// attrs[portSelector] = { ref: 'rect', 'ref-y': (index + 0.5) * (1 / total) }; +// +// if (selector === '.outPorts') { attrs[portSelector]['ref-dx'] = 0; } +// +// return attrs; +// } +//})); +joint.shapes.basic.PortsModelInterface = { + + initialize: function() { + + this.updatePortsAttrs(); + this.on('change:inPorts change:outPorts', this.updatePortsAttrs, this); + + // Call the `initialize()` of the parent. + this.constructor.__super__.constructor.__super__.initialize.apply(this, arguments); + }, + + updatePortsAttrs: function(eventName) { + + if (this._portSelectors) { + + var newAttrs = joint.util.omit(this.get('attrs'), this._portSelectors); + this.set('attrs', newAttrs, { silent: true }); + } + + // This holds keys to the `attrs` object for all the port specific attribute that + // we set in this method. This is necessary in order to remove previously set + // attributes for previous ports. + this._portSelectors = []; + + var attrs = {}; + + joint.util.toArray(this.get('inPorts')).forEach(function(portName, index, ports) { + var portAttributes = this.getPortAttrs(portName, index, ports.length, '.inPorts', 'in'); + this._portSelectors = this._portSelectors.concat(Object.keys(portAttributes)); + joint.util.assign(attrs, portAttributes); + }, this); + + joint.util.toArray(this.get('outPorts')).forEach(function(portName, index, ports) { + var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out'); + this._portSelectors = this._portSelectors.concat(Object.keys(portAttributes)); + joint.util.assign(attrs, portAttributes); + }, this); + + // Silently set `attrs` on the cell so that noone knows the attrs have changed. This makes sure + // that, for example, command manager does not register `change:attrs` command but only + // the important `change:inPorts`/`change:outPorts` command. + this.attr(attrs, { silent: true }); + // Manually call the `processPorts()` method that is normally called on `change:attrs` (that we just made silent). + this.processPorts(); + // Let the outside world (mainly the `ModelView`) know that we're done configuring the `attrs` object. + this.trigger('process:ports'); + }, + + getPortSelector: function(name) { + + var selector = '.inPorts'; + var index = this.get('inPorts').indexOf(name); + + if (index < 0) { + selector = '.outPorts'; + index = this.get('outPorts').indexOf(name); + + if (index < 0) throw new Error("getPortSelector(): Port doesn't exist."); + } + + return selector + '>g:nth-child(' + (index + 1) + ')>.port-body'; + } +}; + +joint.shapes.basic.PortsViewInterface = { + + initialize: function() { + + // `Model` emits the `process:ports` whenever it's done configuring the `attrs` object for ports. + this.listenTo(this.model, 'process:ports', this.update); + + joint.dia.ElementView.prototype.initialize.apply(this, arguments); + }, + + update: function() { + + // First render ports so that `attrs` can be applied to those newly created DOM elements + // in `ElementView.prototype.update()`. + this.renderPorts(); + joint.dia.ElementView.prototype.update.apply(this, arguments); + }, + + renderPorts: function() { + + var $inPorts = this.$('.inPorts').empty(); + var $outPorts = this.$('.outPorts').empty(); + + var portTemplate = joint.util.template(this.model.portMarkup); + + var ports = this.model.ports || []; + ports.filter(function(p) { + return p.type === 'in'; + }).forEach(function(port, index) { + + $inPorts.append(V(portTemplate({ id: index, port: port })).node); + }); + + ports.filter(function(p) { + return p.type === 'out'; + }).forEach(function(port, index) { + + $outPorts.append(V(portTemplate({ id: index, port: port })).node); + }); + } +}; + +joint.shapes.basic.Generic.define('basic.TextBlock', { + // see joint.css for more element styles + attrs: { + rect: { + fill: '#ffffff', + stroke: '#000000', + width: 80, + height: 100 + }, + text: { + fill: '#000000', + 'font-size': 14, + 'font-family': 'Arial, helvetica, sans-serif' + }, + '.content': { + text: '', + 'ref-x': .5, + 'ref-y': .5, + 'y-alignment': 'middle', + 'x-alignment': 'middle' + } + }, + + content: '' +}, { + markup: [ + '', + '', + joint.env.test('svgforeignobject') ? '
' : '', + '' + ].join(''), + + initialize: function() { + + this.listenTo(this, 'change:size', this.updateSize); + this.listenTo(this, 'change:content', this.updateContent); + this.updateSize(this, this.get('size')); + this.updateContent(this, this.get('content')); + joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments); + }, + + updateSize: function(cell, size) { + + // Selector `foreignObject' doesn't work across all browsers, we're using class selector instead. + // We have to clone size as we don't want attributes.div.style to be same object as attributes.size. + this.attr({ + '.fobj': joint.util.assign({}, size), + div: { + style: joint.util.assign({}, size) + } + }); + }, + + updateContent: function(cell, content) { + + if (joint.env.test('svgforeignobject')) { + + // Content element is a
element. + this.attr({ + '.content': { + html: joint.util.sanitizeHTML(content) + } + }); + + } else { + + // Content element is a element. + // SVG elements don't have innerHTML attribute. + this.attr({ + '.content': { + text: content + } + }); + } + }, + + // Here for backwards compatibility: + setForeignObjectSize: function() { + + this.updateSize.apply(this, arguments); + }, + + // Here for backwards compatibility: + setDivContent: function() { + + this.updateContent.apply(this, arguments); + } +}); + +// TextBlockView implements the fallback for IE when no foreignObject exists and +// the text needs to be manually broken. +joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({ + + initialize: function() { + + joint.dia.ElementView.prototype.initialize.apply(this, arguments); + + // Keep this for backwards compatibility: + this.noSVGForeignObjectElement = !joint.env.test('svgforeignobject'); + + if (!joint.env.test('svgforeignobject')) { + + this.listenTo(this.model, 'change:content change:size', function(cell) { + // avoiding pass of extra paramters + this.updateContent(cell); + }); + } + }, + + update: function(cell, renderingOnlyAttrs) { + + var model = this.model; + + if (!joint.env.test('svgforeignobject')) { + + // Update everything but the content first. + var noTextAttrs = joint.util.omit(renderingOnlyAttrs || model.get('attrs'), '.content'); + joint.dia.ElementView.prototype.update.call(this, model, noTextAttrs); + + if (!renderingOnlyAttrs || joint.util.has(renderingOnlyAttrs, '.content')) { + // Update the content itself. + this.updateContent(model, renderingOnlyAttrs); + } + + } else { + + joint.dia.ElementView.prototype.update.call(this, model, renderingOnlyAttrs); + } + }, + + updateContent: function(cell, renderingOnlyAttrs) { + + // Create copy of the text attributes + var textAttrs = joint.util.merge({}, (renderingOnlyAttrs || cell.get('attrs'))['.content']); + + textAttrs = joint.util.omit(textAttrs, 'text'); + + // Break the content to fit the element size taking into account the attributes + // set on the model. + var text = joint.util.breakText(cell.get('content'), cell.get('size'), textAttrs, { + // measuring sandbox svg document + svgDocument: this.paper.svg + }); + + // Create a new attrs with same structure as the model attrs { text: { *textAttributes* }} + var attrs = joint.util.setByPath({}, '.content', textAttrs, '/'); + + // Replace text attribute with the one we just processed. + attrs['.content'].text = text; + + // Update the view using renderingOnlyAttributes parameter. + joint.dia.ElementView.prototype.update.call(this, cell, attrs); + } +}); + +(function(dia, util, env, V) { + + 'use strict'; + + // ELEMENTS + + var Element = dia.Element; + + Element.define('standard.Rectangle', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body', + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Circle', { + attrs: { + body: { + refCx: '50%', + refCy: '50%', + refR: '50%', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'circle', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Ellipse', { + attrs: { + body: { + refCx: '50%', + refCy: '50%', + refRx: '50%', + refRy: '50%', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'ellipse', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Path', { + attrs: { + body: { + refD: 'M 0 0 L 10 0 10 10 0 10 Z', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Polygon', { + attrs: { + body: { + refPoints: '0 0 10 0 10 10 0 10', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'polygon', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Polyline', { + attrs: { + body: { + refPoints: '0 0 10 0 10 10 0 10 0 0', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'polyline', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Image', { + attrs: { + image: { + refWidth: '100%', + refHeight: '100%', + // xlinkHref: '[URL]' + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'image', + selector: 'image' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.BorderedImage', { + attrs: { + border: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + strokeWidth: 2 + }, + image: { + // xlinkHref: '[URL]' + refWidth: -1, + refHeight: -1, + x: 0.5, + y: 0.5 + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'image', + selector: 'image' + }, { + tagName: 'rect', + selector: 'border', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.EmbeddedImage', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + fill: '#FFFFFF', + strokeWidth: 2 + }, + image: { + // xlinkHref: '[URL]' + refWidth: '30%', + refHeight: -20, + x: 10, + y: 10, + preserveAspectRatio: 'xMidYMin' + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'left', + refX: '30%', + refX2: 20, // 10 + 10 + refY: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, { + tagName: 'image', + selector: 'image' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.HeaderedRectangle', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + header: { + refWidth: '100%', + height: 30, + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + headerText: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: 15, + fontSize: 16, + fill: '#333333' + }, + bodyText: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + refY2: 15, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, { + tagName: 'rect', + selector: 'header' + }, { + tagName: 'text', + selector: 'headerText' + }, { + tagName: 'text', + selector: 'bodyText' + }] + }); + + var CYLINDER_TILT = 10; + + joint.dia.Element.define('standard.Cylinder', { + attrs: { + body: { + lateralArea: CYLINDER_TILT, + fill: '#FFFFFF', + stroke: '#333333', + strokeWidth: 2 + }, + top: { + refCx: '50%', + cy: CYLINDER_TILT, + refRx: '50%', + ry: CYLINDER_TILT, + fill: '#FFFFFF', + stroke: '#333333', + strokeWidth: 2 + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 15, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'body' + }, { + tagName: 'ellipse', + selector: 'top' + }, { + tagName: 'text', + selector: 'label' + }], + + topRy: function(t, opt) { + // getter + if (t === undefined) return this.attr('body/lateralArea'); + + // setter + var isPercentage = util.isPercentage(t); + + var bodyAttrs = { lateralArea: t }; + var topAttrs = isPercentage + ? { refCy: t, refRy: t, cy: null, ry: null } + : { refCy: null, refRy: null, cy: t, ry: t }; + + return this.attr({ body: bodyAttrs, top: topAttrs }, opt); + } + + }, { + attributes: { + lateralArea: { + set: function(t, refBBox) { + var isPercentage = util.isPercentage(t); + if (isPercentage) t = parseFloat(t) / 100; + + var x = refBBox.x; + var y = refBBox.y; + var w = refBBox.width; + var h = refBBox.height; + + // curve control point variables + var rx = w / 2; + var ry = isPercentage ? (h * t) : t; + + var kappa = V.KAPPA; + var cx = kappa * rx; + var cy = kappa * (isPercentage ? (h * t) : t); + + // shape variables + var xLeft = x; + var xCenter = x + (w / 2); + var xRight = x + w; + + var ySideTop = y + ry; + var yCurveTop = ySideTop - ry; + var ySideBottom = y + h - ry; + var yCurveBottom = y + h; + + // return calculated shape + var data = [ + 'M', xLeft, ySideTop, + 'L', xLeft, ySideBottom, + 'C', x, (ySideBottom + cy), (xCenter - cx), yCurveBottom, xCenter, yCurveBottom, + 'C', (xCenter + cx), yCurveBottom, xRight, (ySideBottom + cy), xRight, ySideBottom, + 'L', xRight, ySideTop, + 'C', xRight, (ySideTop - cy), (xCenter + cx), yCurveTop, xCenter, yCurveTop, + 'C', (xCenter - cx), yCurveTop, xLeft, (ySideTop - cy), xLeft, ySideTop, + 'Z' + ]; + return { d: data.join(' ') }; + } + } + } + }); + + var foLabelMarkup = { + tagName: 'foreignObject', + selector: 'foreignObject', + attributes: { + 'overflow': 'hidden' + }, + children: [{ + tagName: 'div', + namespaceURI: 'http://www.w3.org/1999/xhtml', + selector: 'label', + style: { + width: '100%', + height: '100%', + position: 'static', + backgroundColor: 'transparent', + textAlign: 'center', + margin: 0, + padding: '0px 5px', + boxSizing: 'border-box', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }] + }; + + var svgLabelMarkup = { + tagName: 'text', + selector: 'label', + attributes: { + 'text-anchor': 'middle' + } + }; + + Element.define('standard.TextBlock', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + fill: '#ffffff', + strokeWidth: 2 + }, + foreignObject: { + refWidth: '100%', + refHeight: '100%' + }, + label: { + style: { + fontSize: 14 + } + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, + (env.test('svgforeignobject')) ? foLabelMarkup : svgLabelMarkup + ] + }, { + attributes: { + text: { + set: function(text, refBBox, node, attrs) { + if (node instanceof HTMLElement) { + node.textContent = text; + } else { + // No foreign object + var style = attrs.style || {}; + var wrapValue = { text: text, width: -5, height: '100%' }; + var wrapAttrs = util.assign({ textVerticalAnchor: 'middle' }, style); + dia.attributes.textWrap.set.call(this, wrapValue, refBBox, node, wrapAttrs); + return { fill: style.color || null }; + } + }, + position: function(text, refBBox, node) { + // No foreign object + if (node instanceof SVGElement) return refBBox.center(); + } + } + } + }); + + // LINKS + + var Link = dia.Link; + + Link.define('standard.Link', { + attrs: { + line: { + connection: true, + stroke: '#333333', + strokeWidth: 2, + strokeLinejoin: 'round', + targetMarker: { + type: 'path', + d: 'M 10 -5 0 0 10 5 z' + } + }, + wrapper: { + connection: true, + strokeWidth: 10, + strokeLinejoin: 'round' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'wrapper', + attributes: { + 'fill': 'none', + 'cursor': 'pointer', + 'stroke': 'transparent' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none', + 'pointer-events': 'none' + } + }] + }); + + Link.define('standard.DoubleLink', { + attrs: { + line: { + connection: true, + stroke: '#DDDDDD', + strokeWidth: 4, + strokeLinejoin: 'round', + targetMarker: { + type: 'path', + stroke: '#000000', + d: 'M 10 -3 10 -10 -2 0 10 10 10 3' + } + }, + outline: { + connection: true, + stroke: '#000000', + strokeWidth: 6, + strokeLinejoin: 'round' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'outline', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none' + } + }] + }); + + Link.define('standard.ShadowLink', { + attrs: { + line: { + connection: true, + stroke: '#FF0000', + strokeWidth: 20, + strokeLinejoin: 'round', + targetMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M 0 -10 -10 0 0 10 z' + }, + sourceMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M -10 -10 0 0 -10 10 0 10 0 -10 z' + } + }, + shadow: { + connection: true, + refX: 3, + refY: 6, + stroke: '#000000', + strokeOpacity: 0.2, + strokeWidth: 20, + strokeLinejoin: 'round', + targetMarker: { + 'type': 'path', + 'd': 'M 0 -10 -10 0 0 10 z', + 'stroke': 'none' + }, + sourceMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M -10 -10 0 0 -10 10 0 10 0 -10 z' + } + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'shadow', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none' + } + }] + }); + + +})(joint.dia, joint.util, joint.env, V); + +joint.routers.manhattan = (function(g, _, joint, util) { + + 'use strict'; + + var config = { + + // size of the step to find a route (the grid of the manhattan pathfinder) + step: 10, + + // the number of route finding loops that cause the router to abort + // returns fallback route instead + maximumLoops: 2000, + + // the number of decimal places to round floating point coordinates + precision: 10, + + // maximum change of direction + maxAllowedDirectionChange: 90, + + // should the router use perpendicular linkView option? + // does not connect anchor of element but rather a point close-by that is orthogonal + // this looks much better + perpendicular: true, + + // should the source and/or target not be considered as obstacles? + excludeEnds: [], // 'source', 'target' + + // should certain types of elements not be considered as obstacles? + excludeTypes: ['basic.Text'], + + // possible starting directions from an element + startDirections: ['top', 'right', 'bottom', 'left'], + + // possible ending directions to an element + endDirections: ['top', 'right', 'bottom', 'left'], + + // specify the directions used above and what they mean + directionMap: { + top: { x: 0, y: -1 }, + right: { x: 1, y: 0 }, + bottom: { x: 0, y: 1 }, + left: { x: -1, y: 0 } + }, + + // cost of an orthogonal step + cost: function() { + + return this.step; + }, + + // an array of directions to find next points on the route + // different from start/end directions + directions: function() { + + var step = this.step; + var cost = this.cost(); + + return [ + { offsetX: step , offsetY: 0 , cost: cost }, + { offsetX: 0 , offsetY: step , cost: cost }, + { offsetX: -step , offsetY: 0 , cost: cost }, + { offsetX: 0 , offsetY: -step , cost: cost } + ]; + }, + + // a penalty received for direction change + penalties: function() { + + return { + 0: 0, + 45: this.step / 2, + 90: this.step / 2 + }; + }, + + // padding applied on the element bounding boxes + paddingBox: function() { + + var step = this.step; + + return { + x: -step, + y: -step, + width: 2 * step, + height: 2 * step + }; + }, + + // a router to use when the manhattan router fails + // (one of the partial routes returns null) + fallbackRouter: function(vertices, opt, linkView) { + + if (!util.isFunction(joint.routers.orthogonal)) { + throw new Error('Manhattan requires the orthogonal router as default fallback.'); + } + + return joint.routers.orthogonal(vertices, util.assign({}, config, opt), linkView); + }, + + /* Deprecated */ + // a simple route used in situations when main routing method fails + // (exceed max number of loop iterations, inaccessible) + fallbackRoute: function(from, to, opt) { + + return null; // null result will trigger the fallbackRouter + + // left for reference: + /*// Find an orthogonal route ignoring obstacles. + + var point = ((opt.previousDirAngle || 0) % 180 === 0) + ? new g.Point(from.x, to.y) + : new g.Point(to.x, from.y); + + return [point];*/ + }, + + // if a function is provided, it's used to route the link while dragging an end + // i.e. function(from, to, opt) { return []; } + draggingRoute: null + }; + + // HELPER CLASSES // + + // Map of obstacles + // Helper structure to identify whether a point lies inside an obstacle. + function ObstacleMap(opt) { + + this.map = {}; + this.options = opt; + // tells how to divide the paper when creating the elements map + this.mapGridSize = 100; + } + + ObstacleMap.prototype.build = function(graph, link) { + + var opt = this.options; + + // source or target element could be excluded from set of obstacles + var excludedEnds = util.toArray(opt.excludeEnds).reduce(function(res, item) { + + var end = link.get(item); + if (end) { + var cell = graph.getCell(end.id); + if (cell) { + res.push(cell); + } + } + + return res; + }, []); - var elementBBox = g.Rect(this.model.size()); - var portsMetrics = this.model._portSettingsData.getGroupPortsMetrics(groupName, elementBBox); + // Exclude any embedded elements from the source and the target element. + var excludedAncestors = []; + + var source = graph.getCell(link.get('source').id); + if (source) { + excludedAncestors = util.union(excludedAncestors, source.getAncestors().map(function(cell) { return cell.id })); + } + + var target = graph.getCell(link.get('target').id); + if (target) { + excludedAncestors = util.union(excludedAncestors, target.getAncestors().map(function(cell) { return cell.id })); + } + + // Builds a map of all elements for quicker obstacle queries (i.e. is a point contained + // in any obstacle?) (a simplified grid search). + // The paper is divided into smaller cells, where each holds information about which + // elements belong to it. When we query whether a point lies inside an obstacle we + // don't need to go through all obstacles, we check only those in a particular cell. + var mapGridSize = this.mapGridSize; + + graph.getElements().reduce(function(map, element) { + + var isExcludedType = util.toArray(opt.excludeTypes).includes(element.get('type')); + var isExcludedEnd = excludedEnds.find(function(excluded) { return excluded.id === element.id }); + var isExcludedAncestor = excludedAncestors.includes(element.id); + + var isExcluded = isExcludedType || isExcludedEnd || isExcludedAncestor; + if (!isExcluded) { + var bbox = element.getBBox().moveAndExpand(opt.paddingBox); + + var origin = bbox.origin().snapToGrid(mapGridSize); + var corner = bbox.corner().snapToGrid(mapGridSize); + + for (var x = origin.x; x <= corner.x; x += mapGridSize) { + for (var y = origin.y; y <= corner.y; y += mapGridSize) { + var gridKey = x + '@' + y; + map[gridKey] = map[gridKey] || []; + map[gridKey].push(bbox); + } + } + } + + return map; + }, this.map); + + return this; + }; + + ObstacleMap.prototype.isPointAccessible = function(point) { + + var mapKey = point.clone().snapToGrid(this.mapGridSize).toString(); + + return util.toArray(this.map[mapKey]).every( function(obstacle) { + return !obstacle.containsPoint(point); + }); + }; + + // Sorted Set + // Set of items sorted by given value. + function SortedSet() { + this.items = []; + this.hash = {}; + this.values = {}; + this.OPEN = 1; + this.CLOSE = 2; + } + + SortedSet.prototype.add = function(item, value) { + + if (this.hash[item]) { + // item removal + this.items.splice(this.items.indexOf(item), 1); + } else { + this.hash[item] = this.OPEN; + } + + this.values[item] = value; + + var index = joint.util.sortedIndex(this.items, item, function(i) { + return this.values[i]; + }.bind(this)); + + this.items.splice(index, 0, item); + }; + + SortedSet.prototype.remove = function(item) { + + this.hash[item] = this.CLOSE; + }; + + SortedSet.prototype.isOpen = function(item) { + + return this.hash[item] === this.OPEN; + }; + + SortedSet.prototype.isClose = function(item) { + + return this.hash[item] === this.CLOSE; + }; + + SortedSet.prototype.isEmpty = function() { + + return this.items.length === 0; + }; + + SortedSet.prototype.pop = function() { + + var item = this.items.shift(); + this.remove(item); + return item; + }; + + // HELPERS // + + // return source bbox + function getSourceBBox(linkView, opt) { + + // expand by padding box + if (opt && opt.paddingBox) return linkView.sourceBBox.clone().moveAndExpand(opt.paddingBox); + + return linkView.sourceBBox.clone(); + } + + // return target bbox + function getTargetBBox(linkView, opt) { + + // expand by padding box + if (opt && opt.paddingBox) return linkView.targetBBox.clone().moveAndExpand(opt.paddingBox); + + return linkView.targetBBox.clone(); + } + + // return source anchor + function getSourceAnchor(linkView, opt) { + + if (linkView.sourceAnchor) return linkView.sourceAnchor; + + // fallback: center of bbox + var sourceBBox = getSourceBBox(linkView, opt); + return sourceBBox.center(); + } + + // return target anchor + function getTargetAnchor(linkView, opt) { + + if (linkView.targetAnchor) return linkView.targetAnchor; + + // fallback: center of bbox + var targetBBox = getTargetBBox(linkView, opt); + return targetBBox.center(); // default + } + + // returns a direction index from start point to end point + // corrects for grid deformation between start and end + function getDirectionAngle(start, end, numDirections, grid, opt) { + + var quadrant = 360 / numDirections; + var angleTheta = start.theta(fixAngleEnd(start, end, grid, opt)); + var normalizedAngle = g.normalizeAngle(angleTheta + (quadrant / 2)); + return quadrant * Math.floor(normalizedAngle / quadrant); + } + + // helper function for getDirectionAngle() + // corrects for grid deformation + // (if a point is one grid steps away from another in both dimensions, + // it is considered to be 45 degrees away, even if the real angle is different) + // this causes visible angle discrepancies if `opt.step` is much larger than `paper.gridSize` + function fixAngleEnd(start, end, grid, opt) { + + var step = opt.step; + + var diffX = end.x - start.x; + var diffY = end.y - start.y; + + var gridStepsX = diffX / grid.x; + var gridStepsY = diffY / grid.y + + var distanceX = gridStepsX * step; + var distanceY = gridStepsY * step; + + return new g.Point(start.x + distanceX, start.y + distanceY); + } + + // return the change in direction between two direction angles + function getDirectionChange(angle1, angle2) { + + var directionChange = Math.abs(angle1 - angle2); + return (directionChange > 180) ? (360 - directionChange) : directionChange; + } + + // fix direction offsets according to current grid + function getGridOffsets(directions, grid, opt) { + + var step = opt.step; + + util.toArray(opt.directions).forEach(function(direction) { + + direction.gridOffsetX = (direction.offsetX / step) * grid.x; + direction.gridOffsetY = (direction.offsetY / step) * grid.y; + }); + } + + // get grid size in x and y dimensions, adapted to source and target positions + function getGrid(step, source, target) { + + return { + source: source.clone(), + x: getGridDimension(target.x - source.x, step), + y: getGridDimension(target.y - source.y, step) + } + } + + // helper function for getGrid() + function getGridDimension(diff, step) { + + // return step if diff = 0 + if (!diff) return step; + + var absDiff = Math.abs(diff); + var numSteps = Math.round(absDiff / step); + + // return absDiff if less than one step apart + if (!numSteps) return absDiff; + + // otherwise, return corrected step + var roundedDiff = numSteps * step; + var remainder = absDiff - roundedDiff; + var stepCorrection = remainder / numSteps; + + return step + stepCorrection; + } + + // return a clone of point snapped to grid + function snapToGrid(point, grid) { + + var source = grid.source; + + var snappedX = g.snapToGrid(point.x - source.x, grid.x) + source.x; + var snappedY = g.snapToGrid(point.y - source.y, grid.y) + source.y; + + return new g.Point(snappedX, snappedY); + } + + // round the point to opt.precision + function round(point, opt) { + + if (!point) return point; + + return point.round(opt.precision); + } + + // return a string representing the point + // string is rounded to nearest int in both dimensions + function getKey(point) { + + return point.clone().round().toString(); + } + + // return a normalized vector from given point + // used to determine the direction of a difference of two points + function normalizePoint(point) { + + return new g.Point( + point.x === 0 ? 0 : Math.abs(point.x) / point.x, + point.y === 0 ? 0 : Math.abs(point.y) / point.y + ); + } + + // PATHFINDING // + + // reconstructs a route by concatenating points with their parents + function reconstructRoute(parents, points, tailPoint, from, to, opt) { + + var route = []; + + var prevDiff = normalizePoint(to.difference(tailPoint)); + + var currentKey = getKey(tailPoint); + var parent = parents[currentKey]; + + var point; + while (parent) { + + point = round(points[currentKey], opt); + + var diff = normalizePoint(point.difference(round(parent.clone(), opt))); + if (!diff.equals(prevDiff)) { + route.unshift(point); + prevDiff = diff; + } + + currentKey = getKey(parent); + parent = parents[currentKey]; + } + + var leadPoint = round(points[currentKey], opt); + + var fromDiff = normalizePoint(leadPoint.difference(from)); + if (!fromDiff.equals(prevDiff)) { + route.unshift(leadPoint); + } + + return route; + } + + // heuristic method to determine the distance between two points + function estimateCost(from, endPoints) { + + var min = Infinity; + + for (var i = 0, len = endPoints.length; i < len; i++) { + var cost = from.manhattanDistance(endPoints[i]); + if (cost < min) min = cost; + } + + return min; + } + + // find points around the bbox taking given directions into account + // lines are drawn from anchor in given directions, intersections recorded + // if anchor is outside bbox, only those directions that intersect get a rect point + // the anchor itself is returned as rect point (representing some directions) + // (since those directions are unobstructed by the bbox) + function getRectPoints(anchor, bbox, directionList, grid, opt) { + + var directionMap = opt.directionMap; + + var snappedAnchor = round(snapToGrid(anchor, grid), opt); + var snappedCenter = round(snapToGrid(bbox.center(), grid), opt); + var anchorCenterVector = snappedAnchor.difference(snappedCenter); + + var keys = util.isObject(directionMap) ? Object.keys(directionMap) : []; + var dirList = util.toArray(directionList); + var rectPoints = keys.reduce(function(res, key) { + + if (dirList.includes(key)) { + var direction = directionMap[key]; + + // create a line that is guaranteed to intersect the bbox if bbox is in the direction + // even if anchor lies outside of bbox + var endpoint = new g.Point( + snappedAnchor.x + direction.x * (Math.abs(anchorCenterVector.x) + bbox.width), + snappedAnchor.y + direction.y * (Math.abs(anchorCenterVector.y) + bbox.height) + ); + var intersectionLine = new g.Line(anchor, endpoint); + + // get the farther intersection, in case there are two + // (that happens if anchor lies next to bbox) + var intersections = intersectionLine.intersect(bbox) || []; + var numIntersections = intersections.length; + var farthestIntersectionDistance; + var farthestIntersection = null; + for (var i = 0; i < numIntersections; i++) { + var currentIntersection = intersections[i]; + var distance = snappedAnchor.squaredDistance(currentIntersection); + if (farthestIntersectionDistance === undefined || (distance > farthestIntersectionDistance)) { + farthestIntersectionDistance = distance; + farthestIntersection = snapToGrid(currentIntersection, grid); + } + } + var point = round(farthestIntersection, opt); + + // if an intersection was found in this direction, it is our rectPoint + if (point) { + // if the rectPoint lies inside the bbox, offset it by one more step + if (bbox.containsPoint(point)) { + round(point.offset(direction.x * grid.x, direction.y * grid.y), opt); + } + + // then add the point to the result array + res.push(point); + } + } + + return res; + }, []); + + // if anchor lies outside of bbox, add it to the array of points + if (!bbox.containsPoint(snappedAnchor)) rectPoints.push(snappedAnchor); + + return rectPoints; + } + + // finds the route between two points/rectangles (`from`, `to`) implementing A* algorithm + // rectangles get rect points assigned by getRectPoints() + function findRoute(from, to, map, opt) { + + // Get grid for this route. + + var sourceAnchor, targetAnchor; + + if (from instanceof g.Rect) { // `from` is sourceBBox + sourceAnchor = getSourceAnchor(this, opt).clone(); + } else { + sourceAnchor = from.clone(); + } + + if (to instanceof g.Rect) { // `to` is targetBBox + targetAnchor = getTargetAnchor(this, opt).clone(); + } else { + targetAnchor = to.clone(); + } + + var grid = getGrid(opt.step, sourceAnchor, targetAnchor); + + // Get pathfinding points. + + var start, end; + var startPoints, endPoints; + + // set of points we start pathfinding from + if (from instanceof g.Rect) { // `from` is sourceBBox + start = round(snapToGrid(sourceAnchor, grid), opt); + startPoints = getRectPoints(start, from, opt.startDirections, grid, opt); + + } else { + start = round(snapToGrid(sourceAnchor, grid), opt); + startPoints = [start]; + } + + // set of points we want the pathfinding to finish at + if (to instanceof g.Rect) { // `to` is targetBBox + end = round(snapToGrid(targetAnchor, grid), opt); + endPoints = getRectPoints(targetAnchor, to, opt.endDirections, grid, opt); + + } else { + end = round(snapToGrid(targetAnchor, grid), opt); + endPoints = [end]; + } + + // take into account only accessible rect points (those not under obstacles) + startPoints = startPoints.filter(map.isPointAccessible, map); + endPoints = endPoints.filter(map.isPointAccessible, map); + + // Check that there is an accessible route point on both sides. + // Otherwise, use fallbackRoute(). + if (startPoints.length > 0 && endPoints.length > 0) { - for (var i = 0, n = portsMetrics.length; i < n; i++) { - var metrics = portsMetrics[i]; - var portId = metrics.portId; - var cached = this._portElementsCache[portId] || {}; - var portTransformation = metrics.portTransformation; - this.applyPortTransform(cached.portElement, portTransformation); - this.updateDOMSubtreeAttributes(cached.portElement.node, metrics.portAttrs, { - rootBBox: g.Rect(metrics.portSize) - }); + // The set of tentative points to be evaluated, initially containing the start points. + // Rounded to nearest integer for simplicity. + var openSet = new SortedSet(); + // Keeps reference to actual points for given elements of the open set. + var points = {}; + // Keeps reference to a point that is immediate predecessor of given element. + var parents = {}; + // Cost from start to a point along best known path. + var costs = {}; - var labelTransformation = metrics.labelTransformation; - if (labelTransformation) { - this.applyPortTransform(cached.portLabelElement, labelTransformation, (-portTransformation.angle || 0)); - this.updateDOMSubtreeAttributes(cached.portLabelElement.node, labelTransformation.attrs, { - rootBBox: g.Rect(metrics.labelSize) - }); - } + for (var i = 0, n = startPoints.length; i < n; i++) { + var point = startPoints[i]; + + var key = getKey(point); + openSet.add(key, estimateCost(point, endPoints)); + points[key] = point; + costs[key] = 0; } - }, - /** - * @param {Vectorizer} element - * @param {{dx:number, dy:number, angle: number, attrs: Object, x:number: y:number}} transformData - * @param {number=} initialAngle - * @constructor - */ - applyPortTransform: function(element, transformData, initialAngle) { + var previousRouteDirectionAngle = opt.previousDirectionAngle; // undefined for first route + var isPathBeginning = (previousRouteDirectionAngle === undefined); - var matrix = V.createSVGMatrix() - .rotate(initialAngle || 0) - .translate(transformData.x || 0, transformData.y || 0) - .rotate(transformData.angle || 0); + // directions + var direction, directionChange; + var directions = opt.directions; + getGridOffsets(directions, grid, opt); - element.transform(matrix, { absolute: true }); - }, + var numDirections = directions.length; - /** - * @param {Port} port - * @returns {string} - * @private - */ - _getPortMarkup: function(port) { + var endPointsKeys = util.toArray(endPoints).reduce(function(res, endPoint) { - return port.markup || this.model.get('portMarkup') || this.model.portMarkup || this.portMarkup; - }, + var key = getKey(endPoint); + res.push(key); + return res; + }, []); - /** - * @param {Object} label - * @returns {string} - * @private - */ - _getPortLabelMarkup: function(label) { + // main route finding loop + var loopsRemaining = opt.maximumLoops; + while (!openSet.isEmpty() && loopsRemaining > 0) { - return label.markup || this.model.get('portLabelMarkup') || this.model.portLabelMarkup || this.portLabelMarkup; - } + // remove current from the open list + var currentKey = openSet.pop(); + var currentPoint = points[currentKey]; + var currentParent = parents[currentKey]; + var currentCost = costs[currentKey]; - }); -}(joint, _, joint.util)); + var isRouteBeginning = (currentParent === undefined); // undefined for route starts + var isStart = currentPoint.equals(start); // (is source anchor or `from` point) = can leave in any direction -joint.dia.Element.define('basic.Generic', { - attrs: { - '.': { fill: '#ffffff', stroke: 'none' } - } -}); + var previousDirectionAngle; + if (!isRouteBeginning) previousDirectionAngle = getDirectionAngle(currentParent, currentPoint, numDirections, grid, opt); // a vertex on the route + else if (!isPathBeginning) previousDirectionAngle = previousRouteDirectionAngle; // beginning of route on the path + else if (!isStart) previousDirectionAngle = getDirectionAngle(start, currentPoint, numDirections, grid, opt); // beginning of path, start rect point + else previousDirectionAngle = null; // beginning of path, source anchor or `from` point -joint.shapes.basic.Generic.define('basic.Rect', { - attrs: { - 'rect': { - fill: '#ffffff', - stroke: '#000000', - width: 100, - height: 60 - }, - 'text': { - fill: '#000000', - text: '', - 'font-size': 14, - 'ref-x': .5, - 'ref-y': .5, - 'text-anchor': 'middle', - 'y-alignment': 'middle', - 'font-family': 'Arial, helvetica, sans-serif' - } - } -}, { - markup: '' -}); + // check if we reached any endpoint + if (endPointsKeys.indexOf(currentKey) >= 0) { + opt.previousDirectionAngle = previousDirectionAngle; + return reconstructRoute(parents, points, currentPoint, start, end, opt); + } -joint.shapes.basic.TextView = joint.dia.ElementView.extend({ + // go over all possible directions and find neighbors + for (i = 0; i < numDirections; i++) { + direction = directions[i]; - initialize: function() { - joint.dia.ElementView.prototype.initialize.apply(this, arguments); - // The element view is not automatically rescaled to fit the model size - // when the attribute 'attrs' is changed. - this.listenTo(this.model, 'change:attrs', this.resize); - } -}); + var directionAngle = direction.angle; + directionChange = getDirectionChange(previousDirectionAngle, directionAngle); -joint.shapes.basic.Generic.define('basic.Text', { - attrs: { - 'text': { - 'font-size': 18, - fill: '#000000' - } - } -}, { - markup: '', -}); + // if the direction changed rapidly, don't use this point + // any direction is allowed for starting points + if (!(isPathBeginning && isStart) && directionChange > opt.maxAllowedDirectionChange) continue; -joint.shapes.basic.Generic.define('basic.Circle', { - size: { width: 60, height: 60 }, - attrs: { - 'circle': { - fill: '#ffffff', - stroke: '#000000', - r: 30, - cx: 30, - cy: 30 - }, - 'text': { - 'font-size': 14, - text: '', - 'text-anchor': 'middle', - 'ref-x': .5, - 'ref-y': .5, - 'y-alignment': 'middle', - fill: '#000000', - 'font-family': 'Arial, helvetica, sans-serif' - } - } -}, { - markup: '', -}); + var neighborPoint = currentPoint.clone().offset(direction.gridOffsetX, direction.gridOffsetY); + var neighborKey = getKey(neighborPoint); -joint.shapes.basic.Generic.define('basic.Ellipse', { - size: { width: 60, height: 40 }, - attrs: { - 'ellipse': { - fill: '#ffffff', - stroke: '#000000', - rx: 30, - ry: 20, - cx: 30, - cy: 20 - }, - 'text': { - 'font-size': 14, - text: '', - 'text-anchor': 'middle', - 'ref-x': .5, - 'ref-y': .5, - 'y-alignment': 'middle', - fill: '#000000', - 'font-family': 'Arial, helvetica, sans-serif' - } - } -}, { - markup: '', -}); + // Closed points from the openSet were already evaluated. + if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) continue; -joint.shapes.basic.Generic.define('basic.Polygon', { - size: { width: 60, height: 40 }, - attrs: { - 'polygon': { - fill: '#ffffff', - stroke: '#000000' - }, - 'text': { - 'font-size': 14, - text: '', - 'text-anchor': 'middle', - 'ref-x': .5, - 'ref-dy': 20, - 'y-alignment': 'middle', - fill: '#000000', - 'font-family': 'Arial, helvetica, sans-serif' - } - } -}, { - markup: '', -}); + // We can only enter end points at an acceptable angle. + if (endPointsKeys.indexOf(neighborKey) >= 0) { // neighbor is an end point + round(neighborPoint, opt); // remove rounding errors -joint.shapes.basic.Generic.define('basic.Polyline', { - size: { width: 60, height: 40 }, - attrs: { - 'polyline': { - fill: '#ffffff', - stroke: '#000000' - }, - 'text': { - 'font-size': 14, - text: '', - 'text-anchor': 'middle', - 'ref-x': .5, - 'ref-dy': 20, - 'y-alignment': 'middle', - fill: '#000000', - 'font-family': 'Arial, helvetica, sans-serif' - } - } -}, { - markup: '', -}); + var isNeighborEnd = neighborPoint.equals(end); // (is target anchor or `to` point) = can be entered in any direction -joint.shapes.basic.Generic.define('basic.Image', { - attrs: { - 'text': { - 'font-size': 14, - text: '', - 'text-anchor': 'middle', - 'ref-x': .5, - 'ref-dy': 20, - 'y-alignment': 'middle', - fill: '#000000', - 'font-family': 'Arial, helvetica, sans-serif' - } - } -}, { - markup: '', -}); + if (!isNeighborEnd) { + var endDirectionAngle = getDirectionAngle(neighborPoint, end, numDirections, grid, opt); + var endDirectionChange = getDirectionChange(directionAngle, endDirectionAngle); -joint.shapes.basic.Generic.define('basic.Path', { - size: { width: 60, height: 60 }, - attrs: { - 'path': { - fill: '#ffffff', - stroke: '#000000' - }, - 'text': { - 'font-size': 14, - text: '', - 'text-anchor': 'middle', - 'ref': 'path', - 'ref-x': .5, - 'ref-dy': 10, - fill: '#000000', - 'font-family': 'Arial, helvetica, sans-serif' - } - } + if (endDirectionChange > opt.maxAllowedDirectionChange) continue; + } + } -}, { - markup: '', -}); + // The current direction is ok. -joint.shapes.basic.Path.define('basic.Rhombus', { - attrs: { - 'path': { - d: 'M 30 0 L 60 30 30 60 0 30 z' - }, - 'text': { - 'ref-y': .5, - 'ref-dy': null, - 'y-alignment': 'middle' + var neighborCost = direction.cost; + var neighborPenalty = isStart ? 0 : opt.penalties[directionChange]; // no penalties for start point + var costFromStart = currentCost + neighborCost + neighborPenalty; + + if (!openSet.isOpen(neighborKey) || (costFromStart < costs[neighborKey])) { + // neighbor point has not been processed yet + // or the cost of the path from start is lower than previously calculated + + points[neighborKey] = neighborPoint; + parents[neighborKey] = currentPoint; + costs[neighborKey] = costFromStart; + openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints)); + } + } + + loopsRemaining--; + } } + + // no route found (`to` point either wasn't accessible or finding route took + // way too much calculation) + return opt.fallbackRoute.call(this, start, end, opt); } -}); + // resolve some of the options + function resolveOptions(opt) { -/** - * @deprecated use the port api instead - */ -// PortsModelInterface is a common interface for shapes that have ports. This interface makes it easy -// to create new shapes with ports functionality. It is assumed that the new shapes have -// `inPorts` and `outPorts` array properties. Only these properties should be used to set ports. -// In other words, using this interface, it is no longer recommended to set ports directly through the -// `attrs` object. + opt.directions = util.result(opt, 'directions'); + opt.penalties = util.result(opt, 'penalties'); + opt.paddingBox = util.result(opt, 'paddingBox'); -// Usage: -// joint.shapes.custom.MyElementWithPorts = joint.shapes.basic.Path.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, { -// getPortAttrs: function(portName, index, total, selector, type) { -// var attrs = {}; -// var portClass = 'port' + index; -// var portSelector = selector + '>.' + portClass; -// var portTextSelector = portSelector + '>text'; -// var portBodySelector = portSelector + '>.port-body'; -// -// attrs[portTextSelector] = { text: portName }; -// attrs[portBodySelector] = { port: { id: portName || _.uniqueId(type) , type: type } }; -// attrs[portSelector] = { ref: 'rect', 'ref-y': (index + 0.5) * (1 / total) }; -// -// if (selector === '.outPorts') { attrs[portSelector]['ref-dx'] = 0; } -// -// return attrs; -// } -//})); -joint.shapes.basic.PortsModelInterface = { + util.toArray(opt.directions).forEach(function(direction) { - initialize: function() { + var point1 = new g.Point(0, 0); + var point2 = new g.Point(direction.offsetX, direction.offsetY); - this.updatePortsAttrs(); - this.on('change:inPorts change:outPorts', this.updatePortsAttrs, this); + direction.angle = g.normalizeAngle(point1.theta(point2)); + }); + } - // Call the `initialize()` of the parent. - this.constructor.__super__.constructor.__super__.initialize.apply(this, arguments); - }, + // initialization of the route finding + function router(vertices, opt, linkView) { - updatePortsAttrs: function(eventName) { + resolveOptions(opt); - if (this._portSelectors) { + // enable/disable linkView perpendicular option + linkView.options.perpendicular = !!opt.perpendicular; - var newAttrs = joint.util.omit(this.get('attrs'), this._portSelectors); - this.set('attrs', newAttrs, { silent: true }); - } + var sourceBBox = getSourceBBox(linkView, opt); + var targetBBox = getTargetBBox(linkView, opt); - // This holds keys to the `attrs` object for all the port specific attribute that - // we set in this method. This is necessary in order to remove previously set - // attributes for previous ports. - this._portSelectors = []; + var sourceAnchor = getSourceAnchor(linkView, opt); + //var targetAnchor = getTargetAnchor(linkView, opt); - var attrs = {}; + // pathfinding + var map = (new ObstacleMap(opt)).build(linkView.paper.model, linkView.model); + var oldVertices = util.toArray(vertices).map(g.Point); + var newVertices = []; + var tailPoint = sourceAnchor; // the origin of first route's grid, does not need snapping - joint.util.toArray(this.get('inPorts')).forEach(function(portName, index, ports) { - var portAttributes = this.getPortAttrs(portName, index, ports.length, '.inPorts', 'in'); - this._portSelectors = this._portSelectors.concat(Object.keys(portAttributes)); - joint.util.assign(attrs, portAttributes); - }, this); + // find a route by concatenating all partial routes (routes need to pass through vertices) + // source -> vertex[1] -> ... -> vertex[n] -> target + for (var i = 0, len = oldVertices.length; i <= len; i++) { - joint.util.toArray(('outPorts')).forEach(function(portName, index, ports) { - var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out'); - this._portSelectors = this._portSelectors.concat(Object.keys(portAttributes)); - joint.util.assign(attrs, portAttributes); - }, this); + var partialRoute = null; - // Silently set `attrs` on the cell so that noone knows the attrs have changed. This makes sure - // that, for example, command manager does not register `change:attrs` command but only - // the important `change:inPorts`/`change:outPorts` command. - this.attr(attrs, { silent: true }); - // Manually call the `processPorts()` method that is normally called on `change:attrs` (that we just made silent). - this.processPorts(); - // Let the outside world (mainly the `ModelView`) know that we're done configuring the `attrs` object. - this.trigger('process:ports'); - }, + var from = to || sourceBBox; + var to = oldVertices[i]; - getPortSelector: function(name) { + if (!to) { + // this is the last iteration + // we ran through all vertices in oldVertices + // 'to' is not a vertex. - var selector = '.inPorts'; - var index = this.get('inPorts').indexOf(name); + to = targetBBox; - if (index < 0) { - selector = '.outPorts'; - index = this.get('outPorts').indexOf(name); + // If the target is a point (i.e. it's not an element), we + // should use dragging route instead of main routing method if it has been provided. + var isEndingAtPoint = !linkView.model.get('source').id || !linkView.model.get('target').id; - if (index < 0) throw new Error("getPortSelector(): Port doesn't exist."); - } + if (isEndingAtPoint && util.isFunction(opt.draggingRoute)) { + // Make sure we are passing points only (not rects). + var dragFrom = (from === sourceBBox) ? sourceAnchor : from; + var dragTo = to.origin(); - return selector + '>g:nth-child(' + (index + 1) + ')>.port-body'; - } -}; + partialRoute = opt.draggingRoute.call(linkView, dragFrom, dragTo, opt); + } + } -joint.shapes.basic.PortsViewInterface = { + // if partial route has not been calculated yet use the main routing method to find one + partialRoute = partialRoute || findRoute.call(linkView, from, to, map, opt); - initialize: function() { + if (partialRoute === null) { // the partial route cannot be found + return opt.fallbackRouter(vertices, opt, linkView); + } - // `Model` emits the `process:ports` whenever it's done configuring the `attrs` object for ports. - this.listenTo(this.model, 'process:ports', this.update); + var leadPoint = partialRoute[0]; - joint.dia.ElementView.prototype.initialize.apply(this, arguments); - }, + // remove the first point if the previous partial route had the same point as last + if (leadPoint && leadPoint.equals(tailPoint)) partialRoute.shift(); - update: function() { + // save tailPoint for next iteration + tailPoint = partialRoute[partialRoute.length - 1] || tailPoint; - // First render ports so that `attrs` can be applied to those newly created DOM elements - // in `ElementView.prototype.update()`. - this.renderPorts(); - joint.dia.ElementView.prototype.update.apply(this, arguments); - }, + Array.prototype.push.apply(newVertices, partialRoute); + } - renderPorts: function() { + return newVertices; + } - var $inPorts = this.$('.inPorts').empty(); - var $outPorts = this.$('.outPorts').empty(); + // public function + return function(vertices, opt, linkView) { - var portTemplate = joint.util.template(this.model.portMarkup); + return router(vertices, util.assign({}, config, opt), linkView); + }; - var ports = this.model.ports || []; - ports.filter(function(p) { - return p.type === 'in'; - }).forEach(function(port, index) { +})(g, _, joint, joint.util); - $inPorts.append(V(portTemplate({ id: index, port: port })).node); - }); +joint.routers.metro = (function(util) { - ports.filter(function(p) { - return p.type === 'out'; - }).forEach(function(port, index) { + var config = { - $outPorts.append(V(portTemplate({ id: index, port: port })).node); - }); - } -}; + maxAllowedDirectionChange: 45, -joint.shapes.basic.Generic.define('basic.TextBlock', { - // see joint.css for more element styles - attrs: { - rect: { - fill: '#ffffff', - stroke: '#000000', - width: 80, - height: 100 + // cost of a diagonal step + diagonalCost: function() { + + var step = this.step; + return Math.ceil(Math.sqrt(step * step << 1)); }, - text: { - fill: '#000000', - 'font-size': 14, - 'font-family': 'Arial, helvetica, sans-serif' + + // an array of directions to find next points on the route + // different from start/end directions + directions: function() { + + var step = this.step; + var cost = this.cost(); + var diagonalCost = this.diagonalCost(); + + return [ + { offsetX: step , offsetY: 0 , cost: cost }, + { offsetX: step , offsetY: step , cost: diagonalCost }, + { offsetX: 0 , offsetY: step , cost: cost }, + { offsetX: -step , offsetY: step , cost: diagonalCost }, + { offsetX: -step , offsetY: 0 , cost: cost }, + { offsetX: -step , offsetY: -step , cost: diagonalCost }, + { offsetX: 0 , offsetY: -step , cost: cost }, + { offsetX: step , offsetY: -step , cost: diagonalCost } + ]; }, - '.content': { - text: '', - 'ref-x': .5, - 'ref-y': .5, - 'y-alignment': 'middle', - 'x-alignment': 'middle' - } - }, - content: '' -}, { - markup: [ - '', - '', - joint.env.test('svgforeignobject') ? '
' : '', - '' - ].join(''), + // a simple route used in situations when main routing method fails + // (exceed max number of loop iterations, inaccessible) + fallbackRoute: function(from, to, opt) { - initialize: function() { + // Find a route which breaks by 45 degrees ignoring all obstacles. - this.listenTo(this, 'change:size', this.updateSize); - this.listenTo(this, 'change:content', this.updateContent); - this.updateSize(this, this.get('size')); - this.updateContent(this, this.get('content')); - joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments); - }, + var theta = from.theta(to); - updateSize: function(cell, size) { + var route = []; - // Selector `foreignObject' doesn't work accross all browsers, we'r using class selector instead. - // We have to clone size as we don't want attributes.div.style to be same object as attributes.size. - this.attr({ - '.fobj': joint.util.assign({}, size), - div: { - style: joint.util.assign({}, size) + var a = { x: to.x, y: from.y }; + var b = { x: from.x, y: to.y }; + + if (theta % 180 > 90) { + var t = a; + a = b; + b = t; } - }); - }, - updateContent: function(cell, content) { + var p1 = (theta % 90) < 45 ? a : b; + var l1 = new g.Line(from, p1); - if (joint.env.test('svgforeignobject')) { + var alpha = 90 * Math.ceil(theta / 90); - // Content element is a
element. - this.attr({ - '.content': { - html: content - } - }); + var p2 = g.Point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); + var l2 = new g.Line(to, p2); - } else { + var intersectionPoint = l1.intersection(l2); + var point = intersectionPoint ? intersectionPoint : to; - // Content element is a element. - // SVG elements don't have innerHTML attribute. - this.attr({ - '.content': { - text: content - } - }); + var directionFrom = intersectionPoint ? point : from; + + var quadrant = 360 / opt.directions.length; + var angleTheta = directionFrom.theta(to); + var normalizedAngle = g.normalizeAngle(angleTheta + (quadrant / 2)); + var directionAngle = quadrant * Math.floor(normalizedAngle / quadrant); + + opt.previousDirectionAngle = directionAngle; + + if (point) route.push(point.round()); + route.push(to); + + return route; } - }, + }; + + // public function + return function(vertices, opt, linkView) { + + if (!util.isFunction(joint.routers.manhattan)) { + throw new Error('Metro requires the manhattan router.'); + } + + return joint.routers.manhattan(vertices, util.assign({}, config, opt), linkView); + }; + +})(joint.util); + +// Does not make any changes to vertices. +// Returns the arguments that are passed to it, unchanged. +joint.routers.normal = function(vertices, opt, linkView) { + + return vertices; +}; + +// Routes the link always to/from a certain side +// +// Arguments: +// padding ... gap between the element and the first vertex. :: Default 40. +// side ... 'left' | 'right' | 'top' | 'bottom' :: Default 'bottom'. +// +joint.routers.oneSide = function(vertices, opt, linkView) { - // Here for backwards compatibility: - setForeignObjectSize: function() { + var side = opt.side || 'bottom'; + var padding = opt.padding || 40; - this.updateSize.apply(this, arguments); - }, + // LinkView contains cached source an target bboxes. + // Note that those are Geometry rectangle objects. + var sourceBBox = linkView.sourceBBox; + var targetBBox = linkView.targetBBox; + var sourcePoint = sourceBBox.center(); + var targetPoint = targetBBox.center(); - // Here for backwards compatibility: - setDivContent: function() { + var coordinate, dimension, direction; - this.updateContent.apply(this, arguments); + switch (side) { + case 'bottom': + direction = 1; + coordinate = 'y'; + dimension = 'height'; + break; + case 'top': + direction = -1; + coordinate = 'y'; + dimension = 'height'; + break; + case 'left': + direction = -1; + coordinate = 'x'; + dimension = 'width'; + break; + case 'right': + direction = 1; + coordinate = 'x'; + dimension = 'width'; + break; + default: + throw new Error('Router: invalid side'); } -}); -// TextBlockView implements the fallback for IE when no foreignObject exists and -// the text needs to be manually broken. -joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({ + // move the points from the center of the element to outside of it. + sourcePoint[coordinate] += direction * (sourceBBox[dimension] / 2 + padding); + targetPoint[coordinate] += direction * (targetBBox[dimension] / 2 + padding); - initialize: function() { + // make link orthogonal (at least the first and last vertex). + if (direction * (sourcePoint[coordinate] - targetPoint[coordinate]) > 0) { + targetPoint[coordinate] = sourcePoint[coordinate]; + } else { + sourcePoint[coordinate] = targetPoint[coordinate]; + } - joint.dia.ElementView.prototype.initialize.apply(this, arguments); + return [sourcePoint].concat(vertices, targetPoint); +}; - // Keep this for backwards compatibility: - this.noSVGForeignObjectElement = !joint.env.test('svgforeignobject'); +joint.routers.orthogonal = (function(util) { - if (!joint.env.test('svgforeignobject')) { + // bearing -> opposite bearing + var opposites = { + N: 'S', + S: 'N', + E: 'W', + W: 'E' + }; - this.listenTo(this.model, 'change:content change:size', function(cell) { - // avoiding pass of extra paramters - this.updateContent(cell); - }); - } - }, + // bearing -> radians + var radians = { + N: -Math.PI / 2 * 3, + S: -Math.PI / 2, + E: 0, + W: Math.PI + }; - update: function(cell, renderingOnlyAttrs) { + // HELPERS // - var model = this.model; + // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained + // in the given box + function freeJoin(p1, p2, bbox) { - if (!joint.env.test('svgforeignobject')) { + var p = new g.Point(p1.x, p2.y); + if (bbox.containsPoint(p)) p = new g.Point(p2.x, p1.y); + // kept for reference + // if (bbox.containsPoint(p)) p = null; - // Update everything but the content first. - var noTextAttrs = joint.util.omit(renderingOnlyAttrs || model.get('attrs'), '.content'); - joint.dia.ElementView.prototype.update.call(this, model, noTextAttrs); + return p; + } - if (!renderingOnlyAttrs || joint.util.has(renderingOnlyAttrs, '.content')) { - // Update the content itself. - this.updateContent(model, renderingOnlyAttrs); - } + // returns either width or height of a bbox based on the given bearing + function getBBoxSize(bbox, bearing) { - } else { + return bbox[(bearing === 'W' || bearing === 'E') ? 'width' : 'height']; + } - joint.dia.ElementView.prototype.update.call(this, model, renderingOnlyAttrs); - } - }, + // simple bearing method (calculates only orthogonal cardinals) + function getBearing(from, to) { - updateContent: function(cell, renderingOnlyAttrs) { + if (from.x === to.x) return (from.y > to.y) ? 'N' : 'S'; + if (from.y === to.y) return (from.x > to.x) ? 'W' : 'E'; + return null; + } - // Create copy of the text attributes - var textAttrs = joint.util.merge({}, (renderingOnlyAttrs || cell.get('attrs'))['.content']); + // transform point to a rect + function getPointBox(p) { - textAttrs = joint.util.omit(textAttrs, 'text'); + return new g.Rect(p.x, p.y, 0, 0); + } - // Break the content to fit the element size taking into account the attributes - // set on the model. - var text = joint.util.breakText(cell.get('content'), cell.get('size'), textAttrs, { - // measuring sandbox svg document - svgDocument: this.paper.svg - }); + // return source bbox + function getSourceBBox(linkView, opt) { - // Create a new attrs with same structure as the model attrs { text: { *textAttributes* }} - var attrs = joint.util.setByPath({}, '.content', textAttrs, '/'); + var padding = (opt && opt.elementPadding) || 20; + return linkView.sourceBBox.clone().inflate(padding); + } - // Replace text attribute with the one we just processed. - attrs['.content'].text = text; + // return target bbox + function getTargetBBox(linkView, opt) { - // Update the view using renderingOnlyAttributes parameter. - joint.dia.ElementView.prototype.update.call(this, cell, attrs); + var padding = (opt && opt.elementPadding) || 20; + return linkView.targetBBox.clone().inflate(padding); } -}); -joint.routers.manhattan = (function(g, _, joint, util) { + // return source anchor + function getSourceAnchor(linkView, opt) { - 'use strict'; + if (linkView.sourceAnchor) return linkView.sourceAnchor; - var config = { + // fallback: center of bbox + var sourceBBox = getSourceBBox(linkView, opt); + return sourceBBox.center(); + } - // size of the step to find a route - step: 10, + // return target anchor + function getTargetAnchor(linkView, opt) { - // use of the perpendicular linkView option to connect center of element with first vertex - perpendicular: true, + if (linkView.targetAnchor) return linkView.targetAnchor; - // should be source or target not to be consider as an obstacle - excludeEnds: [], // 'source', 'target' + // fallback: center of bbox + var targetBBox = getTargetBBox(linkView, opt); + return targetBBox.center(); // default + } - // should be any element with a certain type not to be consider as an obstacle - excludeTypes: ['basic.Text'], + // PARTIAL ROUTERS // - // if number of route finding loops exceed the maximum, stops searching and returns - // fallback route - maximumLoops: 2000, + function vertexVertex(from, to, bearing) { - // possible starting directions from an element - startDirections: ['left', 'right', 'top', 'bottom'], + var p1 = new g.Point(from.x, to.y); + var p2 = new g.Point(to.x, from.y); + var d1 = getBearing(from, p1); + var d2 = getBearing(from, p2); + var opposite = opposites[bearing]; - // possible ending directions to an element - endDirections: ['left', 'right', 'top', 'bottom'], + var p = (d1 === bearing || (d1 !== opposite && (d2 === opposite || d2 !== bearing))) ? p1 : p2; - // specify directions above - directionMap: { - right: { x: 1, y: 0 }, - bottom: { x: 0, y: 1 }, - left: { x: -1, y: 0 }, - top: { x: 0, y: -1 } - }, + return { points: [p], direction: getBearing(p, to) }; + } - // maximum change of the direction - maxAllowedDirectionChange: 90, + function elementVertex(from, to, fromBBox) { - // padding applied on the element bounding boxes - paddingBox: function() { + var p = freeJoin(from, to, fromBBox); - var step = this.step; + return { points: [p], direction: getBearing(p, to) }; + } - return { - x: -step, - y: -step, - width: 2 * step, - height: 2 * step - }; - }, + function vertexElement(from, to, toBBox, bearing) { - // an array of directions to find next points on the route - directions: function() { + var route = {}; - var step = this.step; + var points = [new g.Point(from.x, to.y), new g.Point(to.x, from.y)]; + var freePoints = points.filter(function(pt) { return !toBBox.containsPoint(pt); }); + var freeBearingPoints = freePoints.filter(function(pt) { return getBearing(pt, from) !== bearing; }); - return [ - { offsetX: step , offsetY: 0 , cost: step }, - { offsetX: 0 , offsetY: step , cost: step }, - { offsetX: -step , offsetY: 0 , cost: step }, - { offsetX: 0 , offsetY: -step , cost: step } - ]; - }, + var p; - // a penalty received for direction change - penalties: function() { + if (freeBearingPoints.length > 0) { + // Try to pick a point which bears the same direction as the previous segment. - return { - 0: 0, - 45: this.step / 2, - 90: this.step / 2 - }; - }, + p = freeBearingPoints.filter(function(pt) { return getBearing(from, pt) === bearing; }).pop(); + p = p || freeBearingPoints[0]; - // * Deprecated * - // a simple route used in situations, when main routing method fails - // (exceed loops, inaccessible). - /* i.e. - function(from, to, opts) { - // Find an orthogonal route ignoring obstacles. - var point = ((opts.previousDirAngle || 0) % 180 === 0) - ? g.point(from.x, to.y) - : g.point(to.x, from.y); - return [point, to]; - }, - */ - fallbackRoute: function() { - return null; - }, + route.points = [p]; + route.direction = getBearing(p, to); - // if a function is provided, it's used to route the link while dragging an end - // i.e. function(from, to, opts) { return []; } - draggingRoute: null - }; + } else { + // Here we found only points which are either contained in the element or they would create + // a link segment going in opposite direction from the previous one. + // We take the point inside element and move it outside the element in the direction the + // route is going. Now we can join this point with the current end (using freeJoin). - // Map of obstacles - // Helper structure to identify whether a point lies in an obstacle. - function ObstacleMap(opt) { + p = util.difference(points, freePoints)[0]; - this.map = {}; - this.options = opt; - // tells how to divide the paper when creating the elements map - this.mapGridSize = 100; - } + var p2 = (new g.Point(to)).move(p, -getBBoxSize(toBBox, bearing) / 2); + var p1 = freeJoin(p2, from, toBBox); - ObstacleMap.prototype.build = function(graph, link) { + route.points = [p1, p2]; + route.direction = getBearing(p2, to); + } - var opt = this.options; + return route; + } + function elementElement(from, to, fromBBox, toBBox) { - // source or target element could be excluded from set of obstacles - var excludedEnds = util.toArray(opt.excludeEnds).reduce(function(res, item) { - var end = link.get(item); - if (end) { - var cell = graph.getCell(end.id); - if (cell) { - res.push(cell); - } - } - return res; - }, []); + var route = elementVertex(to, from, toBBox); + var p1 = route.points[0]; - // Exclude any embedded elements from the source and the target element. - var excludedAncestors = []; + if (fromBBox.containsPoint(p1)) { - var source = graph.getCell(link.get('source').id); - if (source) { - excludedAncestors = util.union(excludedAncestors, source.getAncestors().map(function(cell) { return cell.id })); - } + route = elementVertex(from, to, fromBBox); + var p2 = route.points[0]; - var target = graph.getCell(link.get('target').id); - if (target) { - excludedAncestors = util.union(excludedAncestors, target.getAncestors().map(function(cell) { return cell.id })); - } + if (toBBox.containsPoint(p2)) { - // builds a map of all elements for quicker obstacle queries (i.e. is a point contained - // in any obstacle?) (a simplified grid search) - // The paper is divided to smaller cells, where each of them holds an information which - // elements belong to it. When we query whether a point is in an obstacle we don't need - // to go through all obstacles, we check only those in a particular cell. - var mapGridSize = this.mapGridSize; + var fromBorder = (new g.Point(from)).move(p2, -getBBoxSize(fromBBox, getBearing(from, p2)) / 2); + var toBorder = (new g.Point(to)).move(p1, -getBBoxSize(toBBox, getBearing(to, p1)) / 2); + var mid = (new g.Line(fromBorder, toBorder)).midpoint(); - graph.getElements().reduce(function(map, element) { + var startRoute = elementVertex(from, mid, fromBBox); + var endRoute = vertexVertex(mid, to, startRoute.direction); - var isExcludedType = util.toArray(opt.excludeTypes).includes(element.get('type')); - var isExcludedEnd = excludedEnds.find(function(excluded) { return excluded.id === element.id }); - var isExcludedAncestor = excludedAncestors.includes(element.id); + route.points = [startRoute.points[0], endRoute.points[0]]; + route.direction = endRoute.direction; + } + } - var isExcluded = isExcludedType || isExcludedEnd || isExcludedAncestor; - if (!isExcluded) { - var bBox = element.getBBox().moveAndExpand(opt.paddingBox); + return route; + } - var origin = bBox.origin().snapToGrid(mapGridSize); - var corner = bBox.corner().snapToGrid(mapGridSize); + // Finds route for situations where one element is inside the other. + // Typically the route is directed outside the outer element first and + // then back towards the inner element. + function insideElement(from, to, fromBBox, toBBox, bearing) { - for (var x = origin.x; x <= corner.x; x += mapGridSize) { - for (var y = origin.y; y <= corner.y; y += mapGridSize) { - var gridKey = x + '@' + y; - map[gridKey] = map[gridKey] || []; - map[gridKey].push(bBox); - } - } - } - return map; - }, this.map); + var route = {}; + var boundary = fromBBox.union(toBBox).inflate(1); - return this; - }; + // start from the point which is closer to the boundary + var reversed = boundary.center().distance(to) > boundary.center().distance(from); + var start = reversed ? to : from; + var end = reversed ? from : to; - ObstacleMap.prototype.isPointAccessible = function(point) { + var p1, p2, p3; - var mapKey = point.clone().snapToGrid(this.mapGridSize).toString(); + if (bearing) { + // Points on circle with radius equals 'W + H` are always outside the rectangle + // with width W and height H if the center of that circle is the center of that rectangle. + p1 = g.Point.fromPolar(boundary.width + boundary.height, radians[bearing], start); + p1 = boundary.pointNearestToPoint(p1).move(p1, -1); - return util.toArray(this.map[mapKey]).every( function(obstacle) { - return !obstacle.containsPoint(point); - }); - }; + } else { + p1 = boundary.pointNearestToPoint(start).move(start, 1); + } - // Sorted Set - // Set of items sorted by given value. - function SortedSet() { - this.items = []; - this.hash = {}; - this.values = {}; - this.OPEN = 1; - this.CLOSE = 2; - } + p2 = freeJoin(p1, end, boundary); - SortedSet.prototype.add = function(item, value) { + if (p1.round().equals(p2.round())) { + p2 = g.Point.fromPolar(boundary.width + boundary.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); + p2 = boundary.pointNearestToPoint(p2).move(end, 1).round(); + p3 = freeJoin(p1, p2, boundary); + route.points = reversed ? [p2, p3, p1] : [p1, p3, p2]; - if (this.hash[item]) { - // item removal - this.items.splice(this.items.indexOf(item), 1); } else { - this.hash[item] = this.OPEN; + route.points = reversed ? [p2, p1] : [p1, p2]; } - this.values[item] = value; - - var index = joint.util.sortedIndex(this.items, item, function(i) { - return this.values[i]; - }.bind(this)); + route.direction = reversed ? getBearing(p1, to) : getBearing(p2, to); - this.items.splice(index, 0, item); - }; + return route; + } - SortedSet.prototype.remove = function(item) { - this.hash[item] = this.CLOSE; - }; + // MAIN ROUTER // - SortedSet.prototype.isOpen = function(item) { - return this.hash[item] === this.OPEN; - }; + // Return points through which a connection needs to be drawn in order to obtain an orthogonal link + // routing from source to target going through `vertices`. + function router(vertices, opt, linkView) { - SortedSet.prototype.isClose = function(item) { - return this.hash[item] === this.CLOSE; - }; + var padding = opt.elementPadding || 20; - SortedSet.prototype.isEmpty = function() { - return this.items.length === 0; - }; + var sourceBBox = getSourceBBox(linkView, opt); + var targetBBox = getTargetBBox(linkView, opt); - SortedSet.prototype.pop = function() { - var item = this.items.shift(); - this.remove(item); - return item; - }; + var sourceAnchor = getSourceAnchor(linkView, opt); + var targetAnchor = getTargetAnchor(linkView, opt); - function normalizePoint(point) { - return g.point( - point.x === 0 ? 0 : Math.abs(point.x) / point.x, - point.y === 0 ? 0 : Math.abs(point.y) / point.y - ); - } + // if anchor lies outside of bbox, the bbox expands to include it + sourceBBox = sourceBBox.union(getPointBox(sourceAnchor)); + targetBBox = targetBBox.union(getPointBox(targetAnchor)); - // reconstructs a route by concating points with their parents - function reconstructRoute(parents, point, startCenter, endCenter) { + vertices = util.toArray(vertices).map(g.Point); + vertices.unshift(sourceAnchor); + vertices.push(targetAnchor); - var route = []; - var prevDiff = normalizePoint(endCenter.difference(point)); - var current = point; - var parent; + var bearing; // bearing of previous route segment - while ((parent = parents[current])) { + var orthogonalVertices = []; // the array of found orthogonal vertices to be returned + for (var i = 0, max = vertices.length - 1; i < max; i++) { - var diff = normalizePoint(current.difference(parent)); + var route = null; - if (!diff.equals(prevDiff)) { + var from = vertices[i]; + var to = vertices[i + 1]; - route.unshift(current); - prevDiff = diff; - } + var isOrthogonal = !!getBearing(from, to); - current = parent; - } + if (i === 0) { // source - var startDiff = normalizePoint(g.point(current).difference(startCenter)); - if (!startDiff.equals(prevDiff)) { - route.unshift(current); - } + if (i + 1 === max) { // route source -> target - return route; - } + // Expand one of the elements by 1px to detect situations when the two + // elements are positioned next to each other with no gap in between. + if (sourceBBox.intersect(targetBBox.clone().inflate(1))) { + route = insideElement(from, to, sourceBBox, targetBBox); - // find points around the rectangle taking given directions in the account - function getRectPoints(bbox, directionList, opt) { + } else if (!isOrthogonal) { + route = elementElement(from, to, sourceBBox, targetBBox); + } - var step = opt.step; - var center = bbox.center(); - var keys = util.isObject(opt.directionMap) ? Object.keys(opt.directionMap) : []; - var dirLis = util.toArray(directionList); - return keys.reduce(function(res, key) { + } else { // route source -> vertex - if (dirLis.includes(key)) { + if (sourceBBox.containsPoint(to)) { + route = insideElement(from, to, sourceBBox, getPointBox(to).inflate(padding)); - var direction = opt.directionMap[key]; + } else if (!isOrthogonal) { + route = elementVertex(from, to, sourceBBox); + } + } - var x = direction.x * bbox.width / 2; - var y = direction.y * bbox.height / 2; + } else if (i + 1 === max) { // route vertex -> target - var point = center.clone().offset(x, y); + // prevent overlaps with previous line segment + var isOrthogonalLoop = isOrthogonal && getBearing(to, from) === bearing; - if (bbox.containsPoint(point)) { + if (targetBBox.containsPoint(from) || isOrthogonalLoop) { + route = insideElement(from, to, getPointBox(from).inflate(padding), targetBBox, bearing); - point.offset(direction.x * step, direction.y * step); + } else if (!isOrthogonal) { + route = vertexElement(from, to, targetBBox, bearing); } - res.push(point.snapToGrid(step)); + } else if (!isOrthogonal) { // route vertex -> vertex + route = vertexVertex(from, to, bearing); } - return res; - }, []); - } + // applicable to all routes: - // returns a direction index from start point to end point - function getDirectionAngle(start, end, dirLen) { + // set bearing for next iteration + if (route) { + Array.prototype.push.apply(orthogonalVertices, route.points); + bearing = route.direction; - var q = 360 / dirLen; - return Math.floor(g.normalizeAngle(start.theta(end) + q / 2) / q) * q; - } + } else { + // orthogonal route and not looped + bearing = getBearing(from, to); + } - function getDirectionChange(angle1, angle2) { + // push `to` point to identified orthogonal vertices array + if (i + 1 < max) { + orthogonalVertices.push(to); + } + } - var dirChange = Math.abs(angle1 - angle2); - return dirChange > 180 ? 360 - dirChange : dirChange; + return orthogonalVertices; } - // heurestic method to determine the distance between two points - function estimateCost(from, endPoints) { + return router; - var min = Infinity; +})(joint.util); - for (var i = 0, len = endPoints.length; i < len; i++) { - var cost = from.manhattanDistance(endPoints[i]); - if (cost < min) min = cost; - } +joint.connectors.normal = function(sourcePoint, targetPoint, route, opt) { - return min; - } + var raw = opt && opt.raw; + var points = [sourcePoint].concat(route).concat([targetPoint]); - // finds the route between to points/rectangles implementing A* alghoritm - function findRoute(start, end, map, opt) { + var polyline = new g.Polyline(points); + var path = new g.Path(polyline); - var step = opt.step; - var startPoints, endPoints; - var startCenter, endCenter; + return (raw) ? path : path.serialize(); +}; - // set of points we start pathfinding from - if (start instanceof g.rect) { - startPoints = getRectPoints(start, opt.startDirections, opt); - startCenter = start.center().snapToGrid(step); - } else { - startCenter = start.clone().snapToGrid(step); - startPoints = [startCenter]; - } +joint.connectors.rounded = function(sourcePoint, targetPoint, route, opt) { - // set of points we want the pathfinding to finish at - if (end instanceof g.rect) { - endPoints = getRectPoints(end, opt.endDirections, opt); - endCenter = end.center().snapToGrid(step); - } else { - endCenter = end.clone().snapToGrid(step); - endPoints = [endCenter]; - } + opt || (opt = {}); - // take into account only accessible end points - startPoints = startPoints.filter(map.isPointAccessible, map); - endPoints = endPoints.filter(map.isPointAccessible, map); + var offset = opt.radius || 10; + var raw = opt.raw; + var path = new g.Path(); + var segment; - // Check if there is a accessible end point. - // We would have to use a fallback route otherwise. - if (startPoints.length > 0 && endPoints.length > 0) { + segment = g.Path.createSegment('M', sourcePoint); + path.appendSegment(segment); - // The set of tentative points to be evaluated, initially containing the start points. - var openSet = new SortedSet(); - // Keeps reference to a point that is immediate predecessor of given element. - var parents = {}; - // Cost from start to a point along best known path. - var costs = {}; + var _13 = 1 / 3; + var _23 = 2 / 3; - for (var i = 0, n = startPoints.length; i < n; i++) { - var point = startPoints[i]; + var curr; + var prev, next; + var prevDistance, nextDistance; + var startMove, endMove; + var roundedStart, roundedEnd; + var control1, control2; - var key = point.toString(); - openSet.add(key, estimateCost(point, endPoints)); - costs[key] = 0; - } + for (var index = 0, n = route.length; index < n; index++) { - // directions - var dir, dirChange; - var dirs = opt.directions; - var dirLen = dirs.length; - var loopsRemain = opt.maximumLoops; - var endPointsKeys = util.invoke(endPoints, 'toString'); + curr = new g.Point(route[index]); - // main route finding loop - while (!openSet.isEmpty() && loopsRemain > 0) { + prev = route[index - 1] || sourcePoint; + next = route[index + 1] || targetPoint; - // remove current from the open list - var currentKey = openSet.pop(); - var currentPoint = g.point(currentKey); - var currentDist = costs[currentKey]; - var previousDirAngle = currentDirAngle; - var currentDirAngle = parents[currentKey] - ? getDirectionAngle(parents[currentKey], currentPoint, dirLen) - : opt.previousDirAngle != null ? opt.previousDirAngle : getDirectionAngle(startCenter, currentPoint, dirLen); - - // Check if we reached any endpoint - if (endPointsKeys.indexOf(currentKey) >= 0) { - // We don't want to allow route to enter the end point in opposite direction. - dirChange = getDirectionChange(currentDirAngle, getDirectionAngle(currentPoint, endCenter, dirLen)); - if (currentPoint.equals(endCenter) || dirChange < 180) { - opt.previousDirAngle = currentDirAngle; - return reconstructRoute(parents, currentPoint, startCenter, endCenter); - } - } + prevDistance = nextDistance || (curr.distance(prev) / 2); + nextDistance = curr.distance(next) / 2; - // Go over all possible directions and find neighbors. - for (i = 0; i < dirLen; i++) { + startMove = -Math.min(offset, prevDistance); + endMove = -Math.min(offset, nextDistance); - dir = dirs[i]; - dirChange = getDirectionChange(currentDirAngle, dir.angle); - // if the direction changed rapidly don't use this point - // Note that check is relevant only for points with previousDirAngle i.e. - // any direction is allowed for starting points - if (previousDirAngle && dirChange > opt.maxAllowedDirectionChange) { - continue; - } + roundedStart = curr.clone().move(prev, startMove).round(); + roundedEnd = curr.clone().move(next, endMove).round(); - var neighborPoint = currentPoint.clone().offset(dir.offsetX, dir.offsetY); - var neighborKey = neighborPoint.toString(); - // Closed points from the openSet were already evaluated. - if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) { - continue; - } + control1 = new g.Point((_13 * roundedStart.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedStart.y)); + control2 = new g.Point((_13 * roundedEnd.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedEnd.y)); - // The current direction is ok to proccess. - var costFromStart = currentDist + dir.cost + opt.penalties[dirChange]; + segment = g.Path.createSegment('L', roundedStart); + path.appendSegment(segment); - if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) { - // neighbor point has not been processed yet or the cost of the path - // from start is lesser than previously calcluated. - parents[neighborKey] = currentPoint; - costs[neighborKey] = costFromStart; - openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints)); - } - } + segment = g.Path.createSegment('C', control1, control2, roundedEnd); + path.appendSegment(segment); + } - loopsRemain--; - } - } + segment = g.Path.createSegment('L', targetPoint); + path.appendSegment(segment); - // no route found ('to' point wasn't either accessible or finding route took - // way to much calculations) - return opt.fallbackRoute(startCenter, endCenter, opt); - } + return (raw) ? path : path.serialize(); +}; - // resolve some of the options - function resolveOptions(opt) { +joint.connectors.smooth = function(sourcePoint, targetPoint, route, opt) { - opt.directions = util.result(opt, 'directions'); - opt.penalties = util.result(opt, 'penalties'); - opt.paddingBox = util.result(opt, 'paddingBox'); + var raw = opt && opt.raw; + var path; - util.toArray(opt.directions).forEach(function(direction) { + if (route && route.length !== 0) { - var point1 = g.point(0, 0); - var point2 = g.point(direction.offsetX, direction.offsetY); + var points = [sourcePoint].concat(route).concat([targetPoint]); + var curves = g.Curve.throughPoints(points); - direction.angle = g.normalizeAngle(point1.theta(point2)); - }); - } + path = new g.Path(curves); - // initiation of the route finding - function router(vertices, opt) { + } else { + // if we have no route, use a default cubic bezier curve + // cubic bezier requires two control points + // the control points have `x` midway between source and target + // this produces an S-like curve - resolveOptions(opt); + path = new g.Path(); - // enable/disable linkView perpendicular option - this.options.perpendicular = !!opt.perpendicular; + var segment; - // expand boxes by specific padding - var sourceBBox = g.rect(this.sourceBBox).moveAndExpand(opt.paddingBox); - var targetBBox = g.rect(this.targetBBox).moveAndExpand(opt.paddingBox); + segment = g.Path.createSegment('M', sourcePoint); + path.appendSegment(segment); - // pathfinding - var map = (new ObstacleMap(opt)).build(this.paper.model, this.model); - var oldVertices = util.toArray(vertices).map(g.point); - var newVertices = []; - var tailPoint = sourceBBox.center().snapToGrid(opt.step); + if ((Math.abs(sourcePoint.x - targetPoint.x)) >= (Math.abs(sourcePoint.y - targetPoint.y))) { + var controlPointX = (sourcePoint.x + targetPoint.x) / 2; - // find a route by concating all partial routes (routes need to go through the vertices) - // startElement -> vertex[1] -> ... -> vertex[n] -> endElement - for (var i = 0, len = oldVertices.length; i <= len; i++) { + segment = g.Path.createSegment('C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, targetPoint.x, targetPoint.y); + path.appendSegment(segment); - var partialRoute = null; + } else { + var controlPointY = (sourcePoint.y + targetPoint.y) / 2; - var from = to || sourceBBox; - var to = oldVertices[i]; + segment = g.Path.createSegment('C', sourcePoint.x, controlPointY, targetPoint.x, controlPointY, targetPoint.x, targetPoint.y); + path.appendSegment(segment); - if (!to) { + } + } - to = targetBBox; + return (raw) ? path : path.serialize(); +}; - // 'to' is not a vertex. If the target is a point (i.e. it's not an element), we - // might use dragging route instead of main routing method if that is enabled. - var endingAtPoint = !this.model.get('source').id || !this.model.get('target').id; +joint.connectors.jumpover = (function(_, g, util) { - if (endingAtPoint && util.isFunction(opt.draggingRoute)) { - // Make sure we passing points only (not rects). - var dragFrom = from instanceof g.rect ? from.center() : from; - partialRoute = opt.draggingRoute(dragFrom, to.origin(), opt); - } - } + // default size of jump if not specified in options + var JUMP_SIZE = 5; - // if partial route has not been calculated yet use the main routing method to find one - partialRoute = partialRoute || findRoute(from, to, map, opt); + // available jump types + // first one taken as default + var JUMP_TYPES = ['arc', 'gap', 'cubic']; - if (partialRoute === null) { - // The partial route could not be found. - // use orthogonal (do not avoid elements) route instead. - if (!util.isFunction(joint.routers.orthogonal)) { - throw new Error('Manhattan requires the orthogonal router.'); - } - return joint.routers.orthogonal(vertices, opt, this); + // takes care of math. error for case when jump is too close to end of line + var CLOSE_PROXIMITY_PADDING = 1; + + // list of connector types not to jump over. + var IGNORED_CONNECTORS = ['smooth']; + + /** + * Transform start/end and route into series of lines + * @param {g.point} sourcePoint start point + * @param {g.point} targetPoint end point + * @param {g.point[]} route optional list of route + * @return {g.line[]} [description] + */ + function createLines(sourcePoint, targetPoint, route) { + // make a flattened array of all points + var points = [].concat(sourcePoint, route, targetPoint); + return points.reduce(function(resultLines, point, idx) { + // if there is a next point, make a line with it + var nextPoint = points[idx + 1]; + if (nextPoint != null) { + resultLines[idx] = g.line(point, nextPoint); } + return resultLines; + }, []); + } - var leadPoint = partialRoute[0]; + function setupUpdating(jumpOverLinkView) { + var updateList = jumpOverLinkView.paper._jumpOverUpdateList; + + // first time setup for this paper + if (updateList == null) { + updateList = jumpOverLinkView.paper._jumpOverUpdateList = []; + jumpOverLinkView.paper.on('cell:pointerup', updateJumpOver); + jumpOverLinkView.paper.model.on('reset', function() { + updateList = jumpOverLinkView.paper._jumpOverUpdateList = []; + }); + } + + // add this link to a list so it can be updated when some other link is updated + if (updateList.indexOf(jumpOverLinkView) < 0) { + updateList.push(jumpOverLinkView); + + // watch for change of connector type or removal of link itself + // to remove the link from a list of jump over connectors + jumpOverLinkView.listenToOnce(jumpOverLinkView.model, 'change:connector remove', function() { + updateList.splice(updateList.indexOf(jumpOverLinkView), 1); + }); + } + } + + /** + * Handler for a batch:stop event to force + * update of all registered links with jump over connector + * @param {object} batchEvent optional object with info about batch + */ + function updateJumpOver() { + var updateList = this._jumpOverUpdateList; + for (var i = 0; i < updateList.length; i++) { + updateList[i].update(); + } + } - if (leadPoint && leadPoint.equals(tailPoint)) { - // remove the first point if the previous partial route had the same point as last - partialRoute.shift(); + /** + * Utility function to collect all intersection poinst of a single + * line against group of other lines. + * @param {g.line} line where to find points + * @param {g.line[]} crossCheckLines lines to cross + * @return {g.point[]} list of intersection points + */ + function findLineIntersections(line, crossCheckLines) { + return util.toArray(crossCheckLines).reduce(function(res, crossCheckLine) { + var intersection = line.intersection(crossCheckLine); + if (intersection) { + res.push(intersection); } - - tailPoint = partialRoute[partialRoute.length - 1] || tailPoint; - - Array.prototype.push.apply(newVertices, partialRoute); - } - - return newVertices; + return res; + }, []); } - // public function - return function(vertices, opt, linkView) { - - return router.call(linkView, vertices, util.assign({}, config, opt)); - }; - -})(g, _, joint, joint.util); - -joint.routers.metro = (function(util) { - - if (!util.isFunction(joint.routers.manhattan)) { - - throw new Error('Metro requires the manhattan router.'); + /** + * Sorting function for list of points by their distance. + * @param {g.point} p1 first point + * @param {g.point} p2 second point + * @return {number} squared distance between points + */ + function sortPoints(p1, p2) { + return g.line(p1, p2).squaredLength(); } - var config = { - - // cost of a diagonal step (calculated if not defined). - diagonalCost: null, - - // an array of directions to find next points on the route - directions: function() { - - var step = this.step; - var diagonalCost = this.diagonalCost || Math.ceil(Math.sqrt(step * step << 1)); - - return [ - { offsetX: step , offsetY: 0 , cost: step }, - { offsetX: step , offsetY: step , cost: diagonalCost }, - { offsetX: 0 , offsetY: step , cost: step }, - { offsetX: -step , offsetY: step , cost: diagonalCost }, - { offsetX: -step , offsetY: 0 , cost: step }, - { offsetX: -step , offsetY: -step , cost: diagonalCost }, - { offsetX: 0 , offsetY: -step , cost: step }, - { offsetX: step , offsetY: -step , cost: diagonalCost } - ]; - }, - maxAllowedDirectionChange: 45, - // a simple route used in situations, when main routing method fails - // (exceed loops, inaccessible). - fallbackRoute: function(from, to, opts) { - - // Find a route which breaks by 45 degrees ignoring all obstacles. + /** + * Split input line into multiple based on intersection points. + * @param {g.line} line input line to split + * @param {g.point[]} intersections poinst where to split the line + * @param {number} jumpSize the size of jump arc (length empty spot on a line) + * @return {g.line[]} list of lines being split + */ + function createJumps(line, intersections, jumpSize) { + return intersections.reduce(function(resultLines, point, idx) { + // skipping points that were merged with the previous line + // to make bigger arc over multiple lines that are close to each other + if (point.skip === true) { + return resultLines; + } - var theta = from.theta(to); + // always grab the last line from buffer and modify it + var lastLine = resultLines.pop() || line; - var a = { x: to.x, y: from.y }; - var b = { x: from.x, y: to.y }; + // calculate start and end of jump by moving by a given size of jump + var jumpStart = g.point(point).move(lastLine.start, -(jumpSize)); + var jumpEnd = g.point(point).move(lastLine.start, +(jumpSize)); - if (theta % 180 > 90) { - var t = a; - a = b; - b = t; + // now try to look at the next intersection point + var nextPoint = intersections[idx + 1]; + if (nextPoint != null) { + var distance = jumpEnd.distance(nextPoint); + if (distance <= jumpSize) { + // next point is close enough, move the jump end by this + // difference and mark the next point to be skipped + jumpEnd = nextPoint.move(lastLine.start, distance); + nextPoint.skip = true; + } + } else { + // this block is inside of `else` as an optimization so the distance is + // not calculated when we know there are no other intersection points + var endDistance = jumpStart.distance(lastLine.end); + // if the end is too close to possible jump, draw remaining line instead of a jump + if (endDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) { + resultLines.push(lastLine); + return resultLines; + } } - var p1 = (theta % 90) < 45 ? a : b; - - var l1 = g.line(from, p1); - - var alpha = 90 * Math.ceil(theta / 90); + var startDistance = jumpEnd.distance(lastLine.start); + if (startDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) { + // if the start of line is too close to jump, draw that line instead of a jump + resultLines.push(lastLine); + return resultLines; + } - var p2 = g.point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); + // finally create a jump line + var jumpLine = g.line(jumpStart, jumpEnd); + // it's just simple line but with a `isJump` property + jumpLine.isJump = true; - var l2 = g.line(to, p2); + resultLines.push( + g.line(lastLine.start, jumpStart), + jumpLine, + g.line(jumpEnd, lastLine.end) + ); + return resultLines; + }, []); + } - var point = l1.intersection(l2); + /** + * Assemble `D` attribute of a SVG path by iterating given lines. + * @param {g.line[]} lines source lines to use + * @param {number} jumpSize the size of jump arc (length empty spot on a line) + * @return {string} + */ + function buildPath(lines, jumpSize, jumpType) { - return point ? [point.round(), to] : [to]; - } - }; + var path = new g.Path(); + var segment; - // public function - return function(vertices, opts, linkView) { + // first move to the start of a first line + segment = g.Path.createSegment('M', lines[0].start); + path.appendSegment(segment); - return joint.routers.manhattan(vertices, util.assign({}, config, opts), linkView); - }; + // make a paths from lines + joint.util.toArray(lines).forEach(function(line, index) { -})(joint.util); + if (line.isJump) { + var angle, diff; -// Does not make any changes to vertices. -// Returns the arguments that are passed to it, unchanged. -joint.routers.normal = function(vertices, opt, linkView) { + var control1, control2; - return vertices; -}; + if (jumpType === 'arc') { // approximates semicircle with 2 curves + angle = -90; + // determine rotation of arc based on difference between points + diff = line.start.difference(line.end); + // make sure the arc always points up (or right) + var xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0)); + if (xAxisRotate) angle += 180; -// Routes the link always to/from a certain side -// -// Arguments: -// padding ... gap between the element and the first vertex. :: Default 40. -// side ... 'left' | 'right' | 'top' | 'bottom' :: Default 'bottom'. -// -joint.routers.oneSide = function(vertices, opt, linkView) { + var midpoint = line.midpoint(); + var centerLine = new g.Line(midpoint, line.end).rotate(midpoint, angle); - var side = opt.side || 'bottom'; - var padding = opt.padding || 40; + var halfLine; - // LinkView contains cached source an target bboxes. - // Note that those are Geometry rectangle objects. - var sourceBBox = linkView.sourceBBox; - var targetBBox = linkView.targetBBox; - var sourcePoint = sourceBBox.center(); - var targetPoint = targetBBox.center(); + // first half + halfLine = new g.Line(line.start, midpoint); - var coordinate, dimension, direction; + control1 = halfLine.pointAt(2 / 3).rotate(line.start, angle); + control2 = centerLine.pointAt(1 / 3).rotate(centerLine.end, -angle); - switch (side) { - case 'bottom': - direction = 1; - coordinate = 'y'; - dimension = 'height'; - break; - case 'top': - direction = -1; - coordinate = 'y'; - dimension = 'height'; - break; - case 'left': - direction = -1; - coordinate = 'x'; - dimension = 'width'; - break; - case 'right': - direction = 1; - coordinate = 'x'; - dimension = 'width'; - break; - default: - throw new Error('Router: invalid side'); - } + segment = g.Path.createSegment('C', control1, control2, centerLine.end); + path.appendSegment(segment); - // move the points from the center of the element to outside of it. - sourcePoint[coordinate] += direction * (sourceBBox[dimension] / 2 + padding); - targetPoint[coordinate] += direction * (targetBBox[dimension] / 2 + padding); + // second half + halfLine = new g.Line(midpoint, line.end); - // make link orthogonal (at least the first and last vertex). - if (direction * (sourcePoint[coordinate] - targetPoint[coordinate]) > 0) { - targetPoint[coordinate] = sourcePoint[coordinate]; - } else { - sourcePoint[coordinate] = targetPoint[coordinate]; - } + control1 = centerLine.pointAt(1 / 3).rotate(centerLine.end, angle); + control2 = halfLine.pointAt(1 / 3).rotate(line.end, -angle); - return [sourcePoint].concat(vertices, targetPoint); -}; + segment = g.Path.createSegment('C', control1, control2, line.end); + path.appendSegment(segment); -joint.routers.orthogonal = (function(util) { + } else if (jumpType === 'gap') { + segment = g.Path.createSegment('M', line.end); + path.appendSegment(segment); - // bearing -> opposite bearing - var opposite = { - N: 'S', - S: 'N', - E: 'W', - W: 'E' - }; + } else if (jumpType === 'cubic') { // approximates semicircle with 1 curve + angle = line.start.theta(line.end); - // bearing -> radians - var radians = { - N: -Math.PI / 2 * 3, - S: -Math.PI / 2, - E: 0, - W: Math.PI - }; + var xOffset = jumpSize * 0.6; + var yOffset = jumpSize * 1.35; - // HELPERS // + // determine rotation of arc based on difference between points + diff = line.start.difference(line.end); + // make sure the arc always points up (or right) + xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0)); + if (xAxisRotate) yOffset *= -1; - // simple bearing method (calculates only orthogonal cardinals) - function bearing(from, to) { - if (from.x == to.x) return from.y > to.y ? 'N' : 'S'; - if (from.y == to.y) return from.x > to.x ? 'W' : 'E'; - return null; - } + control1 = g.Point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); + control2 = g.Point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); - // returns either width or height of a bbox based on the given bearing - function boxSize(bbox, brng) { - return bbox[brng == 'W' || brng == 'E' ? 'width' : 'height']; - } + segment = g.Path.createSegment('C', control1, control2, line.end); + path.appendSegment(segment); + } - // expands a box by specific value - function expand(bbox, val) { - return g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val }); - } + } else { + segment = g.Path.createSegment('L', line.end); + path.appendSegment(segment); + } + }); - // transform point to a rect - function pointBox(p) { - return g.rect(p.x, p.y, 0, 0); + return path; } - // returns a minimal rect which covers the given boxes - function boundary(bbox1, bbox2) { - - var x1 = Math.min(bbox1.x, bbox2.x); - var y1 = Math.min(bbox1.y, bbox2.y); - var x2 = Math.max(bbox1.x + bbox1.width, bbox2.x + bbox2.width); - var y2 = Math.max(bbox1.y + bbox1.height, bbox2.y + bbox2.height); + /** + * Actual connector function that will be run on every update. + * @param {g.point} sourcePoint start point of this link + * @param {g.point} targetPoint end point of this link + * @param {g.point[]} route of this link + * @param {object} opt options + * @property {number} size optional size of a jump arc + * @return {string} created `D` attribute of SVG path + */ + return function(sourcePoint, targetPoint, route, opt) { // eslint-disable-line max-params - return g.rect(x1, y1, x2 - x1, y2 - y1); - } + setupUpdating(this); - // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained - // in the given box - function freeJoin(p1, p2, bbox) { + var raw = opt.raw; + var jumpSize = opt.size || JUMP_SIZE; + var jumpType = opt.jump && ('' + opt.jump).toLowerCase(); + var ignoreConnectors = opt.ignoreConnectors || IGNORED_CONNECTORS; - var p = g.point(p1.x, p2.y); - if (bbox.containsPoint(p)) p = g.point(p2.x, p1.y); - // kept for reference - // if (bbox.containsPoint(p)) p = null; - return p; - } + // grab the first jump type as a default if specified one is invalid + if (JUMP_TYPES.indexOf(jumpType) === -1) { + jumpType = JUMP_TYPES[0]; + } - // PARTIAL ROUTERS // + var paper = this.paper; + var graph = paper.model; + var allLinks = graph.getLinks(); - function vertexVertex(from, to, brng) { + // there is just one link, draw it directly + if (allLinks.length === 1) { + return buildPath( + createLines(sourcePoint, targetPoint, route), + jumpSize, jumpType + ); + } - var p1 = g.point(from.x, to.y); - var p2 = g.point(to.x, from.y); - var d1 = bearing(from, p1); - var d2 = bearing(from, p2); - var xBrng = opposite[brng]; + var thisModel = this.model; + var thisIndex = allLinks.indexOf(thisModel); + var defaultConnector = paper.options.defaultConnector || {}; - var p = (d1 == brng || (d1 != xBrng && (d2 == xBrng || d2 != brng))) ? p1 : p2; + // not all links are meant to be jumped over. + var links = allLinks.filter(function(link, idx) { - return { points: [p], direction: bearing(p, to) }; - } + var connector = link.get('connector') || defaultConnector; - function elementVertex(from, to, fromBBox) { + // avoid jumping over links with connector type listed in `ignored connectors`. + if (util.toArray(ignoreConnectors).includes(connector.name)) { + return false; + } + // filter out links that are above this one and have the same connector type + // otherwise there would double hoops for each intersection + if (idx > thisIndex) { + return connector.name !== 'jumpover'; + } + return true; + }); - var p = freeJoin(from, to, fromBBox); + // find views for all links + var linkViews = links.map(function(link) { + return paper.findViewByModel(link); + }); - return { points: [p], direction: bearing(p, to) }; - } + // create lines for this link + var thisLines = createLines( + sourcePoint, + targetPoint, + route + ); - function vertexElement(from, to, toBBox, brng) { + // create lines for all other links + var linkLines = linkViews.map(function(linkView) { + if (linkView == null) { + return []; + } + if (linkView === this) { + return thisLines; + } + return createLines( + linkView.sourcePoint, + linkView.targetPoint, + linkView.route + ); + }, this); - var route = {}; + // transform lines for this link by splitting with jump lines at + // points of intersection with other links + var jumpingLines = thisLines.reduce(function(resultLines, thisLine) { + // iterate all links and grab the intersections with this line + // these are then sorted by distance so the line can be split more easily - var pts = [g.point(from.x, to.y), g.point(to.x, from.y)]; - var freePts = pts.filter(function(pt) { return !toBBox.containsPoint(pt); }); - var freeBrngPts = freePts.filter(function(pt) { return bearing(pt, from) != brng; }); + var intersections = links.reduce(function(res, link, i) { + // don't intersection with itself + if (link !== thisModel) { - var p; + var lineIntersections = findLineIntersections(thisLine, linkLines[i]); + res.push.apply(res, lineIntersections); + } + return res; + }, []).sort(function(a, b) { + return sortPoints(thisLine.start, a) - sortPoints(thisLine.start, b); + }); - if (freeBrngPts.length > 0) { + if (intersections.length > 0) { + // split the line based on found intersection points + resultLines.push.apply(resultLines, createJumps(thisLine, intersections, jumpSize)); + } else { + // without any intersection the line goes uninterrupted + resultLines.push(thisLine); + } + return resultLines; + }, []); - // try to pick a point which bears the same direction as the previous segment - p = freeBrngPts.filter(function(pt) { return bearing(from, pt) == brng; }).pop(); - p = p || freeBrngPts[0]; + var path = buildPath(jumpingLines, jumpSize, jumpType); + return (raw) ? path : path.serialize(); + }; +}(_, g, joint.util)); - route.points = [p]; - route.direction = bearing(p, to); +(function(_, g, joint, util) { - } else { + function portTransformAttrs(point, angle, opt) { - // Here we found only points which are either contained in the element or they would create - // a link segment going in opposite direction from the previous one. - // We take the point inside element and move it outside the element in the direction the - // route is going. Now we can join this point with the current end (using freeJoin). + var trans = point.toJSON(); - p = util.difference(pts, freePts)[0]; + trans.angle = angle || 0; - var p2 = g.point(to).move(p, -boxSize(toBBox, brng) / 2); - var p1 = freeJoin(p2, from, toBBox); + return joint.util.defaults({}, opt, trans); + } - route.points = [p1, p2]; - route.direction = bearing(p2, to); - } + function lineLayout(ports, p1, p2) { + return ports.map(function(port, index, ports) { + var p = this.pointAt(((index + 0.5) / ports.length)); + // `dx`,`dy` per port offset option + if (port.dx || port.dy) { + p.offset(port.dx || 0, port.dy || 0); + } - return route; + return portTransformAttrs(p.round(), 0, port); + }, g.line(p1, p2)); } - function elementElement(from, to, fromBBox, toBBox) { + function ellipseLayout(ports, elBBox, startAngle, stepFn) { - var route = elementVertex(to, from, toBBox); - var p1 = route.points[0]; + var center = elBBox.center(); + var ratio = elBBox.width / elBBox.height; + var p1 = elBBox.topMiddle(); - if (fromBBox.containsPoint(p1)) { + var ellipse = g.Ellipse.fromRect(elBBox); - route = elementVertex(from, to, fromBBox); - var p2 = route.points[0]; + return ports.map(function(port, index, ports) { - if (toBBox.containsPoint(p2)) { + var angle = startAngle + stepFn(index, ports.length); + var p2 = p1.clone() + .rotate(center, -angle) + .scale(ratio, 1, center); - var fromBorder = g.point(from).move(p2, -boxSize(fromBBox, bearing(from, p2)) / 2); - var toBorder = g.point(to).move(p1, -boxSize(toBBox, bearing(to, p1)) / 2); - var mid = g.line(fromBorder, toBorder).midpoint(); + var theta = port.compensateRotation ? -ellipse.tangentTheta(p2) : 0; - var startRoute = elementVertex(from, mid, fromBBox); - var endRoute = vertexVertex(mid, to, startRoute.direction); + // `dx`,`dy` per port offset option + if (port.dx || port.dy) { + p2.offset(port.dx || 0, port.dy || 0); + } - route.points = [startRoute.points[0], endRoute.points[0]]; - route.direction = endRoute.direction; + // `dr` delta radius option + if (port.dr) { + p2.move(center, port.dr); } - } - return route; + return portTransformAttrs(p2.round(), theta, port); + }); } - // Finds route for situations where one of end is inside the other. - // Typically the route is conduct outside the outer element first and - // let go back to the inner element. - function insideElement(from, to, fromBBox, toBBox, brng) { + // Creates a point stored in arguments + function argPoint(bbox, args) { + + var x = args.x; + if (util.isString(x)) { + x = parseFloat(x) / 100 * bbox.width; + } - var route = {}; - var bndry = expand(boundary(fromBBox, toBBox), 1); + var y = args.y; + if (util.isString(y)) { + y = parseFloat(y) / 100 * bbox.height; + } - // start from the point which is closer to the boundary - var reversed = bndry.center().distance(to) > bndry.center().distance(from); - var start = reversed ? to : from; - var end = reversed ? from : to; + return g.point(x || 0, y || 0); + } - var p1, p2, p3; + joint.layout.Port = { - if (brng) { - // Points on circle with radius equals 'W + H` are always outside the rectangle - // with width W and height H if the center of that circle is the center of that rectangle. - p1 = g.point.fromPolar(bndry.width + bndry.height, radians[brng], start); - p1 = bndry.pointNearestToPoint(p1).move(p1, -1); - } else { - p1 = bndry.pointNearestToPoint(start).move(start, 1); - } + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + absolute: function(ports, elBBox, opt) { + //TODO v.talas angle + return ports.map(argPoint.bind(null, elBBox)); + }, - p2 = freeJoin(p1, end, bndry); + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + fn: function(ports, elBBox, opt) { + return opt.fn(ports, elBBox, opt); + }, - if (p1.round().equals(p2.round())) { - p2 = g.point.fromPolar(bndry.width + bndry.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); - p2 = bndry.pointNearestToPoint(p2).move(end, 1).round(); - p3 = freeJoin(p1, p2, bndry); - route.points = reversed ? [p2, p3, p1] : [p1, p3, p2]; - } else { - route.points = reversed ? [p2, p1] : [p1, p2]; - } + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + line: function(ports, elBBox, opt) { - route.direction = reversed ? bearing(p1, to) : bearing(p2, to); + var start = argPoint(elBBox, opt.start || elBBox.origin()); + var end = argPoint(elBBox, opt.end || elBBox.corner()); - return route; - } + return lineLayout(ports, start, end); + }, - // MAIN ROUTER // + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + left: function(ports, elBBox, opt) { + return lineLayout(ports, elBBox.origin(), elBBox.bottomLeft()); + }, - // Return points that one needs to draw a connection through in order to have a orthogonal link - // routing from source to target going through `vertices`. - function findOrthogonalRoute(vertices, opt, linkView) { + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + right: function(ports, elBBox, opt) { + return lineLayout(ports, elBBox.topRight(), elBBox.corner()); + }, - var padding = opt.elementPadding || 20; + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + top: function(ports, elBBox, opt) { + return lineLayout(ports, elBBox.origin(), elBBox.topRight()); + }, + + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt opt Group options + * @returns {Array} + */ + bottom: function(ports, elBBox, opt) { + return lineLayout(ports, elBBox.bottomLeft(), elBBox.corner()); + }, - var orthogonalVertices = []; - var sourceBBox = expand(linkView.sourceBBox, padding); - var targetBBox = expand(linkView.targetBBox, padding); + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt Group options + * @returns {Array} + */ + ellipseSpread: function(ports, elBBox, opt) { - vertices = util.toArray(vertices).map(g.point); - vertices.unshift(sourceBBox.center()); - vertices.push(targetBBox.center()); + var startAngle = opt.startAngle || 0; + var stepAngle = opt.step || 360 / ports.length; - var brng; + return ellipseLayout(ports, elBBox, startAngle, function(index) { + return index * stepAngle; + }); + }, - for (var i = 0, max = vertices.length - 1; i < max; i++) { + /** + * @param {Array} ports + * @param {g.Rect} elBBox + * @param {Object=} opt Group options + * @returns {Array} + */ + ellipse: function(ports, elBBox, opt) { - var route = null; - var from = vertices[i]; - var to = vertices[i + 1]; - var isOrthogonal = !!bearing(from, to); + var startAngle = opt.startAngle || 0; + var stepAngle = opt.step || 20; - if (i == 0) { + return ellipseLayout(ports, elBBox, startAngle, function(index, count) { + return (index + 0.5 - count / 2) * stepAngle; + }); + } + }; - if (i + 1 == max) { // route source -> target +})(_, g, joint, joint.util); - // Expand one of elements by 1px so we detect also situations when they - // are positioned one next other with no gap between. - if (sourceBBox.intersect(expand(targetBBox, 1))) { - route = insideElement(from, to, sourceBBox, targetBBox); - } else if (!isOrthogonal) { - route = elementElement(from, to, sourceBBox, targetBBox); - } +(function(_, g, joint, util) { - } else { // route source -> vertex + function labelAttributes(opt1, opt2) { - if (sourceBBox.containsPoint(to)) { - route = insideElement(from, to, sourceBBox, expand(pointBox(to), padding)); - } else if (!isOrthogonal) { - route = elementVertex(from, to, sourceBBox); - } + return util.defaultsDeep({}, opt1, opt2, { + x: 0, + y: 0, + angle: 0, + attrs: { + '.': { + y: '0', + 'text-anchor': 'start' } + } + }); - } else if (i + 1 == max) { // route vertex -> target + } - var orthogonalLoop = isOrthogonal && bearing(to, from) == brng; + function outsideLayout(portPosition, elBBox, autoOrient, opt) { - if (targetBBox.containsPoint(from) || orthogonalLoop) { - route = insideElement(from, to, expand(pointBox(from), padding), targetBBox, brng); - } else if (!isOrthogonal) { - route = vertexElement(from, to, targetBBox, brng); - } + opt = util.defaults({}, opt, { offset: 15 }); + var angle = elBBox.center().theta(portPosition); + var x = getBBoxAngles(elBBox); - } else if (!isOrthogonal) { // route vertex -> vertex - route = vertexVertex(from, to, brng); - } + var tx, ty, y, textAnchor; + var offset = opt.offset; + var orientAngle = 0; - if (route) { - Array.prototype.push.apply(orthogonalVertices, route.points); - brng = route.direction; + if (angle < x[1] || angle > x[2]) { + y = '.3em'; + tx = offset; + ty = 0; + textAnchor = 'start'; + } else if (angle < x[0]) { + y = '0'; + tx = 0; + ty = -offset; + if (autoOrient) { + orientAngle = -90; + textAnchor = 'start'; } else { - // orthogonal route and not looped - brng = bearing(from, to); + textAnchor = 'middle'; } - - if (i + 1 < max) { - orthogonalVertices.push(to); + } else if (angle < x[3]) { + y = '.3em'; + tx = -offset; + ty = 0; + textAnchor = 'end'; + } else { + y = '.6em'; + tx = 0; + ty = offset; + if (autoOrient) { + orientAngle = 90; + textAnchor = 'start'; + } else { + textAnchor = 'middle'; } } - return orthogonalVertices; + var round = Math.round; + return labelAttributes({ + x: round(tx), + y: round(ty), + angle: orientAngle, + attrs: { + '.': { + y: y, + 'text-anchor': textAnchor + } + } + }); } - return findOrthogonalRoute; - -})(joint.util); - -joint.connectors.normal = function(sourcePoint, targetPoint, vertices) { - - // Construct the `d` attribute of the `` element. - var d = ['M', sourcePoint.x, sourcePoint.y]; - - joint.util.toArray(vertices).forEach(function(vertex) { - - d.push(vertex.x, vertex.y); - }); - - d.push(targetPoint.x, targetPoint.y); - - return d.join(' '); -}; + function getBBoxAngles(elBBox) { -joint.connectors.rounded = function(sourcePoint, targetPoint, vertices, opts) { + var center = elBBox.center(); - opts = opts || {}; + var tl = center.theta(elBBox.origin()); + var bl = center.theta(elBBox.bottomLeft()); + var br = center.theta(elBBox.corner()); + var tr = center.theta(elBBox.topRight()); - var offset = opts.radius || 10; + return [tl, tr, br, bl]; + } - var c1, c2, d1, d2, prev, next; + function insideLayout(portPosition, elBBox, autoOrient, opt) { - // Construct the `d` attribute of the `` element. - var d = ['M', sourcePoint.x, sourcePoint.y]; + var angle = elBBox.center().theta(portPosition); + opt = util.defaults({}, opt, { offset: 15 }); - joint.util.toArray(vertices).forEach(function(vertex, index) { + var tx, ty, y, textAnchor; + var offset = opt.offset; + var orientAngle = 0; - // the closest vertices - prev = vertices[index - 1] || sourcePoint; - next = vertices[index + 1] || targetPoint; + var bBoxAngles = getBBoxAngles(elBBox); - // a half distance to the closest vertex - d1 = d2 || g.point(vertex).distance(prev) / 2; - d2 = g.point(vertex).distance(next) / 2; + if (angle < bBoxAngles[1] || angle > bBoxAngles[2]) { + y = '.3em'; + tx = -offset; + ty = 0; + textAnchor = 'end'; + } else if (angle < bBoxAngles[0]) { + y = '.6em'; + tx = 0; + ty = offset; + if (autoOrient) { + orientAngle = 90; + textAnchor = 'start'; + } else { + textAnchor = 'middle'; + } + } else if (angle < bBoxAngles[3]) { + y = '.3em'; + tx = offset; + ty = 0; + textAnchor = 'start'; + } else { + y = '0em'; + tx = 0; + ty = -offset; + if (autoOrient) { + orientAngle = -90; + textAnchor = 'start'; + } else { + textAnchor = 'middle'; + } + } - // control points - c1 = g.point(vertex).move(prev, -Math.min(offset, d1)).round(); - c2 = g.point(vertex).move(next, -Math.min(offset, d2)).round(); + var round = Math.round; + return labelAttributes({ + x: round(tx), + y: round(ty), + angle: orientAngle, + attrs: { + '.': { + y: y, + 'text-anchor': textAnchor + } + } + }); + } - d.push(c1.x, c1.y, 'S', vertex.x, vertex.y, c2.x, c2.y, 'L'); - }); + function radialLayout(portCenterOffset, autoOrient, opt) { - d.push(targetPoint.x, targetPoint.y); + opt = util.defaults({}, opt, { offset: 20 }); - return d.join(' '); -}; + var origin = g.point(0, 0); + var angle = -portCenterOffset.theta(origin); + var orientAngle = angle; + var offset = portCenterOffset.clone() + .move(origin, opt.offset) + .difference(portCenterOffset) + .round(); -joint.connectors.smooth = function(sourcePoint, targetPoint, vertices) { + var y = '.3em'; + var textAnchor; - var d; + if ((angle + 90) % 180 === 0) { + textAnchor = autoOrient ? 'end' : 'middle'; + if (!autoOrient && angle === -270) { + y = '0em'; + } + } else if (angle > -270 && angle < -90) { + textAnchor = 'start'; + orientAngle = angle - 180; + } else { + textAnchor = 'end'; + } - if (vertices.length) { + var round = Math.round; + return labelAttributes({ + x: round(offset.x), + y: round(offset.y), + angle: autoOrient ? orientAngle : 0, + attrs: { + '.': { + y: y, + 'text-anchor': textAnchor + } + } + }); + } - d = g.bezier.curveThroughPoints([sourcePoint].concat(vertices).concat([targetPoint])); + joint.layout.PortLabel = { - } else { - // if we have no vertices use a default cubic bezier curve, cubic bezier requires - // two control points. The two control points are both defined with X as mid way - // between the source and target points. SourceControlPoint Y is equal to sourcePoint Y - // and targetControlPointY being equal to targetPointY. - var controlPointX = (sourcePoint.x + targetPoint.x) / 2; + manual: function(portPosition, elBBox, opt) { + return labelAttributes(opt, portPosition) + }, - d = [ - 'M', sourcePoint.x, sourcePoint.y, - 'C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, - targetPoint.x, targetPoint.y - ]; - } + left: function(portPosition, elBBox, opt) { + return labelAttributes(opt, { x: -15, attrs: { '.': { y: '.3em', 'text-anchor': 'end' } } }); + }, - return d.join(' '); -}; + right: function(portPosition, elBBox, opt) { + return labelAttributes(opt, { x: 15, attrs: { '.': { y: '.3em', 'text-anchor': 'start' } } }); + }, -joint.connectors.jumpover = (function(_, g, util) { + top: function(portPosition, elBBox, opt) { + return labelAttributes(opt, { y: -15, attrs: { '.': { 'text-anchor': 'middle' } } }); + }, - // default size of jump if not specified in options - var JUMP_SIZE = 5; + bottom: function(portPosition, elBBox, opt) { + return labelAttributes(opt, { y: 15, attrs: { '.': { y: '.6em', 'text-anchor': 'middle' } } }); + }, - // available jump types - var JUMP_TYPES = ['arc', 'gap', 'cubic']; + outsideOriented: function(portPosition, elBBox, opt) { + return outsideLayout(portPosition, elBBox, true, opt); + }, - // takes care of math. error for case when jump is too close to end of line - var CLOSE_PROXIMITY_PADDING = 1; + outside: function(portPosition, elBBox, opt) { + return outsideLayout(portPosition, elBBox, false, opt); + }, - // list of connector types not to jump over. - var IGNORED_CONNECTORS = ['smooth']; + insideOriented: function(portPosition, elBBox, opt) { + return insideLayout(portPosition, elBBox, true, opt); + }, - /** - * Transform start/end and vertices into series of lines - * @param {g.point} sourcePoint start point - * @param {g.point} targetPoint end point - * @param {g.point[]} vertices optional list of vertices - * @return {g.line[]} [description] - */ - function createLines(sourcePoint, targetPoint, vertices) { - // make a flattened array of all points - var points = [].concat(sourcePoint, vertices, targetPoint); - return points.reduce(function(resultLines, point, idx) { - // if there is a next point, make a line with it - var nextPoint = points[idx + 1]; - if (nextPoint != null) { - resultLines[idx] = g.line(point, nextPoint); - } - return resultLines; - }, []); - } + inside: function(portPosition, elBBox, opt) { + return insideLayout(portPosition, elBBox, false, opt); + }, - function setupUpdating(jumpOverLinkView) { - var updateList = jumpOverLinkView.paper._jumpOverUpdateList; + radial: function(portPosition, elBBox, opt) { + return radialLayout(portPosition.difference(elBBox.center()), false, opt); + }, - // first time setup for this paper - if (updateList == null) { - updateList = jumpOverLinkView.paper._jumpOverUpdateList = []; - jumpOverLinkView.paper.on('cell:pointerup', updateJumpOver); - jumpOverLinkView.paper.model.on('reset', function() { - updateList = jumpOverLinkView.paper._jumpOverUpdateList = []; - }); + radialOriented: function(portPosition, elBBox, opt) { + return radialLayout(portPosition.difference(elBBox.center()), true, opt); } + }; - // add this link to a list so it can be updated when some other link is updated - if (updateList.indexOf(jumpOverLinkView) < 0) { - updateList.push(jumpOverLinkView); +})(_, g, joint, joint.util); - // watch for change of connector type or removal of link itself - // to remove the link from a list of jump over connectors - jumpOverLinkView.listenToOnce(jumpOverLinkView.model, 'change:connector remove', function() { - updateList.splice(updateList.indexOf(jumpOverLinkView), 1); - }); - } - } +joint.highlighters.addClass = { - /** - * Handler for a batch:stop event to force - * update of all registered links with jump over connector - * @param {object} batchEvent optional object with info about batch - */ - function updateJumpOver() { - var updateList = this._jumpOverUpdateList; - for (var i = 0; i < updateList.length; i++) { - updateList[i].update(); - } - } + className: joint.util.addClassNamePrefix('highlighted'), /** - * Utility function to collect all intersection poinst of a single - * line against group of other lines. - * @param {g.line} line where to find points - * @param {g.line[]} crossCheckLines lines to cross - * @return {g.point[]} list of intersection points + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt */ - function findLineIntersections(line, crossCheckLines) { - return util.toArray(crossCheckLines).reduce(function(res, crossCheckLine) { - var intersection = line.intersection(crossCheckLine); - if (intersection) { - res.push(intersection); - } - return res; - }, []); - } + highlight: function(cellView, magnetEl, opt) { - /** - * Sorting function for list of points by their distance. - * @param {g.point} p1 first point - * @param {g.point} p2 second point - * @return {number} squared distance between points - */ - function sortPoints(p1, p2) { - return g.line(p1, p2).squaredLength(); - } + var options = opt || {}; + var className = options.className || this.className; + V(magnetEl).addClass(className); + }, /** - * Split input line into multiple based on intersection points. - * @param {g.line} line input line to split - * @param {g.point[]} intersections poinst where to split the line - * @param {number} jumpSize the size of jump arc (length empty spot on a line) - * @return {g.line[]} list of lines being split + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt */ - function createJumps(line, intersections, jumpSize) { - return intersections.reduce(function(resultLines, point, idx) { - // skipping points that were merged with the previous line - // to make bigger arc over multiple lines that are close to each other - if (point.skip === true) { - return resultLines; - } - - // always grab the last line from buffer and modify it - var lastLine = resultLines.pop() || line; - - // calculate start and end of jump by moving by a given size of jump - var jumpStart = g.point(point).move(lastLine.start, -(jumpSize)); - var jumpEnd = g.point(point).move(lastLine.start, +(jumpSize)); - - // now try to look at the next intersection point - var nextPoint = intersections[idx + 1]; - if (nextPoint != null) { - var distance = jumpEnd.distance(nextPoint); - if (distance <= jumpSize) { - // next point is close enough, move the jump end by this - // difference and mark the next point to be skipped - jumpEnd = nextPoint.move(lastLine.start, distance); - nextPoint.skip = true; - } - } else { - // this block is inside of `else` as an optimization so the distance is - // not calculated when we know there are no other intersection points - var endDistance = jumpStart.distance(lastLine.end); - // if the end is too close to possible jump, draw remaining line instead of a jump - if (endDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) { - resultLines.push(lastLine); - return resultLines; - } - } - - var startDistance = jumpEnd.distance(lastLine.start); - if (startDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) { - // if the start of line is too close to jump, draw that line instead of a jump - resultLines.push(lastLine); - return resultLines; - } - - // finally create a jump line - var jumpLine = g.line(jumpStart, jumpEnd); - // it's just simple line but with a `isJump` property - jumpLine.isJump = true; + unhighlight: function(cellView, magnetEl, opt) { - resultLines.push( - g.line(lastLine.start, jumpStart), - jumpLine, - g.line(jumpEnd, lastLine.end) - ); - return resultLines; - }, []); + var options = opt || {}; + var className = options.className || this.className; + V(magnetEl).removeClass(className); } +}; + +joint.highlighters.opacity = { /** - * Assemble `D` attribute of a SVG path by iterating given lines. - * @param {g.line[]} lines source lines to use - * @param {number} jumpSize the size of jump arc (length empty spot on a line) - * @return {string} + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl */ - function buildPath(lines, jumpSize, jumpType) { - // first move to the start of a first line - var start = ['M', lines[0].start.x, lines[0].start.y]; - - // make a paths from lines - var paths = util.toArray(lines).reduce(function(res, line) { - if (line.isJump) { - var diff; - if (jumpType === 'arc') { - diff = line.start.difference(line.end); - // determine rotation of arc based on difference between points - var xAxisRotate = Number(diff.x < 0 && diff.y < 0); - // for a jump line we create an arc instead - res.push('A', jumpSize, jumpSize, 0, 0, xAxisRotate, line.end.x, line.end.y); - } else if (jumpType === 'gap') { - res = res.concat(['M', line.end.x, line.end.y]); - } else if (jumpType === 'cubic') { - diff = line.start.difference(line.end); - var angle = line.start.theta(line.end); - var xOffset = jumpSize * 0.6; - var yOffset = jumpSize * 1.35; - // determine rotation of curve based on difference between points - if (diff.x < 0 && diff.y < 0) { - yOffset *= -1; - } - var controlStartPoint = g.point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); - var controlEndPoint = g.point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); - // create a cubic bezier curve - res.push('C', controlStartPoint.x, controlStartPoint.y, controlEndPoint.x, controlEndPoint.y, line.end.x, line.end.y); - } - } else { - res.push('L', line.end.x, line.end.y); - } - return res; - }, start); + highlight: function(cellView, magnetEl) { - return paths.join(' '); - } + V(magnetEl).addClass(joint.util.addClassNamePrefix('highlight-opacity')); + }, /** - * Actual connector function that will be run on every update. - * @param {g.point} sourcePoint start point of this link - * @param {g.point} targetPoint end point of this link - * @param {g.point[]} vertices of this link - * @param {object} opts options - * @property {number} size optional size of a jump arc - * @return {string} created `D` attribute of SVG path + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl */ - return function(sourcePoint, targetPoint, vertices, opts) { // eslint-disable-line max-params + unhighlight: function(cellView, magnetEl) { - setupUpdating(this); + V(magnetEl).removeClass(joint.util.addClassNamePrefix('highlight-opacity')); + } +}; - var jumpSize = opts.size || JUMP_SIZE; - var jumpType = opts.jump && ('' + opts.jump).toLowerCase(); - var ignoreConnectors = opts.ignoreConnectors || IGNORED_CONNECTORS; +joint.highlighters.stroke = { - // grab the first jump type as a default if specified one is invalid - if (JUMP_TYPES.indexOf(jumpType) === -1) { - jumpType = JUMP_TYPES[0]; + defaultOptions: { + padding: 3, + rx: 0, + ry: 0, + attrs: { + 'stroke-width': 3, + stroke: '#FEB663' } + }, - var paper = this.paper; - var graph = paper.model; - var allLinks = graph.getLinks(); + _views: {}, - // there is just one link, draw it directly - if (allLinks.length === 1) { - return buildPath( - createLines(sourcePoint, targetPoint, vertices), - jumpSize, jumpType - ); + getHighlighterId: function(magnetEl, opt) { + + return magnetEl.id + JSON.stringify(opt); + }, + + removeHighlighter: function(id) { + if (this._views[id]) { + this._views[id].remove(); + this._views[id] = null; } + }, - var thisModel = this.model; - var thisIndex = allLinks.indexOf(thisModel); - var defaultConnector = paper.options.defaultConnector || {}; + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt + */ + highlight: function(cellView, magnetEl, opt) { - // not all links are meant to be jumped over. - var links = allLinks.filter(function(link, idx) { + var id = this.getHighlighterId(magnetEl, opt); - var connector = link.get('connector') || defaultConnector; + // Only highlight once. + if (this._views[id]) return; - // avoid jumping over links with connector type listed in `ignored connectors`. - if (util.toArray(ignoreConnectors).includes(connector.name)) { - return false; - } - // filter out links that are above this one and have the same connector type - // otherwise there would double hoops for each intersection - if (idx > thisIndex) { - return connector.name !== 'jumpover'; - } - return true; - }); + var options = joint.util.defaults(opt || {}, this.defaultOptions); - // find views for all links - var linkViews = links.map(function(link) { - return paper.findViewByModel(link); - }); + var magnetVel = V(magnetEl); + var magnetBBox; - // create lines for this link - var thisLines = createLines( - sourcePoint, - targetPoint, - vertices - ); + try { - // create lines for all other links - var linkLines = linkViews.map(function(linkView) { - if (linkView == null) { - return []; - } - if (linkView === this) { - return thisLines; - } - return createLines( - linkView.sourcePoint, - linkView.targetPoint, - linkView.route - ); - }, this); + var pathData = magnetVel.convertToPathData(); + + } catch (error) { + + // Failed to get path data from magnet element. + // Draw a rectangle around the entire cell view instead. + magnetBBox = magnetVel.bbox(true/* without transforms */); + pathData = V.rectToPath(joint.util.assign({}, options, magnetBBox)); + } - // transform lines for this link by splitting with jump lines at - // points of intersection with other links - var jumpingLines = thisLines.reduce(function(resultLines, thisLine) { - // iterate all links and grab the intersections with this line - // these are then sorted by distance so the line can be split more easily + var highlightVel = V('path').attr({ + d: pathData, + 'pointer-events': 'none', + 'vector-effect': 'non-scaling-stroke', + 'fill': 'none' + }).attr(options.attrs); - var intersections = links.reduce(function(res, link, i) { - // don't intersection with itself - if (link !== thisModel) { + var highlightMatrix = magnetVel.getTransformToElement(cellView.el); - var lineIntersections = findLineIntersections(thisLine, linkLines[i]); - res.push.apply(res, lineIntersections); - } - return res; - }, []).sort(function(a, b) { - return sortPoints(thisLine.start, a) - sortPoints(thisLine.start, b); - }); + // Add padding to the highlight element. + var padding = options.padding; + if (padding) { - if (intersections.length > 0) { - // split the line based on found intersection points - resultLines.push.apply(resultLines, createJumps(thisLine, intersections, jumpSize)); - } else { - // without any intersection the line goes uninterrupted - resultLines.push(thisLine); - } - return resultLines; - }, []); + magnetBBox || (magnetBBox = magnetVel.bbox(true)); + var cx = magnetBBox.x + (magnetBBox.width / 2); + var cy = magnetBBox.y + (magnetBBox.height / 2); - return buildPath(jumpingLines, jumpSize, jumpType); - }; -}(_, g, joint.util)); + magnetBBox = V.transformRect(magnetBBox, highlightMatrix); -(function(_, g, joint, util) { + var width = Math.max(magnetBBox.width, 1); + var height = Math.max(magnetBBox.height, 1); + var sx = (width + padding) / width; + var sy = (height + padding) / height; - function portTransformAttrs(point, angle, opt) { + var paddingMatrix = V.createSVGMatrix({ + a: sx, + b: 0, + c: 0, + d: sy, + e: cx - sx * cx, + f: cy - sy * cy + }); - var trans = point.toJSON(); + highlightMatrix = highlightMatrix.multiply(paddingMatrix); + } - trans.angle = angle || 0; + highlightVel.transform(highlightMatrix); - return joint.util.defaults({}, opt, trans); - } + // joint.mvc.View will handle the theme class name and joint class name prefix. + var highlightView = this._views[id] = new joint.mvc.View({ + svgElement: true, + className: 'highlight-stroke', + el: highlightVel.node + }); - function lineLayout(ports, p1, p2) { - return ports.map(function(port, index, ports) { - var p = this.pointAt(((index + 0.5) / ports.length)); - // `dx`,`dy` per port offset option - if (port.dx || port.dy) { - p.offset(port.dx || 0, port.dy || 0); - } + // Remove the highlight view when the cell is removed from the graph. + var removeHandler = this.removeHighlighter.bind(this, id); + var cell = cellView.model; + highlightView.listenTo(cell, 'remove', removeHandler); + highlightView.listenTo(cell.graph, 'reset', removeHandler); - return portTransformAttrs(p.round(), 0, port); - }, g.line(p1, p2)); - } + cellView.vel.append(highlightVel); + }, - function ellipseLayout(ports, elBBox, startAngle, stepFn) { + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt + */ + unhighlight: function(cellView, magnetEl, opt) { - var center = elBBox.center(); - var ratio = elBBox.width / elBBox.height; - var p1 = elBBox.topMiddle(); + this.removeHighlighter(this.getHighlighterId(magnetEl, opt)); + } +}; - var ellipse = g.Ellipse.fromRect(elBBox); +(function(joint, util) { - return ports.map(function(port, index, ports) { + function bboxWrapper(method) { - var angle = startAngle + stepFn(index, ports.length); - var p2 = p1.clone() - .rotate(center, -angle) - .scale(ratio, 1, center); + return function(view, magnet, ref, opt) { - var theta = port.compensateRotation ? -ellipse.tangentTheta(p2) : 0; + var rotate = !!opt.rotate; + var bbox = (rotate) ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet); + var anchor = bbox[method](); - // `dx`,`dy` per port offset option - if (port.dx || port.dy) { - p2.offset(port.dx || 0, port.dy || 0); + var dx = opt.dx; + if (dx) { + var dxPercentage = util.isPercentage(dx); + dx = parseFloat(dx); + if (isFinite(dx)) { + if (dxPercentage) { + dx /= 100; + dx *= bbox.width; + } + anchor.x += dx; + } } - // `dr` delta radius option - if (port.dr) { - p2.move(center, port.dr); + var dy = opt.dy; + if (dy) { + var dyPercentage = util.isPercentage(dy); + dy = parseFloat(dy); + if (isFinite(dy)) { + if (dyPercentage) { + dy /= 100; + dy *= bbox.height; + } + anchor.y += dy; + } } - return portTransformAttrs(p2.round(), theta, port); - }); + return (rotate) ? anchor.rotate(view.model.getBBox().center(), -view.model.angle()) : anchor; + } } - // Creates a point stored in arguments - function argPoint(bbox, args) { + function resolveRefAsBBoxCenter(fn) { - var x = args.x; - if (util.isString(x)) { - x = parseFloat(x) / 100 * bbox.width; + return function(view, magnet, ref, opt) { + + if (ref instanceof Element) { + var refView = this.paper.findView(ref); + var refPoint = refView.getNodeBBox(ref).center(); + + return fn.call(this, view, magnet, refPoint, opt) + } + + return fn.apply(this, arguments); } + } - var y = args.y; - if (util.isString(y)) { - y = parseFloat(y) / 100 * bbox.height; + function perpendicular(view, magnet, refPoint, opt) { + + var angle = view.model.angle(); + var bbox = view.getNodeBBox(magnet); + var anchor = bbox.center(); + var topLeft = bbox.origin(); + var bottomRight = bbox.corner(); + + var padding = opt.padding + if (!isFinite(padding)) padding = 0; + + if ((topLeft.y + padding) <= refPoint.y && refPoint.y <= (bottomRight.y - padding)) { + var dy = (refPoint.y - anchor.y); + anchor.x += (angle === 0 || angle === 180) ? 0 : dy * 1 / Math.tan(g.toRad(angle)); + anchor.y += dy; + } else if ((topLeft.x + padding) <= refPoint.x && refPoint.x <= (bottomRight.x - padding)) { + var dx = (refPoint.x - anchor.x); + anchor.y += (angle === 90 || angle === 270) ? 0 : dx * Math.tan(g.toRad(angle)); + anchor.x += dx; } - return g.point(x || 0, y || 0); + return anchor; } - joint.layout.Port = { + function midSide(view, magnet, refPoint, opt) { - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - absolute: function(ports, elBBox, opt) { - //TODO v.talas angle - return ports.map(argPoint.bind(null, elBBox)); - }, + var rotate = !!opt.rotate; + var bbox, angle, center; + if (rotate) { + bbox = view.getNodeUnrotatedBBox(magnet); + center = view.model.getBBox().center(); + angle = view.model.angle(); + } else { + bbox = view.getNodeBBox(magnet); + } - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - fn: function(ports, elBBox, opt) { - return opt.fn(ports, elBBox, opt); - }, + var padding = opt.padding; + if (isFinite(padding)) bbox.inflate(padding); - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - line: function(ports, elBBox, opt) { + if (rotate) refPoint.rotate(center, angle); - var start = argPoint(elBBox, opt.start || elBBox.origin()); - var end = argPoint(elBBox, opt.end || elBBox.corner()); + var side = bbox.sideNearestToPoint(refPoint); + var anchor; + switch (side) { + case 'left': anchor = bbox.leftMiddle(); break; + case 'right': anchor = bbox.rightMiddle(); break; + case 'top': anchor = bbox.topMiddle(); break; + case 'bottom': anchor = bbox.bottomMiddle(); break; + } - return lineLayout(ports, start, end); - }, + return (rotate) ? anchor.rotate(center, -angle) : anchor; + } - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - left: function(ports, elBBox, opt) { - return lineLayout(ports, elBBox.origin(), elBBox.bottomLeft()); - }, + // Can find anchor from model, when there is no selector or the link end + // is connected to a port + function modelCenter(view, magnet) { - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - right: function(ports, elBBox, opt) { - return lineLayout(ports, elBBox.topRight(), elBBox.corner()); - }, + var model = view.model; + var bbox = model.getBBox(); + var center = bbox.center(); + var angle = model.angle(); - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - top: function(ports, elBBox, opt) { - return lineLayout(ports, elBBox.origin(), elBBox.topRight()); - }, + var portId = view.findAttribute('port', magnet); + if (portId) { + portGroup = model.portProp(portId, 'group'); + var portsPositions = model.getPortsPositions(portGroup); + var anchor = new g.Point(portsPositions[portId]).offset(bbox.origin()); + anchor.rotate(center, -angle); + return anchor; + } - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt opt Group options - * @returns {Array} - */ - bottom: function(ports, elBBox, opt) { - return lineLayout(ports, elBBox.bottomLeft(), elBBox.corner()); - }, + return center; + } - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt Group options - * @returns {Array} - */ - ellipseSpread: function(ports, elBBox, opt) { + joint.anchors = { + center: bboxWrapper('center'), + top: bboxWrapper('topMiddle'), + bottom: bboxWrapper('bottomMiddle'), + left: bboxWrapper('leftMiddle'), + right: bboxWrapper('rightMiddle'), + topLeft: bboxWrapper('origin'), + topRight: bboxWrapper('topRight'), + bottomLeft: bboxWrapper('bottomLeft'), + bottomRight: bboxWrapper('corner'), + perpendicular: resolveRefAsBBoxCenter(perpendicular), + midSide: resolveRefAsBBoxCenter(midSide), + modelCenter: modelCenter + } - var startAngle = opt.startAngle || 0; - var stepAngle = opt.step || 360 / ports.length; +})(joint, joint.util); - return ellipseLayout(ports, elBBox, startAngle, function(index) { - return index * stepAngle; - }); - }, +(function(joint, util, g, V) { - /** - * @param {Array} ports - * @param {g.Rect} elBBox - * @param {Object=} opt Group options - * @returns {Array} - */ - ellipse: function(ports, elBBox, opt) { + function closestIntersection(intersections, refPoint) { - var startAngle = opt.startAngle || 0; - var stepAngle = opt.step || 20; + if (intersections.length === 1) return intersections[0]; + return util.sortBy(intersections, function(i) { return i.squaredDistance(refPoint) })[0]; + } - return ellipseLayout(ports, elBBox, startAngle, function(index, count) { - return (index + 0.5 - count / 2) * stepAngle; - }); - } - }; + function offset(p1, p2, offset) { -})(_, g, joint, joint.util); + if (!isFinite(offset)) return p1; + var length = p1.distance(p2); + if (offset === 0 && length > 0) return p1; + return p1.move(p2, -Math.min(offset, length - 1)); + } -(function(_, g, joint, util) { + function stroke(magnet) { - function labelAttributes(opt1, opt2) { + var stroke = magnet.getAttribute('stroke-width'); + if (stroke === null) return 0; + return parseFloat(stroke) || 0; + } - return util.defaultsDeep({}, opt1, opt2, { - x: 0, - y: 0, - angle: 0, - attrs: { - '.': { - y: '0', - 'text-anchor': 'start' - } - } - }); + // Connection Points + + function anchor(line, view, magnet, opt) { + return offset(line.end, line.start, opt.offset); } - function outsideLayout(portPosition, elBBox, autoOrient, opt) { + function bboxIntersection(line, view, magnet, opt) { - opt = util.defaults({}, opt, { offset: 15 }); - var angle = elBBox.center().theta(portPosition); - var x = getBBoxAngles(elBBox); + var bbox = view.getNodeBBox(magnet); + if (opt.stroke) bbox.inflate(stroke(magnet) / 2); + var intersections = line.intersect(bbox); + var cp = (intersections) + ? closestIntersection(intersections, line.start) + : line.end; + return offset(cp, line.start, opt.offset); + } - var tx, ty, y, textAnchor; - var offset = opt.offset; - var orientAngle = 0; + function rectangleIntersection(line, view, magnet, opt) { - if (angle < x[1] || angle > x[2]) { - y = '.3em'; - tx = offset; - ty = 0; - textAnchor = 'start'; - } else if (angle < x[0]) { - y = '0'; - tx = 0; - ty = -offset; - if (autoOrient) { - orientAngle = -90; - textAnchor = 'start'; - } else { - textAnchor = 'middle'; - } - } else if (angle < x[3]) { - y = '.3em'; - tx = -offset; - ty = 0; - textAnchor = 'end'; + var angle = view.model.angle(); + if (angle === 0) { + return bboxIntersection(line, view, magnet, opt); + } + + var bboxWORotation = view.getNodeUnrotatedBBox(magnet); + if (opt.stroke) bboxWORotation.inflate(stroke(magnet) / 2); + var center = bboxWORotation.center(); + var lineWORotation = line.clone().rotate(center, angle); + var intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation); + var cp = (intersections) + ? closestIntersection(intersections, lineWORotation.start).rotate(center, -angle) + : line.end; + return offset(cp, line.start, opt.offset); + } + + var BNDR_SUBDIVISIONS = 'segmentSubdivisons'; + var BNDR_SHAPE_BBOX = 'shapeBBox'; + + function boundaryIntersection(line, view, magnet, opt) { + + var node, intersection; + var selector = opt.selector; + var anchor = line.end; + + if (typeof selector === 'string') { + node = view.findBySelector(selector)[0]; + } else if (Array.isArray(selector)) { + node = util.getByPath(magnet, selector); } else { - y = '.6em'; - tx = 0; - ty = offset; - if (autoOrient) { - orientAngle = 90; - textAnchor = 'start'; + // Find the closest non-group descendant + node = magnet; + do { + var tagName = node.tagName.toUpperCase(); + if (tagName === 'G') { + node = node.firstChild; + } else if (tagName === 'TITLE') { + node = node.nextSibling; + } else break; + } while (node) + } + + if (!(node instanceof Element)) return anchor; + + var localShape = view.getNodeShape(node); + var magnetMatrix = view.getNodeMatrix(node); + var translateMatrix = view.getRootTranslateMatrix(); + var rotateMatrix = view.getRootRotateMatrix(); + var targetMatrix = translateMatrix.multiply(rotateMatrix).multiply(magnetMatrix); + var localMatrix = targetMatrix.inverse(); + var localLine = V.transformLine(line, localMatrix); + var localRef = localLine.start.clone(); + var data = view.getNodeData(node); + + if (opt.insideout === false) { + if (!data[BNDR_SHAPE_BBOX]) data[BNDR_SHAPE_BBOX] = localShape.bbox(); + var localBBox = data[BNDR_SHAPE_BBOX]; + if (localBBox.containsPoint(localRef)) return anchor; + } + + // Caching segment subdivisions for paths + var pathOpt + if (localShape instanceof g.Path) { + var precision = opt.precision || 2; + if (!data[BNDR_SUBDIVISIONS]) data[BNDR_SUBDIVISIONS] = localShape.getSegmentSubdivisions({ precision: precision }); + segmentSubdivisions = data[BNDR_SUBDIVISIONS]; + pathOpt = { + precision: precision, + segmentSubdivisions: data[BNDR_SUBDIVISIONS] + } + } + + if (opt.extrapolate === true) localLine.setLength(1e6); + + intersection = localLine.intersect(localShape, pathOpt); + if (intersection) { + // More than one intersection + if (V.isArray(intersection)) intersection = closestIntersection(intersection, localRef); + } else if (opt.sticky === true) { + // No intersection, find the closest point instead + if (localShape instanceof g.Rect) { + intersection = localShape.pointNearestToPoint(localRef); + } else if (localShape instanceof g.Ellipse) { + intersection = localShape.intersectionWithLineFromCenterToPoint(localRef); } else { - textAnchor = 'middle'; + intersection = localShape.closestPoint(localRef, pathOpt); } } - var round = Math.round; - return labelAttributes({ - x: round(tx), - y: round(ty), - angle: orientAngle, - attrs: { - '.': { - y: y, - 'text-anchor': textAnchor - } - } - }); + var cp = (intersection) ? V.transformPoint(intersection, targetMatrix) : anchor; + var cpOffset = opt.offset || 0; + if (opt.stroke) cpOffset += stroke(node) / 2; + + return offset(cp, line.start, cpOffset); } - function getBBoxAngles(elBBox) { + joint.connectionPoints = { + anchor: anchor, + bbox: bboxIntersection, + rectangle: rectangleIntersection, + boundary: boundaryIntersection + } - var center = elBBox.center(); +})(joint, joint.util, g, V); - var tl = center.theta(elBBox.origin()); - var bl = center.theta(elBBox.bottomLeft()); - var br = center.theta(elBBox.corner()); - var tr = center.theta(elBBox.topRight()); +(function(joint, util) { - return [tl, tr, br, bl]; - } + function abs2rel(value, max) { - function insideLayout(portPosition, elBBox, autoOrient, opt) { + if (max === 0) return '0%'; + return Math.round(value / max * 100) + '%'; + } - var angle = elBBox.center().theta(portPosition); - opt = util.defaults({}, opt, { offset: 15 }); + function pin(relative) { - var tx, ty, y, textAnchor; - var offset = opt.offset; - var orientAngle = 0; + return function(end, view, magnet, coords) { - var bBoxAngles = getBBoxAngles(elBBox); + var angle = view.model.angle(); + var bbox = view.getNodeUnrotatedBBox(magnet); + var origin = view.model.getBBox().center(); + coords.rotate(origin, angle); + var dx = coords.x - bbox.x; + var dy = coords.y - bbox.y; - if (angle < bBoxAngles[1] || angle > bBoxAngles[2]) { - y = '.3em'; - tx = -offset; - ty = 0; - textAnchor = 'end'; - } else if (angle < bBoxAngles[0]) { - y = '.6em'; - tx = 0; - ty = offset; - if (autoOrient) { - orientAngle = 90; - textAnchor = 'start'; - } else { - textAnchor = 'middle'; - } - } else if (angle < bBoxAngles[3]) { - y = '.3em'; - tx = offset; - ty = 0; - textAnchor = 'start'; - } else { - y = '0em'; - tx = 0; - ty = -offset; - if (autoOrient) { - orientAngle = -90; - textAnchor = 'start'; - } else { - textAnchor = 'middle'; + if (relative) { + dx = abs2rel(dx, bbox.width); + dy = abs2rel(dy, bbox.height); } - } - var round = Math.round; - return labelAttributes({ - x: round(tx), - y: round(ty), - angle: orientAngle, - attrs: { - '.': { - y: y, - 'text-anchor': textAnchor + end.anchor = { + name: 'topLeft', + args: { + dx: dx, + dy: dy, + rotate: true } - } - }); + }; + + return end; + } } - function radialLayout(portCenterOffset, autoOrient, opt) { + joint.connectionStrategies = { + useDefaults: util.noop, + pinAbsolute: pin(false), + pinRelative: pin(true) + } - opt = util.defaults({}, opt, { offset: 20 }); +})(joint, joint.util); - var origin = g.point(0, 0); - var angle = -portCenterOffset.theta(origin); - var orientAngle = angle; - var offset = portCenterOffset.clone() - .move(origin, opt.offset) - .difference(portCenterOffset) - .round(); +(function(joint, util, V, g) { - var y = '.3em'; - var textAnchor; + function getAnchor(coords, view, magnet) { + // take advantage of an existing logic inside of the + // pin relative connection strategy + var end = joint.connectionStrategies.pinRelative.call( + this.paper, + {}, + view, + magnet, + coords, + this.model + ); + return end.anchor; + } - if ((angle + 90) % 180 === 0) { - textAnchor = autoOrient ? 'end' : 'middle'; - if (!autoOrient && angle === -270) { - y = '0em'; - } - } else if (angle > -270 && angle < -90) { - textAnchor = 'start'; - orientAngle = angle - 180; - } else { - textAnchor = 'end'; + function snapAnchor(coords, view, magnet, type, relatedView, toolView) { + var snapRadius = toolView.options.snapRadius; + var isSource = (type === 'source'); + var refIndex = (isSource ? 0 : -1); + var ref = this.model.vertex(refIndex) || this.getEndAnchor(isSource ? 'target' : 'source'); + if (ref) { + if (Math.abs(ref.x - coords.x) < snapRadius) coords.x = ref.x; + if (Math.abs(ref.y - coords.y) < snapRadius) coords.y = ref.y; } + return coords; + } - var round = Math.round; - return labelAttributes({ - x: round(offset.x), - y: round(offset.y), - angle: autoOrient ? orientAngle : 0, - attrs: { - '.': { - y: y, - 'text-anchor': textAnchor + var ToolView = joint.dia.ToolView; + + // Vertex Handles + var VertexHandle = joint.mvc.View.extend({ + tagName: 'circle', + svgElement: true, + className: 'marker-vertex', + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown', + dblclick: 'onDoubleClick' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + attributes: { + 'r': 6, + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move' + }, + position: function(x, y) { + this.vel.attr({ cx: x, cy: y }); + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + this.options.paper.undelegateEvents(); + this.delegateDocumentEvents(null, evt.data); + this.trigger('will-change'); + }, + onPointerMove: function(evt) { + this.trigger('changing', this, evt); + }, + onDoubleClick: function(evt) { + this.trigger('remove', this, evt); + }, + onPointerUp: function(evt) { + this.trigger('changed', this, evt); + this.undelegateDocumentEvents(); + this.options.paper.delegateEvents(); + } + }); + + var Vertices = ToolView.extend({ + name: 'vertices', + options: { + handleClass: VertexHandle, + snapRadius: 20, + redundancyRemoval: true, + vertexAdding: true, + }, + children: [{ + tagName: 'path', + selector: 'connection', + className: 'joint-vertices-path', + attributes: { + 'fill': 'none', + 'stroke': 'transparent', + 'stroke-width': 10, + 'cursor': 'cell' + } + }], + handles: null, + events: { + 'mousedown .joint-vertices-path': 'onPathPointerDown' + }, + onRender: function() { + this.resetHandles(); + if (this.options.vertexAdding) { + this.renderChildren(); + this.updatePath(); + } + var relatedView = this.relatedView; + var vertices = relatedView.model.vertices(); + for (var i = 0, n = vertices.length; i < n; i++) { + var vertex = vertices[i]; + var handle = new (this.options.handleClass)({ index: i, paper: this.paper }); + handle.render(); + handle.position(vertex.x, vertex.y); + this.simulateRelatedView(handle.el); + handle.vel.appendTo(this.el); + this.handles.push(handle); + this.startHandleListening(handle); + } + return this; + }, + update: function() { + this.render(); + return this; + }, + updatePath: function() { + var connection = this.childNodes.connection; + if (connection) connection.setAttribute('d', this.relatedView.getConnection().serialize()); + }, + startHandleListening: function(handle) { + var relatedView = this.relatedView; + if (relatedView.can('vertexMove')) { + this.listenTo(handle, 'will-change', this.onHandleWillChange); + this.listenTo(handle, 'changing', this.onHandleChanging); + this.listenTo(handle, 'changed', this.onHandleChanged); + } + if (relatedView.can('vertexRemove')) { + this.listenTo(handle, 'remove', this.onHandleRemove); + } + }, + resetHandles: function() { + var handles = this.handles; + this.handles = []; + this.stopListening(); + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + handles[i].remove(); + } + }, + getNeighborPoints: function(index) { + var linkView = this.relatedView; + var vertices = linkView.model.vertices(); + var prev = (index > 0) ? vertices[index - 1] : linkView.sourceAnchor; + var next = (index < vertices.length - 1) ? vertices[index + 1] : linkView.targetAnchor; + return { + prev: new g.Point(prev), + next: new g.Point(next) + } + }, + onHandleWillChange: function(handle, evt) { + this.focus(); + this.relatedView.model.startBatch('vertex-move', { ui: true, tool: this.cid }); + }, + onHandleChanging: function(handle, evt) { + var relatedView = this.relatedView; + var paper = relatedView.paper; + var index = handle.options.index; + var vertex = paper.snapToGrid(evt.clientX, evt.clientY).toJSON(); + this.snapVertex(vertex, index); + relatedView.model.vertex(index, vertex, { ui: true, tool: this.cid }); + handle.position(vertex.x, vertex.y); + }, + snapVertex: function(vertex, index) { + var snapRadius = this.options.snapRadius; + if (snapRadius > 0) { + var neighbors = this.getNeighborPoints(index); + var prev = neighbors.prev; + var next = neighbors.next; + if (Math.abs(vertex.x - prev.x) < snapRadius) { + vertex.x = prev.x; + } else if (Math.abs(vertex.x - next.x) < snapRadius) { + vertex.x = next.x; + } + if (Math.abs(vertex.y - prev.y) < snapRadius) { + vertex.y = neighbors.prev.y; + } else if (Math.abs(vertex.y - next.y) < snapRadius) { + vertex.y = next.y; } } - }); - } - - joint.layout.PortLabel = { + }, + onHandleChanged: function(handle, evt) { + if (this.options.vertexAdding) this.updatePath(); + if (!this.options.redundancyRemoval) return; + var linkView = this.relatedView; + var verticesRemoved = linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + if (verticesRemoved) this.render(); + this.blur(); + linkView.model.stopBatch('vertex-move', { ui: true, tool: this.cid }); + if (this.eventData(evt).vertexAdded) { + linkView.model.stopBatch('vertex-add', { ui: true, tool: this.cid }); + } + }, + onHandleRemove: function(handle) { + var index = handle.options.index; + this.relatedView.model.removeVertex(index, { ui: true }); + }, + onPathPointerDown: function(evt) { + evt.stopPropagation(); + var vertex = this.paper.snapToGrid(evt.clientX, evt.clientY).toJSON(); + var relatedView = this.relatedView; + relatedView.model.startBatch('vertex-add', { ui: true, tool: this.cid }); + var index = relatedView.getVertexIndex(vertex.x, vertex.y); + this.snapVertex(vertex, index); + relatedView.model.insertVertex(index, vertex, { ui: true, tool: this.cid }); + this.render(); + var handle = this.handles[index]; + this.eventData(evt, { vertexAdded: true }); + handle.onPointerDown(evt); + }, + onRemove: function() { + this.resetHandles(); + } + }, { + VertexHandle: VertexHandle // keep as class property + }); - manual: function(portPosition, elBBox, opt) { - return labelAttributes(opt, portPosition) + var SegmentHandle = joint.mvc.View.extend({ + tagName: 'g', + svgElement: true, + className: 'marker-segment', + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + children: [{ + tagName: 'line', + selector: 'line', + attributes: { + 'stroke': '#33334F', + 'stroke-width': 2, + 'fill': 'none', + 'pointer-events': 'none' + } + }, { + tagName: 'rect', + selector: 'handle', + attributes: { + 'width': 20, + 'height': 8, + 'x': -10, + 'y': -4, + 'rx': 4, + 'ry': 4, + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2 + } + }], + onRender: function() { + this.renderChildren(); + }, + position: function(x, y, angle, view) { + + var matrix = V.createSVGMatrix().translate(x, y).rotate(angle); + var handle = this.childNodes.handle; + handle.setAttribute('transform', V.matrixToTransformString(matrix)); + handle.setAttribute('cursor', (angle % 180 === 0) ? 'row-resize' : 'col-resize'); + + var viewPoint = view.getClosestPoint(new g.Point(x, y)); + var line = this.childNodes.line; + line.setAttribute('x1', x); + line.setAttribute('y1', y); + line.setAttribute('x2', viewPoint.x); + line.setAttribute('y2', viewPoint.y); + }, + onPointerDown: function(evt) { + this.trigger('change:start', this, evt); + evt.stopPropagation(); + this.options.paper.undelegateEvents(); + this.delegateDocumentEvents(null, evt.data); + }, + onPointerMove: function(evt) { + this.trigger('changing', this, evt); + }, + onPointerUp: function(evt) { + this.undelegateDocumentEvents(); + this.options.paper.delegateEvents(); + this.trigger('change:end', this, evt); }, + show: function() { + this.el.style.display = ''; + }, + hide: function() { + this.el.style.display = 'none'; + } + }); - left: function(portPosition, elBBox, opt) { - return labelAttributes(opt, { x: -15, attrs: { '.': { y: '.3em', 'text-anchor': 'end' } } }); + var Segments = ToolView.extend({ + name: 'segments', + precision: .5, + options: { + handleClass: SegmentHandle, + segmentLengthThreshold: 40, + redundancyRemoval: true, + anchor: getAnchor, + snapRadius: 10, + snapHandle: true + }, + handles: null, + onRender: function() { + this.resetHandles(); + var relatedView = this.relatedView; + var vertices = relatedView.model.vertices(); + vertices.unshift(relatedView.sourcePoint); + vertices.push(relatedView.targetPoint); + for (var i = 0, n = vertices.length; i < n - 1; i++) { + var vertex = vertices[i]; + var nextVertex = vertices[i + 1]; + var handle = this.renderHandle(vertex, nextVertex); + this.simulateRelatedView(handle.el); + this.handles.push(handle); + handle.options.index = i; + } + return this; + }, + renderHandle: function(vertex, nextVertex) { + var handle = new (this.options.handleClass)({ paper: this.paper }); + handle.render(); + this.updateHandle(handle, vertex, nextVertex); + handle.vel.appendTo(this.el); + this.startHandleListening(handle); + return handle; + }, + update: function() { + this.render(); + return this; + }, + startHandleListening: function(handle) { + this.listenTo(handle, 'change:start', this.onHandleChangeStart); + this.listenTo(handle, 'changing', this.onHandleChanging); + this.listenTo(handle, 'change:end', this.onHandleChangeEnd); + }, + resetHandles: function() { + var handles = this.handles; + this.handles = []; + this.stopListening(); + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + handles[i].remove(); + } + }, + shiftHandleIndexes: function(value) { + var handles = this.handles; + for (var i = 0, n = handles.length; i < n; i++) handles[i].options.index += value; + }, + resetAnchor: function(type, anchor) { + var relatedModel = this.relatedView.model; + if (anchor) { + relatedModel.prop([type, 'anchor'], anchor, { + rewrite: true, + ui: true, + tool: this.cid + }); + } else { + relatedModel.removeProp([type, 'anchor'], { + ui: true, + tool: this.cid + }); + } + }, + snapHandle: function(handle, position, data) { + + var index = handle.options.index; + var linkView = this.relatedView; + var link = linkView.model; + var vertices = link.vertices(); + var axis = handle.options.axis; + var prev = vertices[index - 2] || data.sourceAnchor; + var next = vertices[index + 1] || data.targetAnchor; + var snapRadius = this.options.snapRadius; + if (Math.abs(position[axis] - prev[axis]) < snapRadius) { + position[axis] = prev[axis]; + } else if (Math.abs(position[axis] - next[axis]) < snapRadius) { + position[axis] = next[axis]; + } + return position; }, - right: function(portPosition, elBBox, opt) { - return labelAttributes(opt, { x: 15, attrs: { '.': { y: '.3em', 'text-anchor': 'start' } } }); + onHandleChanging: function(handle, evt) { + + var data = this.eventData(evt); + var relatedView = this.relatedView; + var paper = relatedView.paper; + var index = handle.options.index - 1; + var coords = paper.snapToGrid(evt.clientX, evt.clientY); + var position = this.snapHandle(handle, coords.clone(), data); + var axis = handle.options.axis; + var offset = (this.options.snapHandle) ? 0 : (coords[axis] - position[axis]); + var link = relatedView.model; + var vertices = util.cloneDeep(link.vertices()); + var vertex = vertices[index]; + var nextVertex = vertices[index + 1]; + var anchorFn = this.options.anchor; + if (typeof anchorFn !== 'function') anchorFn = null; + + // First Segment + var sourceView = relatedView.sourceView; + var sourceBBox = relatedView.sourceBBox; + var changeSourceAnchor = false; + var deleteSourceAnchor = false; + if (!vertex) { + vertex = relatedView.sourceAnchor.toJSON(); + vertex[axis] = position[axis]; + if (sourceBBox.containsPoint(vertex)) { + vertex[axis] = position[axis]; + changeSourceAnchor = true; + } else { + // we left the area of the source magnet for the first time + vertices.unshift(vertex); + this.shiftHandleIndexes(1); + delateSourceAnchor = true; + } + } else if (index === 0) { + if (sourceBBox.containsPoint(vertex)) { + vertices.shift(); + this.shiftHandleIndexes(-1); + changeSourceAnchor = true; + } else { + vertex[axis] = position[axis]; + deleteSourceAnchor = true; + } + } else { + vertex[axis] = position[axis]; + } + + if (anchorFn && sourceView) { + if (changeSourceAnchor) { + var sourceAnchorPosition = data.sourceAnchor.clone(); + sourceAnchorPosition[axis] = position[axis]; + var sourceAnchor = anchorFn.call(relatedView, sourceAnchorPosition, sourceView, relatedView.sourceMagnet || sourceView.el, 'source', relatedView); + this.resetAnchor('source', sourceAnchor); + } + if (deleteSourceAnchor) { + this.resetAnchor('source', data.sourceAnchorDef); + } + } + + // Last segment + var targetView = relatedView.targetView; + var targetBBox = relatedView.targetBBox; + var changeTargetAnchor = false; + var deleteTargetAnchor = false; + if (!nextVertex) { + nextVertex = relatedView.targetAnchor.toJSON(); + nextVertex[axis] = position[axis]; + if (targetBBox.containsPoint(nextVertex)) { + changeTargetAnchor = true; + } else { + // we left the area of the target magnet for the first time + vertices.push(nextVertex); + deleteTargetAnchor = true; + } + } else if (index === vertices.length - 2) { + if (targetBBox.containsPoint(nextVertex)) { + vertices.pop(); + changeTargetAnchor = true; + } else { + nextVertex[axis] = position[axis]; + deleteTargetAnchor = true; + } + } else { + nextVertex[axis] = position[axis]; + } + + if (anchorFn && targetView) { + if (changeTargetAnchor) { + var targetAnchorPosition = data.targetAnchor.clone(); + targetAnchorPosition[axis] = position[axis]; + var targetAnchor = anchorFn.call(relatedView, targetAnchorPosition, targetView, relatedView.targetMagnet || targetView.el, 'target', relatedView); + this.resetAnchor('target', targetAnchor); + } + if (deleteTargetAnchor) { + this.resetAnchor('target', data.targetAnchorDef); + } + } + + link.vertices(vertices, { ui: true, tool: this.cid }); + this.updateHandle(handle, vertex, nextVertex, offset); + }, + onHandleChangeStart: function(handle, evt) { + var index = handle.options.index; + var handles = this.handles; + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + if (i !== index) handles[i].hide() + } + this.focus(); + var relatedView = this.relatedView; + var relatedModel = relatedView.model; + this.eventData(evt, { + sourceAnchor: relatedView.sourceAnchor.clone(), + targetAnchor: relatedView.targetAnchor.clone(), + sourceAnchorDef: util.clone(relatedModel.prop(['source', 'anchor'])), + targetAnchorDef: util.clone(relatedModel.prop(['target', 'anchor'])) + }); + relatedView.model.startBatch('segment-move', { ui: true, tool: this.cid }); + }, + onHandleChangeEnd: function(handle) { + var linkView = this.relatedView; + if (this.options.redundancyRemoval) { + linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + } + this.render(); + this.blur(); + linkView.model.stopBatch('segment-move', { ui: true, tool: this.cid }); }, - - top: function(portPosition, elBBox, opt) { - return labelAttributes(opt, { y: -15, attrs: { '.': { 'text-anchor': 'middle' } } }); + updateHandle: function(handle, vertex, nextVertex, offset) { + var vertical = Math.abs(vertex.x - nextVertex.x) < this.precision; + var horizontal = Math.abs(vertex.y - nextVertex.y) < this.precision; + if (vertical || horizontal) { + var segmentLine = new g.Line(vertex, nextVertex); + var length = segmentLine.length(); + if (length < this.options.segmentLengthThreshold) { + handle.hide(); + } else { + var position = segmentLine.midpoint() + var axis = (vertical) ? 'x' : 'y'; + position[axis] += offset || 0; + var angle = segmentLine.vector().vectorAngle(new g.Point(1, 0)); + handle.position(position.x, position.y, angle, this.relatedView); + handle.show(); + handle.options.axis = axis; + } + } else { + handle.hide(); + } }, + onRemove: function() { + this.resetHandles(); + } + }, { + SegmentHandle: SegmentHandle // keep as class property + }); - bottom: function(portPosition, elBBox, opt) { - return labelAttributes(opt, { y: 15, attrs: { '.': { y: '.6em', 'text-anchor': 'middle' } } }); + // End Markers + var Arrowhead = ToolView.extend({ + tagName: 'path', + xAxisVector: new g.Point(1, 0), + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown' }, - - outsideOriented: function(portPosition, elBBox, opt) { - return outsideLayout(portPosition, elBBox, true, opt); + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' }, - - outside: function(portPosition, elBBox, opt) { - return outsideLayout(portPosition, elBBox, false, opt); + onRender: function() { + this.update() }, - - insideOriented: function(portPosition, elBBox, opt) { - return insideLayout(portPosition, elBBox, true, opt); + update: function() { + var ratio = this.ratio; + var view = this.relatedView; + var tangent = view.getTangentAtRatio(ratio); + var position, angle; + if (tangent) { + position = tangent.start; + angle = tangent.vector().vectorAngle(this.xAxisVector) || 0; + } else { + position = view.getPointAtRatio(ratio); + angle = 0; + } + var matrix = V.createSVGMatrix().translate(position.x, position.y).rotate(angle); + this.vel.transform(matrix, { absolute: true }); + return this; }, - - inside: function(portPosition, elBBox, opt) { - return insideLayout(portPosition, elBBox, false, opt); + onPointerDown: function(evt) { + evt.stopPropagation(); + var relatedView = this.relatedView; + relatedView.model.startBatch('arrowhead-move', { ui: true, tool: this.cid }); + if (relatedView.can('arrowheadMove')) { + relatedView.startArrowheadMove(this.arrowheadType); + this.delegateDocumentEvents(); + relatedView.paper.undelegateEvents(); + } + this.focus(); + this.el.style.pointerEvents = 'none'; }, - - radial: function(portPosition, elBBox, opt) { - return radialLayout(portPosition.difference(elBBox.center()), false, opt); + onPointerMove: function(evt) { + var coords = this.paper.snapToGrid(evt.clientX, evt.clientY); + this.relatedView.pointermove(evt, coords.x, coords.y); }, - - radialOriented: function(portPosition, elBBox, opt) { - return radialLayout(portPosition.difference(elBBox.center()), true, opt); + onPointerUp: function(evt) { + this.undelegateDocumentEvents(); + var relatedView = this.relatedView; + var paper = relatedView.paper; + var coords = paper.snapToGrid(evt.clientX, evt.clientY); + relatedView.pointerup(evt, coords.x, coords.y); + paper.delegateEvents(); + this.blur(); + this.el.style.pointerEvents = ''; + relatedView.model.stopBatch('arrowhead-move', { ui: true, tool: this.cid }); } - }; - -})(_, g, joint, joint.util); - -joint.highlighters.addClass = { - - className: joint.util.addClassNamePrefix('highlighted'), - - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - highlight: function(cellView, magnetEl, opt) { - - var options = opt || {}; - var className = options.className || this.className; - V(magnetEl).addClass(className); - }, - - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - unhighlight: function(cellView, magnetEl, opt) { - - var options = opt || {}; - var className = options.className || this.className; - V(magnetEl).removeClass(className); - } -}; - -joint.highlighters.opacity = { - - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - */ - highlight: function(cellView, magnetEl) { - - V(magnetEl).addClass(joint.util.addClassNamePrefix('highlight-opacity')); - }, - - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - */ - unhighlight: function(cellView, magnetEl) { - - V(magnetEl).removeClass(joint.util.addClassNamePrefix('highlight-opacity')); - } -}; - -joint.highlighters.stroke = { + }); - defaultOptions: { - padding: 3, - rx: 0, - ry: 0, - attrs: { - 'stroke-width': 3, - stroke: '#FEB663' + var TargetArrowhead = Arrowhead.extend({ + name: 'target-arrowhead', + ratio: 1, + arrowheadType: 'target', + attributes: { + 'd': 'M -10 -8 10 0 -10 8 Z', + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move', + 'class': 'target-arrowhead' } - }, - - _views: {}, - - getHighlighterId: function(magnetEl, opt) { - - return magnetEl.id + JSON.stringify(opt); - }, + }); - removeHighlighter: function(id) { - if (this._views[id]) { - this._views[id].remove(); - this._views[id] = null; + var SourceArrowhead = Arrowhead.extend({ + name: 'source-arrowhead', + ratio: 0, + arrowheadType: 'source', + attributes: { + 'd': 'M 10 -8 -10 0 10 8 Z', + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move', + 'class': 'source-arrowhead' } - }, - - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - highlight: function(cellView, magnetEl, opt) { - - var id = this.getHighlighterId(magnetEl, opt); - - // Only highlight once. - if (this._views[id]) return; - - var options = joint.util.defaults(opt || {}, this.defaultOptions); - - var magnetVel = V(magnetEl); - var magnetBBox; - - try { - - var pathData = magnetVel.convertToPathData(); - - } catch (error) { + }); - // Failed to get path data from magnet element. - // Draw a rectangle around the entire cell view instead. - magnetBBox = magnetVel.bbox(true/* without transforms */); - pathData = V.rectToPath(joint.util.assign({}, options, magnetBBox)); + var Button = ToolView.extend({ + name: 'button', + events: { + 'mousedown': 'onPointerDown', + 'touchstart': 'onPointerDown' + }, + options: { + distance: 0, + offset: 0, + rotate: false + }, + onRender: function() { + this.renderChildren(this.options.markup); + this.update() + }, + update: function() { + var tangent, position, angle; + var distance = this.options.distance || 0; + if (util.isPercentage(distance)) { + tangent = this.relatedView.getTangentAtRatio(parseFloat(distance) / 100); + } else { + tangent = this.relatedView.getTangentAtLength(distance) + } + if (tangent) { + position = tangent.start; + angle = tangent.vector().vectorAngle(new g.Point(1,0)) || 0; + } else { + position = this.relatedView.getConnection().start; + angle = 0; + } + var matrix = V.createSVGMatrix() + .translate(position.x, position.y) + .rotate(angle) + .translate(0, this.options.offset || 0); + if (!this.options.rotate) matrix = matrix.rotate(-angle); + this.vel.transform(matrix, { absolute: true }); + return this; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + var actionFn = this.options.action; + if (typeof actionFn === 'function') { + actionFn.call(this.relatedView, evt, this.relatedView); + } } + }); - var highlightVel = V('path').attr({ - d: pathData, - 'pointer-events': 'none', - 'vector-effect': 'non-scaling-stroke', - 'fill': 'none' - }).attr(options.attrs); - - var highlightMatrix = magnetVel.getTransformToElement(cellView.el); - // Add padding to the highlight element. - var padding = options.padding; - if (padding) { + var Remove = Button.extend({ + children: [{ + tagName: 'circle', + selector: 'button', + attributes: { + 'r': 7, + 'fill': '#FF1D00', + 'cursor': 'pointer' + } + }, { + tagName: 'path', + selector: 'icon', + attributes: { + 'd': 'M -3 -3 3 3 M -3 3 3 -3', + 'fill': 'none', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'pointer-events': 'none' + } + }], + options: { + distance: 60, + offset: 0, + action: function(evt) { + this.model.remove({ ui: true, tool: this.cid }); + } + } + }); - magnetBBox || (magnetBBox = magnetVel.bbox(true)); + var Boundary = ToolView.extend({ + name: 'boundary', + tagName: 'rect', + options: { + padding: 10 + }, + attributes: { + 'fill': 'none', + 'stroke': '#33334F', + 'stroke-width': .5, + 'stroke-dasharray': '5, 5', + 'pointer-events': 'none' + }, + onRender: function() { + this.update(); + }, + update: function() { + var padding = this.options.padding; + if (!isFinite(padding)) padding = 0; + var bbox = this.relatedView.getConnection().bbox().inflate(padding); + this.vel.attr(bbox.toJSON()); + return this; + } + }); - var cx = magnetBBox.x + (magnetBBox.width / 2); - var cy = magnetBBox.y + (magnetBBox.height / 2); + var Anchor = ToolView.extend({ + tagName: 'g', + type: null, + children: [{ + tagName: 'circle', + selector: 'anchor', + attributes: { + 'cursor': 'pointer' + } + }, { + tagName: 'rect', + selector: 'area', + attributes: { + 'pointer-events': 'none', + 'fill': 'none', + 'stroke': '#33334F', + 'stroke-dasharray': '2,4', + 'rx': 5, + 'ry': 5 + } + }], + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown', + dblclick: 'onPointerDblClick' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + options: { + snap: snapAnchor, + anchor: getAnchor, + customAnchorAttributes: { + 'stroke-width': 4, + 'stroke': '#33334F', + 'fill': '#FFFFFF', + 'r': 5 + }, + defaultAnchorAttributes: { + 'stroke-width': 2, + 'stroke': '#FFFFFF', + 'fill': '#33334F', + 'r': 6 + }, + areaPadding: 6, + snapRadius: 10, + restrictArea: true, + redundancyRemoval: true + }, + onRender: function() { + this.renderChildren(); + this.toggleArea(false); + this.update(); + }, + update: function() { + var type = this.type; + var relatedView = this.relatedView; + var view = relatedView.getEndView(type); + if (view) { + this.updateAnchor(); + this.updateArea(); + this.el.style.display = ''; + } else { + this.el.style.display = 'none'; + } + return this; + }, + updateAnchor: function() { + var childNodes = this.childNodes; + if (!childNodes) return; + var anchorNode = childNodes.anchor; + if (!anchorNode) return; + var relatedView = this.relatedView; + var type = this.type; + var position = relatedView.getEndAnchor(type); + var options = this.options; + var customAnchor = relatedView.model.prop([type, 'anchor']); + anchorNode.setAttribute('transform', 'translate(' + position.x + ',' + position.y + ')'); + var anchorAttributes = (customAnchor) ? options.customAnchorAttributes : options.defaultAnchorAttributes; + for (var attrName in anchorAttributes) { + anchorNode.setAttribute(attrName, anchorAttributes[attrName]); + } + }, + updateArea: function() { + var childNodes = this.childNodes; + if (!childNodes) return; + var areaNode = childNodes.area; + if (!areaNode) return; + var relatedView = this.relatedView; + var type = this.type; + var view = relatedView.getEndView(type); + var magnet = relatedView.getEndMagnet(type); + var padding = this.options.areaPadding; + if (!isFinite(padding)) padding = 0; + var bbox = view.getNodeUnrotatedBBox(magnet).inflate(padding); + var angle = view.model.angle(); + areaNode.setAttribute('x', -bbox.width / 2); + areaNode.setAttribute('y', -bbox.height / 2); + areaNode.setAttribute('width', bbox.width); + areaNode.setAttribute('height', bbox.height); + var origin = view.model.getBBox().center(); + var center = bbox.center().rotate(origin, -angle) + areaNode.setAttribute('transform', 'translate(' + center.x + ',' + center.y + ') rotate(' + angle +')'); + }, + toggleArea: function(visible) { + this.childNodes.area.style.display = (visible) ? '' : 'none'; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + this.paper.undelegateEvents(); + this.delegateDocumentEvents(); + this.focus(); + this.toggleArea(this.options.restrictArea); + this.relatedView.model.startBatch('anchor-move', { ui: true, tool: this.cid }); + }, + resetAnchor: function(anchor) { + var type = this.type; + var relatedModel = this.relatedView.model; + if (anchor) { + relatedModel.prop([type, 'anchor'], anchor, { + rewrite: true, + ui: true, + tool: this.cid + }); + } else { + relatedModel.removeProp([type, 'anchor'], { + ui: true, + tool: this.cid + }); + } + }, + onPointerMove: function(evt) { + + var relatedView = this.relatedView; + var type = this.type; + var view = relatedView.getEndView(type); + var magnet = relatedView.getEndMagnet(type); + + var coords = this.paper.clientToLocalPoint(evt.clientX, evt.clientY); + var snapFn = this.options.snap; + if (typeof snapFn === 'function') { + coords = snapFn.call(relatedView, coords, view, magnet, type, relatedView, this); + coords = new g.Point(coords); + } + + if (this.options.restrictArea) { + // snap coords within node bbox + var bbox = view.getNodeUnrotatedBBox(magnet); + var angle = view.model.angle(); + var origin = view.model.getBBox().center(); + var rotatedCoords = coords.clone().rotate(origin, angle); + if (!bbox.containsPoint(rotatedCoords)) { + coords = bbox.pointNearestToPoint(rotatedCoords).rotate(origin, -angle); + } + } - magnetBBox = V.transformRect(magnetBBox, highlightMatrix); + var anchor; + var anchorFn = this.options.anchor; + if (typeof anchorFn === 'function') { + anchor = anchorFn.call(relatedView, coords, view, magnet, type, relatedView); + } - var width = Math.max(magnetBBox.width, 1); - var height = Math.max(magnetBBox.height, 1); - var sx = (width + padding) / width; - var sy = (height + padding) / height; + this.resetAnchor(anchor); + this.update(); + }, - var paddingMatrix = V.createSVGMatrix({ - a: sx, - b: 0, - c: 0, - d: sy, - e: cx - sx * cx, - f: cy - sy * cy - }); + onPointerUp: function(evt) { + this.paper.delegateEvents(); + this.undelegateDocumentEvents(); + this.blur(); + this.toggleArea(false); + var linkView = this.relatedView; + if (this.options.redundancyRemoval) linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + linkView.model.stopBatch('anchor-move', { ui: true, tool: this.cid }); + }, - highlightMatrix = highlightMatrix.multiply(paddingMatrix); + onPointerDblClick: function() { + this.resetAnchor(); + this.update(); } + }); - highlightVel.transform(highlightMatrix); - - // joint.mvc.View will handle the theme class name and joint class name prefix. - var highlightView = this._views[id] = new joint.mvc.View({ - svgElement: true, - className: 'highlight-stroke', - el: highlightVel.node - }); - - // Remove the highlight view when the cell is removed from the graph. - var removeHandler = this.removeHighlighter.bind(this, id); - var cell = cellView.model; - highlightView.listenTo(cell, 'remove', removeHandler); - highlightView.listenTo(cell.graph, 'reset', removeHandler); + var SourceAnchor = Anchor.extend({ + name: 'source-anchor', + type: 'source' + }); - cellView.vel.append(highlightVel); - }, + var TargetAnchor = Anchor.extend({ + name: 'target-anchor', + type: 'target' + }); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - unhighlight: function(cellView, magnetEl, opt) { + // Export + joint.linkTools = { + Vertices: Vertices, + Segments: Segments, + SourceArrowhead: SourceArrowhead, + TargetArrowhead: TargetArrowhead, + SourceAnchor: SourceAnchor, + TargetAnchor: TargetAnchor, + Button: Button, + Remove: Remove, + Boundary: Boundary + }; - this.removeHighlighter(this.getHighlighterId(magnetEl, opt)); - } -}; +})(joint, joint.util, V, g); joint.g = g; diff --git a/dist/joint.core.min.css b/dist/joint.core.min.css index 512c69e07..20aae5e7e 100644 --- a/dist/joint.core.min.css +++ b/dist/joint.core.min.css @@ -1,8 +1,8 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - 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/. */ -.joint-element *,.marker-source,.marker-target{vector-effect:non-scaling-stroke}.joint-viewport{-webkit-user-select:none;-moz-user-select:none;user-select:none}.joint-paper-background,.joint-paper-grid,.joint-paper>svg{position:absolute;top:0;left:0;right:0;bottom:0}[magnet=true]:not(.joint-element){cursor:crosshair}.marker-arrowheads,.marker-vertices{cursor:move;opacity:0}[magnet=true]:not(.joint-element):hover{opacity:.7}.joint-element{cursor:move}.joint-element *{user-drag:none}.joint-paper{position:relative}.joint-highlight-opacity{opacity:.3}.joint-link .connection,.joint-link .connection-wrap{fill:none}.marker-arrowheads{cursor:-webkit-grab;cursor:-moz-grab}.link-tools{opacity:0;cursor:pointer}.link-tools .tool-options{display:none}.joint-link:hover .link-tools,.joint-link:hover .marker-arrowheads,.joint-link:hover .marker-vertices{opacity:1}.marker-vertex-remove{cursor:pointer;opacity:.1}.marker-vertex-group:hover .marker-vertex-remove{opacity:1}.marker-vertex-remove-area{opacity:.1;cursor:pointer}.marker-vertex-group:hover .marker-vertex-remove-area{opacity:1}.joint-element .fobj{overflow:hidden}.joint-element .fobj body{background-color:transparent;margin:0;position:static}.joint-element .fobj div{text-align:center;vertical-align:middle;display:table-cell;padding:0 5px}.joint-paper.joint-theme-dark{background-color:#18191b}.joint-link.joint-theme-dark .connection-wrap{stroke:#8F8FF3;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-dark .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-dark .connection{stroke-linejoin:round}.joint-link.joint-theme-dark .link-tools .tool-remove circle{fill:#F33636}.joint-link.joint-theme-dark .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-dark .link-tools [event="link:options"] circle{fill:green}.joint-link.joint-theme-dark .marker-vertex{fill:#5652DB}.joint-link.joint-theme-dark .marker-vertex:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-arrowhead{fill:#5652DB}.joint-link.joint-theme-dark .marker-arrowhead:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-vertex-remove-area{fill:green;stroke:#006400}.joint-link.joint-theme-dark .marker-vertex-remove{fill:#fff;stroke:#fff}.joint-paper.joint-theme-default{background-color:#FFF}.joint-link.joint-theme-default .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-default .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-default .connection{stroke-linejoin:round}.joint-link.joint-theme-default .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-default .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-default .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-default .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-default .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-default .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-default .marker-vertex-remove{fill:#FFF}@font-face{font-family:lato-light;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAHhgABMAAAAA3HwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcaLe9KEdERUYAAAHEAAAAHgAAACABFgAER1BPUwAAAeQAAAo1AAARwtKX0BJHU1VCAAAMHAAAACwAAAAwuP+4/k9TLzIAAAxIAAAAWQAAAGDX0nerY21hcAAADKQAAAGJAAAB4hcJdWJjdnQgAAAOMAAAADoAAAA6DvoItmZwZ20AAA5sAAABsQAAAmVTtC+nZ2FzcAAAECAAAAAIAAAACAAAABBnbHlmAAAQKAAAXMoAAK3EsE/AsWhlYWQAAGz0AAAAMgAAADYOCCHIaGhlYQAAbSgAAAAgAAAAJA9hCBNobXR4AABtSAAAAkEAAAOkn9Zh6WxvY2EAAG+MAAAByAAAAdTkvg14bWF4cAAAcVQAAAAgAAAAIAIGAetuYW1lAABxdAAABDAAAAxGYqFiYXBvc3QAAHWkAAAB7wAAAtpTFoINcHJlcAAAd5QAAADBAAABOUVnCXh3ZWJmAAB4WAAAAAYAAAAGuclXKQAAAAEAAAAAzD2izwAAAADJKrAQAAAAANNPakh42mNgZGBg4ANiCQYQYGJgBMIXQMwC5jEAAA5CARsAAHjafddrjFTlHcfxP+KCAl1XbKLhRWnqUmpp1Yba4GXV1ktXK21dby0erZumiWmFZLuNMaQQElgWJ00mtNxRQMXLcntz3GUIjsYcNiEmE5PNhoFl2GQgzKvJvOnLJk4/M4DiGzL57v/szJzn/P6/53ee80zMiIg5cXc8GNc9+vhTz0bna/3/WBUL4nrvR7MZrc+vPp7xt7/8fVXc0Dpqc31c1643xIyu/e1vvhpTMTWjHlPX/XXmbXi3o7tjbNY/O7pnvTv7ldm7bvh9R/eNKzq658Sc385+Zea7c9+avWvens7bZtQ7xjq/uOl6r+fVLZ1fXP5vuqur6983benqao0587aO7tbf9tHYN6/W+N+8XKf9mreno7s1zpVXe7z26+rjS695e2be1hq3pfvS39b/7XcejTnNvuhqdsTNzZ6Yr97i/+7ml7FIXawuwVLcg/tiWdyPHi4+rD7W/Dx+3RyJXjyBZ/AcVhlrNdZivXE2YAgbMYxNeBM5Y27FNmzHDuzEbuxzjfeMvx/v4wN8iI8wggOucxCHcBhHkGIUYziKAo7hODJjnlDHjXuKrjKm9HsO046rOI+Fui/rvKzzss7LOi/rsqbLmi5ruqzpskZ9mfoy9WXqy9SXqS9TX6auRl2Nuhp1Nepq1NWoq1FXo65GXY26GnU16srU1WJJzKJnLjrbczJIzTg149SMUzNOzXgsa/bGfbi/mY+e5uvxsOMVzXXxYrMUL6krnbvKuYPqanWNulbNOXcrtmE7dmAndmOfcTJ1XD3lu2Wcdt4ZnEWl7dMgnwb5NBgX/f8DanskqEJxD8U9kjQoRYNSVJGgymWlWyitxQPNk9Qm8WBzkuItVPZQ2ENdKyUVKalISUVKKlJSkZKKlFQoS6hKqOmhpjVrgxT1UNRj9lpKeuKVmCWPc5p7Y67aia7mI/zbQs0j1OyN7zVHYyFul97u5gR1e/k6wdeJuLP5Gm8neDsh05vN9mazvdlsb44nm9X4TfONeNq5fXjGe8+qz6nPqy80t8cfqPyj4xXN6Ugcv6S+3CzESjpW0TCovuHz1Y7XOF6rrnf9DRjCRgxjE95Ejo6t2Ibt2IGd2I33XHc/3scH+BAfYQQHcBCHcBhHkOJj1x5Vx3AUBRzDcXzisyI+xWfIXOOE90/RWMZpes9gio9nVXPK9UdkYYssbJGFLXHRe92y8KUZqMrCl/Edee5UuyRqPm7x/iIsaw7Jw4QsVGXhiCyksjARv/T9fqx0ziDWYL3vbMAQNmIYm/Am9jl3HKd97wymXOOsWsE5xxfVn1HUR00fJX2yUInbvdvt7MVYgju9lqr3tJXl4l5n3sf/+5sZdQOU7TWnBfNpLo2xyhiD6mp1jbpWzTl3K7ZhO3ZgJ3bjLeO9jT3Y277HBvhbpXyAvxX+VnTQp4M+6vuo7+Nrha8VvlZ00Rc3Ut7vyv2u2u+K/c7sd2a/b/b7Zr9v9sddnM9xu5fbvdzOyXsm75m8L+R8TsbvkOtUrlO5TuU5k+dMnlN5zuQ5ledMjjNZzbif436O+znu57if436O+zk5S+UslbNUzlI5S+UslbNMzlI5S+UslbNUzlI5S+Usk7NMzjI5y2QsNWu9ZqvX/TqHO11Wr/m4xfEirMcGDGEjhrEJb2LK987hp9w5+a05vTKfv25e0OsFvV5wD0/o84IeL7hXC+Z03Fo5bl7HOXuSsyc5e/Kac3nAuQdxCIdxBClGMYajKOAYjqM1zyfUU8YtYxpVnMevYtZXEzEXneiKe3SxMOart+upW64XYwmW4h4sa74gmX2S+bpkLpPMPh1O63Bah9O6m9bdtM7e0dkRnb0TK429yriD6mp1jbpWzfl8K7ZhO3ZgJ3Zjn7EPGOcgDuEwjiDFKMZwFAUcw3Fkzjuhjjv3lPHLOO1aZzClp7NqBeccT/usivO46L07zPywmb/VzN9q5ofN/LCs9lmHSzqs6rCqw6oOqzqsSsWwVAxLxbBUDEvFsFQMS8WwtbFkbSxZG0vWxpK1sWRtLFkbS7qq6qqqq6quqrqq6qqqq6quqrqq6qqqq6quWnNXlbJbpYwuczJpTibNyaQ5mTQnk+ZkwopR5eckPyf5OcnPSX5O8nOSn5NWgKoVoGoFqFoBqryajGe+vldv/tb9mrhfE1caat+vi9UluLO51BWHXHEoHvvqfzzp5kk3T7o9l+51Hyfu44Q/3e7jhEfd7uPEc+kh93IiEb0SMeC59Gep6PVcGpKKXvd4IhW9EtF7zXs95/tbsQ3bsQM7sRvv0bMf7+MDfIiPMIIDdBzEIRzGEaT42HVH1TEcRQHHcByf+KyIT/EZMtc44f1TNJZxZb2YRhXn8fDlJ3/xqid/nrM1zuY5W7QC/pCjRU7ul6pRDtY5WOdgnYO7OVfnWp1jZy4/sWvtJ/Zq9dLTusahIoeKHCpyqMihIoeKHCpK3ajUjUrdqNSNSt2o1I1K3SgX6lyoc6HOhToX6lyoc6DOgToH6hyoc6DOgbpu67qt6bZ21ZM3f9WTN6/7mu5ruq+1n7wvc2ABBwY4sIADCzjwOgcSDrzOgQHZystWvu1Ea3VZ5L0rK8ylfF1aZS7tfRLuJNxJuPOCfOXlK8+lRL7ynErkK8+tf8lXXr52ydeIfK2Tr10cXMDBhIMLZCzPxYSLC7iYcHGAiwNcHODiABcHuDjAxYFrrkrX3vMkHE44nHA44XDC4UTO8lxOuJxwOeFywuWEy4mc5eUsL2d5OctfXsESziect9Ok9wym+HdWreCc42mfVXEeF733Ey6nl10tcLTA0QI3C9wscLLEyRInS9wrca7EtTLHJjjVWptT7qScSXVf0H1B9wXdF3Rf0H1B9wUdlnRY0mFJhyUdlnRY0l1JdyXdlXRX0l1JdyXdFHRT0k2qm5TqlOqU6lQ6ZrXuFHRihQS92PwvNTX7m6K9TdG+pmhPUrQnKdqTFO1JivYhxfiuM0ecOWJvV3P2iOfRZs+jumfRZvu3mtEaUpAZrWEv1xpxxIgjRhwx4ogRR4w4YsQRI47ETXK7XGaXU7W8ndlWXlc6HsQanMYZXJqH5eZheXseLqrz+ZvxN+NvaxfT2sFkvMp4lfEq41XGq4xXrV1JxquMVxmvMl5lvGrtQrKY59rrXHtd+5lzrWfIlO+cw/fdbYWvz7rF8aL2fDfoadDToKdBT0PiCxJfkPiCxBckviDxBYlvzWuD1gatDVobtDZobdDaoLVBa4PWBq0NWhu0Nr5WcP3Xu6UrO6EZ8So/5+qm047iZv54asWiWBw/ih/b594Vd8fS+Lln8C+sGff6LX9/POC30IPxkDX0sXg8nogn46n4XTwdfZ5Rz8bzsSJejCReij+ZlVUxYF5Wm5e1sT42xFBsDE/eyMV/Ymtsi+2xI3bGW/F27Im9fr2/E+/F/ng/PogP46PwWz0OxeE4Eh/HaIzF0SjEsTgen8cJv8hPRdlcn7FbOGuOz8V0VON8XPw/fppwigAAAHjaY2BkYGDgYtBh0GNgcnHzCWHgy0ksyWOQYGABijP8/w8kECwgAACeygdreNpjYGYRZtRhYGVgYZ3FaszAwCgPoZkvMrgxMXAwM/EzMzExsTAzMTcwMKx3YEjwYoCCksoAHyDF+5uJrfBfIQMDuwbjUgWgASA55t+sK4GUAgMTABvCDMIAAAB42mNgYGBmgGAZBkYGELgD5DGC+SwMB4C0DoMCkMUDZPEy1DH8ZwxmrGA6xnRHgUtBREFKQU5BSUFNQV/BSiFeYY2ikuqf30z//4PN4QXqW8AYBFXNoCCgIKEgA1VtCVfNCFTN/P/r/yf/D/8v/O/7j+Hv6wcnHhx+cODB/gd7Hux8sPHBigctDyzuH771ivUZ1IVEA0Y2iNfAbCYgwYSugIGBhZWNnYOTi5uHl49fQFBIWERUTFxCUkpaRlZOXkFRSVlFVU1dQ1NLW0dXT9/A0MjYxNTM3MLSytrG1s7ewdHJ2cXVzd3D08vbx9fPPyAwKDgkNCw8IjIqOiY2Lj4hMYmhvaOrZ8rM+UsWL12+bMWqNavXrtuwfuOmLdu2bt+5Y++effsZilPTsu5VLirMeVqezdA5m6GEgSGjAuy63FqGlbubUvJB7Ly6+8nNbTMOH7l2/fadGzd3MRw6yvDk4aPnLxiqbt1laO1t6eueMHFS/7TpDFPnzpvDcOx4EVBTNRADAEXYio8AAAAAAAP7BakAVwA+AEMASQBNAFEAUwBbAF8AtABhAEgATQBVAFsAYQBoAGwAtQBPAEAAZQBZADsAYwURAAB42l1Ru05bQRDdDQ8DgcTYIDnaFLOZkMZ7oQUJxNWNYmQ7heUIaTdykYtxAR9AgUQN2q8ZoKGkSJsGIRdIfEI+IRIza4iiNDs7s3POmTNLypGqd+lrz1PnJJDC3QbNNv1OSLWzAPek6+uNjLSDB1psZvTKdfv+Cwab0ZQ7agDlPW8pDxlNO4FatKf+0fwKhvv8H/M7GLQ00/TUOgnpIQTmm3FLg+8ZzbrLD/qC1eFiMDCkmKbiLj+mUv63NOdqy7C1kdG8gzMR+ck0QFNrbQSa/tQh1fNxFEuQy6axNpiYsv4kE8GFyXRVU7XM+NrBXbKz6GCDKs2BB9jDVnkMHg4PJhTStyTKLA0R9mKrxAgRkxwKOeXcyf6kQPlIEsa8SUo744a1BsaR18CgNk+z/zybTW1vHcL4WRzBd78ZSzr4yIbaGBFiO2IpgAlEQkZV+YYaz70sBuRS+89AlIDl8Y9/nQi07thEPJe1dQ4xVgh6ftvc8suKu1a5zotCd2+qaqjSKc37Xs6+xwOeHgvDQWPBm8/7/kqB+jwsrjRoDgRDejd6/6K16oirvBc+sifTv7FaAAAAAAEAAf//AA942sR9B2Ab15H2vl0sOha76ABJgCgESIIESIAECPYqik2kSFEiqS5Rnaq2bMndlnvNJU7c27nKjpNdkO7lZPtK2uXSLOfuklxyyd0f3O9c7DgXRxIJ/fPeAiRFSy73N9kktoDYeTPzZr6ZN29A0VQnRdGT7CjFUCoqIiEq2phWKdjfxSQl+7PGNEPDISUx+DKLL6dVysLZxjTC1+OCVyjxCt5OujgbQPdmd7Kjp5/rVPw9BR9JvX/2Q3ScPU4JlIdaQaWNFBWWWH0mbaapMBKLoyJ1UtJaM/hn2qql1GHJZMiIpqhYEJescOSKSV4UlqwmwSQZ2VSKksysYBJdqarqZE0zHY+5aauFo/2+oFmIC3Ck8keY9zmnz2r2u4xGl99cmohtpBkl0wE/9GD+qsXn4hJMHd0792JkeHRDKrVhdBjT+zLzOp0AerWUlaqiYIBUWNTHZ1R6SqMIi6YYEm2EZobPiAwv6YA2js9IdhSmqqoxCSoOATGhkoXDl0c1NGfieBp5ckeM4ioUzr77kGCxCA/NHxF+jVGUYjU8P0HVoyEqHQN+iSXxtBHokHhzPD5To4gZDeFp1pOsC9jjUo0yMx2oqIwH7LEZrYrcUrpT9fiWFm7pBJMTbiGxISqWnZRKjJl0SZk2PN1a4tPAB/OSGQZgM2akRhQWE65Xmx/7ww8pa1grxiKcqD8hRdSnWJE/8WrzbX+YItdNcB3+LIyvm3jJqT4lxvhpNqY3w4PJbx3+LUb4aSHCm/Ezpt0lTrjuIb8D+LcY5qcrwib5bZXkbfAh8fwfJskVeE8dfs90Kv/OenydodL6cAT+oVYrq9TpeRih2xMIV1RGYvFkXao+cr5/YqsLy6cRtaC42ZtM2OPmZtSAGK85HrNaVExcpQz5GThWeRmQWW1N0uxlOBRGZjgr8Zq9YzTzL6uyc0pF+T+NK5ym8GZUvTlcjMb/XcmWvbHqf3jY7H9tKufMaCz7D2OsUwhveo0TUAJVr8r+A/oNq9Xy6K6QD6GHzZZsA/obj1qR3Q7n2YOuymy9IKgU6L7sVrsJ/a2hHt1FwSx8MHtK4VceoxqoZdRK6m+ptBVrIkyKdk1GDIJAh6Mif1JqFDJiIy/VgRRrOBB3TZ06PLOSo4pBWUMxsYaX+uFWRMhII7KAW/5j9hksSIUYAkm6Tkht7CnRdoKdtrbZgMshfrog5AKmB/FvsY2fbsfXGWra5gq1Eba/aLW5CoJt7QuclRpBCKIyJenq4FWbklbWwGt3SuwXRH9KjJgkrxtmblV1C0rAhFXYzRGmFiZvC8IyULmRXaX0+yJ0iHGzeDIbEeZ8MoLMFjdtN3MMaob3w/0HC/SCpjBU2z2R8i67fkdr7c57tmiQ0Vii3/Fgm13L68taN3a4q7aM99cVN+5/fKceGQ0l+mPvjFau2J4qWnHxihBKDl+zprJm9f7m50uNNl9pwMXQt9lqR46u7z62s4X5Omf+vmqg1S94y4Ls3EtGX1nt8g1NYw9e0s3+1GD+s3KS+X3L2taIha5VVA9sOfPXbN3aI12d69srzBTFUuNnf89+m32FMlMhsB2dMJe/TKVLYQanW7HZ62Uz6QqQYprFk9nPZmZWJVpZQ1haBYdOIzl0shkkjhMLYzFmRAsvuUF+WjjU8lI1HHbBYRcvDcJhA0zbCXh1WwRT2siWplIpabALjhOtlSlsKVf1gtFsqIbLficcaakUWE3zOVYzQieBx/FYM40Z7PdxtJkIBSn96DPeOB4dPtDSsn+kqnrVvuaWA8PRwUDTcCQy0hIItIxEIsNNgTKFUWnius783mCjV1atPNAK745Wj+xvajm4smpFoHk4GhlpCgSa4N0jzQHFwMQtayORtbdMjN+MX28eHzzQ7fN1HxgcPNDj8/UcODPJ3qPWnt5lQmMTt6yLRNbhd05EIhPwzv3Lvd7l+wcHDy33+ZYfAju69+wH7GGQRSs1TF1HpeNYCo1YCstUmbQBC8ANB24D2ELKbdOALxohXG8Dn9PGS2rgqx/mlh9MHByawNqDtSvHcwms/Sp4dfoF04yBbVy2ImBPiSZB7EuJ5aZ0qDpJeO9eBrcpdXUS35a5Dgpdm+OpXYk1PhiKMJiTVovNDlxPYsZzSIWdRhRxzGKmJ1EwxDF7a9dd3dvTU7P5xpGuy9YmaU7vMKg5RuVvHG9s2ra8dPVa9K1IUk3r9Sm6qwVVrzU5+B9F9l37lZUDX71k+dbGzYfrl199YH0oW65kO/f2l6GLem/cP1Y4fP/Y8ssm4tGhXSlGwRp0BV3N4WDXhrpV949lm3of7TMYN31vffZdtfHvayfaAvGtf7Fl8PBgyNswWI3+nlUVDW0+CK6LQth3IgPxnX7Zc+bcJhJ1eZ9JfvRLneW8h1zkF+HzvpH9kEbKAsoJMwqJLvIZBvj7AvnvMUvtNrDeSuCgCR8ZUYT5hrttajBsUF12xRWXq7jw4FSbm77hyL/+8tdHC1RGre5vsmv//d+ya/9apzWqXUf/9Ze/gudMZj9EL5HnJOTnaE+KVGzGIJtRAy+xsgrgB0sGLcwwWm0HKYusIDLYrtlrkglTbQ0dCoZqWpCbwVNGFQpOqi+//IqjKsSFV0y1FxW1T60Ic7/Q6v4aPflv/46e/BudllMXHP31L//1yJFf/fLXR1wqzMOrmHvoNHuKqqWSlFgSndHoKRXmYCIqlpyU1LFYbCZA6JK09lhMSgJFgRLBNM1yxWWgaZgvSTtY1AhqQnGrRalqBpdnBz6DmfUgVSiCQm5UhPy1NYkkh4woBFoHihm6quAt3sKpVbWsWm/l33KdMBaYTC7+Lec7RqtBiS/rbMYTrrc4l9ns4tiByEGt2WR2m/75n0xus2DRHIgc0GhpRqM+ED2oEQRTgfDP/yQUCEZBs7/ygFrDMFo10ZED1CuKasVfUjqYlyIVFVVxCSkzIhtLUwjjEkqrCacRhQ8Rg6elnoiDjkkasHyKWFqjxfc0KnibVoMPtZQGpCKrRK0XlMpr9Qp+4QB6eQi9ku0eom/pQ9/PxvqyVegHsp4ezM6hIPUNqoCKU2knNgqMHsxuIVYwkQPIC3gU/xQBc5UUuDIbTGjGSXwchp3gxGw5EWM2NjNJosYHq0srqmxlKb9RrVRoi4udCqVRE6xaE4g3VpePjazwGtVaVqvQlibbSmg6LtOynU7QHfQt4PF9mB8S0mTwDxIVUYlC4RnGimcQ1kB5fNbt6Od0YmQE/+0UYOsyGIdAlS1C1vkDhFH0ArrGSI/6BGieOhcpnwuP4Rlnz5x9lv5H9keUmjJSIhNFoiYqacknqVAC/ASMnKWvNJaWz12v9gqrlXTwNGWxUATL9p39UDGe84edOQqdmkzO/6mBwlLZ0xkWPJ05I5XlfFoO75/ju0zNCKhHJquFxjyPoE+4pb6Vd7w+NfXGHcPDd7y5Z+r1O1ZOdh66d9Wqew915l/pd99E9hfHx1/MZt58M5vBR8j+pnTqkeXLHzkliacf6el55DTm7yxg8RD7TYqnAIkrMfUqFaD+GLFt05wSqUE/haioBtNmyKQZNVZHhgXNVDP4UK0EzTTBaBg16A6CsSAODnR4JIjoKehrTRJ8rS80ix7vQ01zVjTAZN/SwrRRNKFDpx/q71fc4w9lfwNmAFHXAz1h4GeMWk+lKUxPpTaT9mBuGrHKxKOiS+ZmeSztsmASXDA5MG+12E4YMlIN5jHmLevBvK0E7ZYU5WDKjMI0a3MFiLOKY63OYS7MUuKr/KFmJq84KvBWcW/MVoSu12nQfzbtGqioHb+4teui8Xq91kMr6Wr9wOH7xkfuuagjtvpQc7be2x2gD/IWv86hRv/VfPjSK7qHLukPlPfubAog9fovT9ZUbf7y1uHbr72sJVutVpv5FJkb15/9QBGF8S6nbqfSnXi8HGgP14kHxoFxSMeIImkAPTk6Y3n01BMVK09KpcCFUlmnkiAbdxL/kdsB3HDzorn4pCC1ADt64XZpJfCAUQMP3MI0F2vsxGZUcoCkJKoFrjoFsTEl+k3p8krs2rGBxQbAg9zsvN7VnsusKFrEKzfKI6jrQ3q9zsKqlbZA7cDOjnW3rY+Ub3nskg1f2lQdX31Rc9dFYw2c2q1iY4b+w/ePj3zlQGvFwM6mRx9ffuXxySue3N2Atgis1mgxJesbIoVNGy9Jdlw0XL2Mjgztbx842Osr69nZkmMnxkbdh1bXG92v3TF+7/7m9j3Xw3xsA/05yj4H+myjeqm0DmMi4qYNgg4ZwiITlwyg4GqILuxRUXcSwl1JC8gHjK8D640up8WCAQ6olIgEsIx5XbYowwjMrhfceRK0OpFso3+6BmkMxt+NzY0aBWYzvZdm0G+Zd2Y7EjpDdhN61KBL0H8SSi1E1veCrBWAHaLUP1HpMJa1msmk7VjARdrMjNcUtgOF5rjkVWfEYqCwKioaTkpBEGJ1LnSd+yOJbEQ7BDYQ0UhFmlOc6D7xquFXb92Ib7BicURyF6nhGiuZbXDTekK08tMWq9kcflX7lRO/gnfpQD+mPe5iczgNv4tvLb7VrwRVSKXhXfBCzVhtbosnIgegGqvNXuQ2WzzFiwNNBFSB8jiceIaZYOqnKSZINEeOfxaZK6UqZMas83sZYtjmwfa9hVqLITY41b3qy3uaIuvv2lR/fU/rIfq2AvfcH9d0XVZ38OsXNwzd/OKOxr2bhg6WGj0l7sT2ezauOLa+BpvG68othdkiwdh68aMbLnrh6g5rIIrt8W3A4yrgcSFEJ2DRHJjLPnUmrcQ6wFU4lDCFOCVMoWpilotgChXxUghEbwY2x+A1VARQQ8c5VGSOVPjw2Mw6eVZgmyF7BNW5Y1lqoW9bvRXdJvhXZ4eKa22NT29Z//Ch1u4rpV3bnjnSvjG+7oaRsTsma2s2HRuauHNLDfr70ZM30BbH3PfKewPN3U0HHt665amjHW2XS2Mrb9maTG6+cXDkxvXxlq1Xy/70BtDxHpJvci3ScMmoJf4w5wSxHwVoRMJMlEiCzt7A/LVKObdTXWhvpx8ymGbf0PHs7pYKwaU5/TPeynoKrDz+fIa6HHhYBjYpBJH5IPUmlfYTOwyxBEnR9CkzM21JvxF0tS4utangqUOEmbI9Ehux5dHCsTYqNcomCvPVbchMW9wxNYQncHFZFBtxaaWs18Lzb1+J1ZcTWV7sOCGl7KdEJwTsdSknCcxZZ6qDqOMM66yTD0lQvqwRZGX0VyaJrJLYyrnBi0p9bXBk0abmoxKmdhEmUMno9byR4ZLzyyOrLu5q2drur9/7wOZND+xt8HduaVl20arosiue37nzG5cvm6zdcsvIyM1bEsv2Hmtqun5qWTQ4dNmqkcuGSsLDRwYGjo6E0dVDV65r4k2tY3uaB26aTKUmb+5vmhprNRmb1105tO7uncnkzrvX91wyGo2OXtKz8er+4uL+q+md9XtHY7HRqYbmqaHKyqEprNsiyD0GcnGDdwTdNlP5ODuizsy4AmYcXLtUspMEcXiAzR6eQA1tzi2WeTCMtrvMhF+RAOi2lrKnlsbMKgSGDkdrBH98gkli1+XHJzc9dnGrPdJenr3e6B9DX/fUWBuObxq/Z2/z5tj4Vf1rbtlQFV93Vd/QjRsTCuX6Rw63tx15envdju1TTXM/dtCrwwOB9uUNU/dNDl0zHm3cdKRpEKZ1fN01BFPdDZhvmPkF6LefqlxAfaI3Ktkx5gsQEIsNtzUjFpIXqeR8yE849/Ru42IgmDz3bEnWdGwJSiR0AaaW6aqkOnIW3Ap0GaMyFo1ERdNJiSqGmMUBlGnJixQFvjtM8+kLSrKGwbU4PpGmCJovBLqX0K08PwZnrj6H5DnqUzH5E8jIPKEYBD9JmWsRsRRKFYToOHB6gqH0/Nx3fKVhD50wGugHytGtHTpek/1XQavhs79UC7oOzI9n0X8yp5jLSD7dJSN7CHMA1LNYCdVRSTNviRD8PMsMzkrMIPrPvj7U2t9P6IB/RgWS6UAEkiVwpIaCTQhZEdIb6WRxmSUgzH27gKGQsUNnUqFiXsNyauTmbB3ZS8qBDt/ZD+kfwLwopeqpKSpdh+US0ecwuBdj8IaoaD4pmTic4Zi2m+IcTAWQUFlUiltJ1qMQTxKBpIglkxlPEm+kDic94oLIp8RCAOrE1XkjcI/SmoJyxmMeAimMyB8CG6PIzxGAu0vE6yvsGtlSv/yqTXVVvav7amh9B1vdM9pTHe7dVNu5pTOkMqpf5FzeRZEKGy6Ml9rDQxctX3FgtK2u3vfMN9nylsamgcmu5Jomj78ioD8zcB493X9WryxlR6gV1Gbq25TYG5Va2Ey6pRfDw5ZOgIfGqGiNS2FFRlwVE9dHJQ+bEWtBbBhabiG2ox5YVc9LLmDHIMSkgzzG+DNBOVsQ5KUqzC8uI22V7XdT5vffku33OC9OnJD8ylOi7wQ17fOPTxC7PX9EsINpUDC9yFo9tS2964GRUlUQT4/2bjI9jC0ksSqth2nygpZymarqc+klUyKwiJ8h2TjJht1mZzjQ4nPsFMIpE5siHktgMOtBSoXfFwjSJfl0kzmCsKT2H/khsj9yy+xbFzfsvG1wYi2d+otVqVV1Be3XvHZJYlNwvV5vD1a76vcMV2197tfX3D77xoGL/w5pvnrvme0qHafkL8q+/8zx7M/+8Ur0nqWssaxksKfFNuys8a+7Z1c9HXsOlbx32ejx008eePn6no3jG0dLuzYk13zz9jGTKftQtM9dWefVNR36y8l7//VrPVPvZD967IXs+69sXNbOcsH+4anvo4o1Zd1xt7N13yhqUqn7jn4NyxcMIusC/28AjFshR0mAa2WYq+EogLmSBs9AexRj2lxEZsZBD4qTXBSD8/5+sxfBVAMoY6RX7qJXruTM7HNzdc8qLMYP6VuyP1VxahWnYo+fXmM0oCeza3UCzdE/EyqdTpwJxjjhPfBHXwM6LJSHKqf25OI1K8QvBI+UQ9BS7CHkFGNywkSzrGaMbQGTkqSj0ZyZVhmdAAqCcD0YlVQQHFfAjaAVaNaDOnjwgTElFgtwKpabRBUeiOBdEnqUeGMJIneIN4kKBP3e99BjV7xwaX1p/97u515pv/LFi7NfRlN/9U7Nli+tzX4FNUzetTb86lvZv2OPV2+8dU1qz0S7yfXNv1j3lR2JVU9+tWtff9lAfNWeui/fQ+zl1Wc/YCMkLo1T6Qgep1ubszAW7bzLdVqIn6Uki1swzWgpQ7DsXN2VVwEUckY0p4cYSXrkXCiir97xOmIfHjx2cFtVsdqkKapoXn2w+/pfPDIx/sBPrlhx2faxMKtValVllbuvumfintMzk/S7TyL+r/fYK9rDEb21OFhsXXv8w6/e/+HT46COIYVSVVE1kCza9TYyEdsAMmMfAJnpKSdVl5OYgclJzMlk5nOQIA6DvHSmssjpSMmJY6J59ucTFCXe/JTzvkfzD2Rf3LbtxewD2Qn01LGf4mTET49lJ9jjk29k//j0M9k/vjE5uvqJ39137++eWE34inWoAejRUd05ajR5ahRMZoZVE/1hMWF6QpjGLKfISPpMowNrRsfkXFkuQSYnx+Sf95jJOSV92dyN9Gn2+Jq5F0fnnlhDnfNcDdUqP3fhmWqWPFONn6k9zzMhKs89ULfkgfLj7p6bwg97ZM3cdmped7aC7tRQ+6l0FdEdZkF3ZkrKqjByK8GOqjavRqKTl/zA/DAE9v4wfq6/FJ6YwDl7J1hLga3C2dmwIBm02GqWgMKJ4ZRkKSMOyuA8j97Np+JziocD2SbkFbDqgWG8evsbyPD0yO1Hd1UVagSN2tiw9Wu77/jNo2PjD//LjX2X7d5Ylf0PHY++lDh8w33rHspmX91Ov/sMEt7eZatoK680KpSV1aGJZz685/6Pjk8YPRUF6CZOk5qbCzaUWnPqJ/OdrSXybslZLpVsuUQ2PsNoCecZ1by0dWYcmos6sloBMiD2IS9nvCgfx/G48N5u5rZdu2YPs8fn1tFPnF5DvzjXKz9vDn5th+cxlHeRnHHqkWTr4dPwDzv/iXO7sMWT/3bt2Q/o78LfuiAOkiNJHZMBWkQljnAoiCoF8lkFZJnSDJ9TiKeJDqdTmZSoFEQFzqWSVY/5mFhewQcrvJZmEK3nNK5AxL3iyrHI7qb9j01GNhq4IqOGU6lV1dse2Ml8a7b+slevbuUIPX8C3vnY5ygflcrxzpbjnQF455V5h7XITwbnI7yTApgmxgs0mVLyGOXFFrIERnLmduIUUIQJI+FPO1ebixwWPb2cL7SOzt1kdpttPoF+cLTAZph7QGe2e53rwU1sZrScjh7nublLLKBbLuvccgCKh3SCjp1blpMz83vgHZv3UBKTm9dIVOZ5n2aofDpRUi0I1freTloEMYjj8zqj3A+f5cnPVVHIjdsYz9dXeAQS7OBMpAA4DtdTmCDYEdU4I4kzgOrClDx8wArIZgehEA6A+uDsZBj5QshmFd5bzgkaerlRrzRo6JRa4HrWK+b+hivgXca5Fxn2uNIwyxd5eS/H/N6gPL1G8eOColl9QQHzX+6CM5WL9duUt66iLkerBmg1E1pNAsGceP1NB7RaiI/GNCqNi2gMYlXx58iKA1nMs8y6mIObHQY6VPozDk+h4sTpNRbFf3gKzjRi237V2Q/ZXy/NRee9lF+7kIu2LOSiLf+7ueirtr2UvRes/uQkWP375l7atmf0gZPXHnvvvlWr7nvv2LUnHxil330arMTuXe9kfw8e4Pdv7wJrIDxz3wfPjI0988F99374zPj4Mx9i+kG/FfuIb7JT7Yutsh2QhM5A9FuHk8AOMgw9dlExUS97KRamnxNz0o69FCt7qWIFAQdeJ5oHBX9Cl1BnEdN9w19dmv0D4jbds7vu+9/N/oE9/i//sPHRi1vnXqYfrN1wTf/TMzKWvir7ltIDPMX5pMF8PinP0wrtQiLJMp9IwjydTySxVoeRBNs+B5BlTYkVQlprpFJL2YuDbjILP4vNFcOHe9HRMYtPn/1u211Dn8nxfW89fm0ku1fHoRUFhefnfJ73Pwfe28G6rM1prkHWXMkH7Lc5CPttqnnzYgf2O2KiXVYkzP4AViQ7aI9JKy8cCjjJbCP1EqJPyAslF+Pa8mYHhZETxRfkc/DMn1NT92xymtFHa3mHLlsllJa/Obvpvl113307+zF7/O3XRm7Z2a41uubugPiwz26aO0j/PLL6aP8DX5XtxfjZD5h3QWZN1D4q3YAlpgXbo20gK2k4p16ER1UK10qL8LVSP16Ea46KjpNSpSEjVvKSEYaSMGSkFnitdJBVMdEovKC1FJXEGnBcmDCJxTC6Ui12t47iBHG3udqPnNyU+dBEpVT5ZCmC61XmwpfxIj2vKSqr79vavPqmDdUt26+75bodzcndD00enO51agRD+fKpwcFLV5Y37yB3mi/9+v67/uH5SqMjUB5w1Exc0T2wtb0ynBi+YkPPjTubu3ujAgpGQpUrttf1buqMVCaGj4yvfezSzm0yTwIg31tAviqIkck6jyxaisGLPThYF5UnsRDTrBKzhMVsUrL4UInXHhciebzuGFBsyzI72aHx8dMiO0Q+/ztnf8+a4fOdVJJKW0luWyvbe5GL50ElmHxcUAb+W+LNuaVmhkyL3Fq5ZYmTjNDf2dV08KmdO5+8qHFn313fvfrq793ZT5cx18xeu+2b1/Usv1bcBsfXHPnB/WNj9/8A04FjIyfQwWN/z+NxUrKDxKtY2D1QEsXnYKw55wsSOWfoN45ADIT+02zQmdDvWLNxeO7ZDexxo+HMimhtslKR1gkADcBSU5Tqx/CMEPVzKh3Cz/AUB+PxOHmUxLnjcWxpsV3FsfHbH79/guTsqQgnKniR4iXGcYqFQynkOPVq4+/e30VuB3HV2QlJy58SdSdefcf3fiqf0OdE7wnJrD0lmk682lTxuyr5ugfXNvHY6Tl18HEumIe6UwwFGq7Q6kxmp8tbslAbhlp5Kn/d7Sn2lgRD5ysfk6gQYEuVzS/bp3gMJ4TmfWXMds4p8qNgSAlmS1jjVqN9Sg3L6lTofoWFK8JsvF+lY1m1Cu1lbNxQtm5DdpVaqdRkR9azxwvPjFuiLlfUonhaJwB7xy2VLmeEnIFPzTgLC51n7LLeAq8Vr5B8fnDB99N5tSqKYuNDSTT2niob8Z4aRMSap1IjWxmSCfcLtD6r38FxLHqZUbPouJLTTWZ1tGYHJ7DZpEKbbVWZ9fT/oN/Wa+ZuVBvV9ISam+ucMwMmeMDIzV2nETBNLqApTeLeqlwWlsqDEaucaALltuUySQSBUPJBXuUWMxGmk2steHf0MGdVq60celhp5tbNZXazxw2GuR2OCps97KDv0xlnn597ll6Nn38JPP9pEv+7c9gKcClZ4ZADJS6K7RdFFjmTyIsXAlTIa71Ez9w/e7HCzs3uZB4Omk2sak3AZjk9uwZ/5jQ4w1NKAT4zSjJ5ajYjqqISYsnn4cmr5jNpNcFragOJunIPMecXxuJ4sXQaLTNxP/4xZ8r+QeUJGIRT23hDCYXO/vnss/TJ/Bo7tXiNncFahmWkLi810leWCl41+6PgqazZiunaB3Sl83QZohIDdCnhT3N0KQAGAF0KPaZLgenS5Omy1yQwvJNDHO8+HlPFo87s6xkDr3yA5wJ/xnUxP2DizLcIXsvX81CkGoVYRXN0AZzll7TlBIqcOMFZlB+g9U1owzKdif1Yw7Esp/kTyxuYOH3J3K2cFr0peAS+WMi2q3lZn6nsb5nQ2QjEI3ZcayBRbAb/kFoIOQqxgo1lQrP/+COCo8cUT6KvgC/TgF8majaj1FNGXC1DQtMZ1koZFPlI1EzWbDGBYxucDv2jSb1Jzb7Cmf6o0mIfvw/84hqFHuxWkrqBShfg2eSN51Z32EzagiiSOUpryLq6htOEZ9i434IDcExi3aJVHoxwRDYmuXD9Mi8VGTN4MqbwWjNmlpASY0Kas2BDIhaZRDdMgjhenqHcqZSkYclb5Hx9Ert9kjGNotyimoCPlxSHQZS6r+ehj5+/7EjvjuWVRotOGBL3D1++sizkUXHlIxO7mmu29kU2+JK9pQ1bR3sDf/Hjm1s/bts3XK3Yc8e9ZdVl5qKh4ZrNt47O7Sy6rqy90u5u3dob76uyuyItJUirCDSPEhwknv1IwYKeWkAfVlJpDvOIiksO4IoSs6dYlRFRNLcGgau3JVqIkXQWrqTRGMhKhFRkxWiew3C6GNBDWiMwqRy0F/AYTbkYMARhedI9D358SpW4pTN94LUf1R96cs/u++uUjCNYf+e6iZvXRp55aNsTbeyP5i6d2Jmdy84eeOvO4ZGVV7p+MdbdfuTpyV+f3Lme6NfE2Y+YvQodRF1Ncl2mVACks5h0AQ4E4tIFPQY8lWQINiA5gpVcKAAoo6aK/fPFfAS7yFnWxXmD+WwVPdF8+Ln9Wx9IOVmtWhtoGG8du3l9LL7u2FDv1tagzqAucCyf2FW/+bGL2lD28InbBloSflZd6C1oPvzUjqknDzX6y/xar6c2ZF124zvA+3Gg/Rs53q+h0iY5eiK8JwPwAO81i3mP2Y5BhJqLxSRdjvcFmPesCfROJ4hGnEHEEqDUxkXLXDY7ia2iBG3TZosNJ4kFOR88Dryf2nFP3ZaES6HtfOHgaz+aJLxvuGti4qa1UXQGs36gh153OlLw6LoppEAKzH3ataa77cjTWIewDF4EGZSAf5ik0l4sBUt+EBXKzEyQ8+KMT1AxHz4YDbjiWTTmIgg+F0EYgXLW4sWTSCtIzkKsUBwuhaXwcUoMCgCtFy8kKf3eT4op6c0FERMth5/bu/rLU40Gbs6T2HLb6oGD/ZU6g6rAuXLrodTOr1/eMUk/Wjl8aNnglWvraNO+V27sbzj01B47b7no+UsavOU+LK2gbfnt3/7J8HUT1bF11xKd88Cgr2Rfg9c2Kl2IpQZwrygu2ZUwV2IYd6lVGUmHRwvBeiGpdCuAAdti6YJCrI8FToCY3hzEjC+GzcQyFCEZdoaCnucrhy9aVtzqZJBZX+6JjTb5UF/2pc1fcjPTpdeuuX6sQqeN4pxG+66Bq3pm9zFf0tJyrnogez3zM7B99dQQNYni4LexMDYpM9N28yZ1WHIpMmIiKrUCyX1RqQI0LRyDQEdajQ3fNiKjBj4jNvCSUgc2jicr3StxHoiDaB487kqBmMW1OAaCQzcvdcFhtZBJV3fhMVY7YIzbZUj4pw9OPCkvl/Tz4vITUrn6lBg5wU6HyyPm8KunzCc24SqN6Up8Cm+Z7ulfbg6n4XRRrQZcw7UaL/SXV0aW9+RQ3ov95eGFU3mxZW2pYGrVMGabX5doXb0JBy9uQSwATeprBU2qbsDBKISlOGXlB6tVCmerBUlXAq8u0zTnXrmWWATwp7nq3vkiX5vdiwtS89U/IbIEozzP2roixDFLl9YHdq+PN/LeiKdnZc2mm4Y7DlYituj+InftxhtWji0PVzdtv+7G67Y1tx55dtfUY/uSayLj165acePWVHzV3iNHa0LtVa6Wku7tbe3buwIly7a3tm3vLplaebhYaK+3RSNlfPltG3ovXR0tdvtctC60Odl7ZDRa4Oz0VERtSpU5MtLZcslEoqJvS0flQJ3X3zJWU9XgNQBANZbGGhkqtbGzpKRzQ738ulH23U+BIv0d2Ccr1ZXDovq47BWEnFewzVsmmvgEHOnoDWTrjGSwkjASDK2cH1zwBsTjCbL9F57a3P3CwVXXrApvOXbT5Nc7weJfvmZH7eSd43OH6dvuenzHxJwC25j7gaBB9gXKDDiimUpb5msBjPpM2opwms1xzsYjC9l4ZDeQLIlkn8/3fLJaHgdi93POYrPJ6+B5h9dk8jq5ss3shMnn5Dinz2Qqxq/Fp19mzsyyFH3277M35mgJ4ayuk6SbgAwtwnAdMJsGMFuMZJ80JzE/pu0aCwfzxConn/QaIMbpJ8QwpPAMzPFConQpfXEWGdRu18jQZk/j2mZ39KWltGYfrNarJ0YUV545VjvREdQqv7OEcpClCLJ8E2Tpns+lWuJpHRA8wxRROpxIZWWReggX3USkUjHJpRaB/Pj5XGrifKlUBHhY3FLFOXl0r85hXp1t1pp1vF2PfjrK2fTZVUKRO8r+aPZitRFdrzNmR7UmpdpumMvqDOg7Jm4uS/TtHfgVABoZsKwyjZigXOYaBIl/FjLX72xmf3Q6ktNT9ocEA+zLxQcOP0SnCEYny8QUl0pBY4tieRBQYcALHGIFT3I4fsP8pgCHjA6kCook1cQAdjhgJkQDKRo04RQIjr1YQz5z6SF1gTZ7bmk8p9jcOSpeW6DQuDsG1lQduMFh6li9rbb/6GjllmuP1G7pq9h86cGRO5PMGddXyrviBddd1LKuqSi25UvrsPp/7cHgwEX9+Ojuh7eOzWbzcxLGaqcGcjziciNV44lpVs2nC+3yGO1ycofLT4TcwIwCCdTM1HzykAzlE7MTk77slUMLExQovW9sz5IJKmOZ00DXObnYPAbwq85bF2z49FzsZ2xVabn0+X37nr+kpeUS/Hppy2R07c1r18rbTPBrFGWPvHVrb++tbx05cuLWnp5bTxzZ/uThlpbDT27f9hT+s6ewXXkqey/QrQcbF6DGqbSQp5uwVIOJ94Lm4ACuZB4BszYZAbtz1i6INzNSctLMLUgagVRO4FUrvUUpozCBRCrnQGEnOgcIP1VrEJAG8NfrP2w48OTUznuT9XetxQDs6Ye3PdmavZfdqjM+tG4qOytj4b6+rJHuHlsug+FdG/BYxmEs34CxYDw5LuNJAibxNF9AlNxSRMlhIF8AiNKQQ5TcPKI0yFpyXkSZJOGmcCFEueuBpAYVJbZ0Tu/PI8rkl9cuIMqhgUOu0w/RRRM75xFlwaoegihzc5r+PYzFga29nBmfl4hFlwEbyhefiMo10k4yGpi6JEDDJstIVhfs86sLMusXMpNYs+MCj9TVTxyJrPBzjKC0+6qLL747wpzhTO9dcbvZ3MEjjVZ9101zu/JrYwwL+t1I/ZBK15N1WyUEjvUkcFRowulCTFkIroUIxAv5cMjRFBXtYG0AH1XIfK4VMlKzDIren3zHIoMiMy8KJ6So85RYfQJOpk1mAXBQlJ+uilYDDoLfi3AQ3CQ4SDCZo1XVORx0zhlBQRU4L61UgAw5YVpTGMA1JWKtSfL4sHKGNDiNa/fU5tK4i9brzsnj+j+Zx13rYPU6Q2nz+q62LW2+6qFtU9uGqqNrrlyx/ktNNpVRV1I/2pRc1xqAO3vgTtXaG0anHpjyqTXeoDfQPBKJd0S93lDDaGtisr+yNukD9+Qqru0OVbVWFntLG1c3dRxaVd1JeF579gP6QXYT5aMOydG7HNIVkJDOpgnjLUieuKQmsDut1uXr80nG3k08r6iKpfVufEOPN6G4Sd7EjQvo9bzEcBmcksAugMHLyTRwRifki9Vqk2Q7KVnoztkeHGFgh1eL0yy133Aigz6CWrMnrMG4u6Q25ODVBaEjbTsu/rLOyDwb1KO9Gi57ec/cQHljyGxzWbXhcM2hI/TLBhjb7aBP32DOyHbcgPUbJ9YkZc70iNp43o6D18NJZA1ojTFG7A224xqG1LiIelyvRUlImfPRJKssT8aFiC9C37712I1bv961JVGENN2vHBq9elUYHaBvmzt81xPbJ+jsLFtwz9huMOpULt/HfA9oM+Gcsonk+1Au35fPEFGmCyb4/K5+zqRAQ1ody+o0aJg16Xuzw6uZM0bt7M8c5TZbhY0J6DhAUvhZdvDd/wAIr5z6M5Uux/6sME4eJ3EFOK8cjuLyGDxf3tG+f2w+r8ySvLLCcIqFQ6nccOrVt3/4u5Q8nXy86DkhCcpTouXEq43Z9x+S88eF8GcOXizkJTve6OyAUFp96tV3yt8vJiXiAsw7wQLzzsdPF/s85vC0F/9Ow8VFsw/uwIvoTVGtOgUrmCx2h6fY64sszjwbqdydgkJPcfk5N/PTExhYjtdo/amlLASjGsuv1+LKa7wgKiff8KKtvZczMwipNApWr0YmlbXUrkIGo1ahUSNaXbA8+9xyXpX9LatmGDWb/XeluXOB7WE7E7bbZ9+NhG0VdibgnGVtTIPRY4T/Z//GllszYW4DuRfM5575eJpGueWEwihO+eRzz9bFuefEeVLPAXQg+/B6nHoOKzhkZ3ntRPZBdGg9zjx/l9Vm31PxOlqD/qDXZIcEC7pVY8ia5/4gaNDbFmN2o8aIdQP82feBHhvBg7IKitboQqEXZb2gFpJ93vYhI2jiGqVWweqUaIQ16/rmXlRaTMtmCFt+aywW+GKecei4029wJnQnPKMfeLACnrko15xPhZEqzwvkmvuN9DVzX6F/aZw7Rh8KCVZm80CZTZj9ywHM17bsH9AZpUAtR4cosT4q1bAZUjwKIbgtKvG5DS4tELu0gheO8hmpMBKLpVuipIARacLTndEWCGZUHfG4VA63PWG4XU72zJSnwJYJMbzrhWyYeOOjdfJW8NaIGAZd46WI5pQY5qUOzalX31r1kYZMIW1E9ETw9uNCuOnhJRW+WfxHA5kJWn5arVXBBNDg3zBhposK8Xxw49+vNs/+8XHytgg/XREJw/VK/BueNN3W2gGn7fh3Go4Xpo3YnkrDu/BRRSoNn7boljuVhufgI0AarbxKrdEWFrk9eO9/a1t7x9JVG/SSWlPkrqic36uen081oJXleG8PBCIlKdFmknTFZHbV5kAj9moNiKTuc8m9RbXx+BQv+BTN11jiP2kLNJTbzHZzqGeqs86k9lUsr3Gb7CZnebLInSh3wqG7ZnmFT22q65zqCcEbbeWN9JYWW3nKW7dnz5765j0rKsI6vSc1HKvfP7UnGWyJFquUxVXNwcTU3n31seGUR68LVwzubknB2+t8deV4HiJ99l40DvrCyFXG8yGQMUN+5BAIgX1H+oHsvaqjf75JxkxT2T/QJUTPrqPE5fLaQV1USoKe+aNSKKdnEJJqC0HP2kGRIm2gSO1ky2V7HehZU7tGTZpfYD03OEHdmuBd1c3wLq6JbNFaDuoWXFC3b390j6xuzogIonDyUjVoVIQo1qtvRT/6K6JuhojYFsHldc1ws42XtPim4Y8XET0y8NM6gxYUR49/v9r84R93k+tOftrlLITrBfi3WM1PR6sjcFqFf7/6VtlHPydva+anW5rb4Hor/p2GP1mkXAWpNLwdH0VTaXjbolutqbQe7/tNiTqsd1qd3uB0FRRGAEY1t7S2fVLvdHpXQbSqpfVcvasDPyxx7aB3SQH7Y79JclSmUrnlmEWql9uTgU9BAYNN89tpSP7Sukglw2iK1/gqemrcZpvZWZ5wY12DQ3dNT4VPw9d17ukNWWwWe3l9IFBfbofDUO9UR92vZUVL7d8LitZcVaxUFUdbSxJTU/sa8oq2Yk9zamrP7hRWNNBSUDhQu1TznsEKoj93odcVFnoOrO1qCuyspFVn0layNdeKEZMrKrFwhXWRBXNeM9/rxWMktUg4zOSNci2S0YNDCCvGmi4t9nSOxTEdAZrxXGBHNtjd5W0eT9Xu272tItgcdgwWN0+kavbt2VYRagw7EHq9bvPystLq0oLqztK6zd34sBAOSS8amCvHAZdzVCHY7jSDDbVenwFvhVdLyTqeNYN/pgvUOCFUaMD3REucZGStMRLEFRQCiXoGU6uHQ9Ei733CpC6kZJJxMBWC//1E6aIuNPNNaDYyz5cmOJevFO7VzS2b7z8TmZN75jyenWPOKLJUlKqnbpL3UoglcakWAjJ7LF1LKh5rCzVynIZXARIqnDAmpfwwiCogtkpuVhAE1FpbfFIQw3HJDsdBXlLK1eliAudnbXCgi5HK/mCCRPeSHaPDEhhdohZwP0cJxfNrHov6dXCI9Osg6QycSs+37GCSuZYdj7dd9fJhHTJyJfrxWxMOVmPy1Q2nKgZ2dpXq1GqF07FsYk+DfH/LXx5u2VS19pqhyg1fnqxB2Yv+6tZB+kcGy5/UDVEfq3a4C9jZa2l/qVfBFrtjQTv9Hm7F0X/Da5dOPnKoTcVcybRe/ATWyS6KUkyxLwPXLpI7PkiVTEY+ADea1uHcm0uTmaEUcZ0hLBbH8eqiWCIzLnUSR4QhvC8olg6l8nFZOhXChykKF7am4powZhYlVeIOJ+UpyaUAbeDNsvMgi6r5Dg+Li0oFeY+fQLbjx+UTvGVU6DILxxO7Htm54tLxVltIYxA4S7RlrHno0uEy9B+CIVvT22oPO5ig0zrr8bfHi+ibvEYrqtz4xJHOYNtYtZ0VipuiBbUbb1yZ/XGpzpT99torKhSKMmNRh6GsYagWrZD1CVEQNm+ASD9JraAwIiqDMCgOU1Qpr1wWn5QCoAkBnuSzOC5DFivxFqiXaLVgcRX5daROK14GV9Q6coWW1SJpl6PlpJ1UmytVdlVIbuqgCpFceCKpWpKNeTz2cORAW8uByMOxh0rC5SUPxx+OHGyB80diD5eUl5WwFX3bU6ntfRX5V0V5/GF4Y+Ch+EO5P4yTNz6cP/95altvRUXvNnh3f0VF/3bQhTWgC+3scaqYuliuTMvXusy4ChyUvJUUr2tYYzNuD7lgjEtuuCCAOnhxuRPePYXzYqZY2u7AOmC3gmHjY2mHHZ85XHgvcUzy4USZg1TNALLwLJTPEIyZT4B6reQ/XJBbS/5bs7LAgLaoOVYjoC24nCa7Ak1mb0GXZm/ZLL/A5eOuuTWWgOAL0cd1xtnvNx5pzB5FN8ELqUtb5PtVME7i/dVk+5cihp2/qIxJKrCxmnkMwMg4YACQAFMw+2+K9Uzh7G/kGrc7z17GXEP2Wq+jHqHkuWJTZtI2EinbBBhsNCo1wJUGAjUbEtimrycGp4fPTCt7sMUsADTQw+NeQ1IALpYHRuBiK1xsjWIwipsrbMg3VYilxB5BTIDjNYl14GOFVr3OzHhC0YauwaHxCZyDGDGRMjlbg2B6QcmVx4YmcrYosWiZZWnmQTm/4zoYSp6brADjpAB9lRdd0J0bdtV1L8pGBBpGm1Ib2gLxVXv271kVX70q2UUyEg822VmDzhBq3bCsZWuHv3bswMX7xxJrSrsmtmyP9LSUNI+s21Sxtp/+58GrgsFt/cmtA5WJhN/g9LiKE8tLo8vqotWp7k0to1cFQpPdJGNR51ervcFiX/NIVc2KxupYbffavvL2RCRc4fJuaY4sT1WWl9pDm7FcShU/pKPsEYivS6gaCu9O8sXJhj9HDL9IjC0GChuMiogsZ2CcbiGL7Bm8WgpyN52bG0WBJeelBkcRRDZ2jrMX87zbgVYaHO75C4LbwZp8HnziEXi33WCwF517Ctq35uwflEVgdwvAY63DPY9IjZtXkUmrcFFGWEEFFOGZsX6ryhCWxkCF+sewCvWvxCjSqlKHZ2rbyb1abI+ITs0UytupCuXtVN1CRuzmcfJ0hpO7n2A1CnaDObJ6VeHa+tExYqCa+gXTi1xhsIrqHsUK1C6I9bLzUuDiQ7wZDW8xWZofti822osX9BO5rf5yYmRN7aabnnh9+/Y3nrxpYyKx8aYnX9+x7Y0nbtpU27j75Y/vuOPUK7t3v/LnO+/4+OXdH3Rd/uy22vH+do9DxWl9DeuXjd42mUhsvn5wzVVJvY7V0MWNT16y5anD7fS7297EH4E/+s1t29/IH7+x/c5Tr+7e/eqpO+889dqePa+dumP7s5d18kXlhT5dgacgse2u8XVf2lpTDngaPmt5x9Fn5Xm8lxmmO0AWQdCWq6m0Bc9jjWJx2Yroi85UEJGIsegMS47ymytC4AVCcqMpFuN+B7gCvK0ihON4TgDkWi3AR/nwqqjDJBblNoFLToBsYkyQqKLFFSzm81Sw2HAByyfbG9VyaG944z1Ty/oqGssKdUaVoXpv1449Xp2O1bpiiZaArzlauMziDTt8qViF7esPML8raY8V0zUrVtqdds5eHbl0W/Zqtb7LEXAaTMGGisJSl87o9FvuZJcRvjxC3UJ/h3mYzKMglZsxMy4rpQY+FMdIaYEL4aJks6Mo10in1my32S0qBm/+NMORES25hBd4H/nYzSP1awaNVv+aCgluDp+rXsfnr6sEN23g0DFea9Trsz+xaNWW7I91BqOWR9ef97Icmz2D1jKn6J9QLFWV3zma746j0Mh7BBSkm1JaQfqMKKj5PQK4A45feIZZuYq+pS97E4qAGzxnfi6jBqknLzBDu7rJLOwCrNTVjT+4qwrUpTE2Uz1IblSz+e3sS6bnMjDt3TFxGS/14bw1nNWeM1lXwtW+ZWDErd6wqo3sHa0VIKoSgyaxEXSou0swzcC0pcitQUGs/RyTlhTVyeZ+SbV0AnQujD7/bEVfnXvo0euP6C0aFBjWGpXZ/6l2FRy894qj+44+9bnn59zzzG2XHN1+TFCZjdmbVFq0Q8dl96MfTa7fsBpkamFpmJddC31+2IxcQLjQ50d9Tp8fC5h9uoPsJV7PjNF/y75K1svaqfn2cXhvNel4klst4xZWy7j/ndWy9VUjB1vbDo5UwWtb24GRqp6SltXV1WuaS0qaV8eqV7eUKG5pOTASjY7sxx3d4G37W/BV8q7VbSUlbatlW3SAGlZUKx6CMRupjYv2QOOQBaCnqImlFaTmSsHhYEZBYkUV1nA+KnInMX4xGHE/krSBw/cMDKijNpbmDCS9gONMQDqCvLtd3ki90P6JeWu2Jd8Carivj97Uhx7NburLbkMP4Dm2lbmf7lFeRVVSvYSyMuCnJSpq45irBQp5x7r2pFTMZdLa4vk+U1EM/stI15wgmDyLIClZ3D0HV7zLIUDLfOMcucfbfOEeaWxI+uYUoa1KzQdFsaDNUVpb1NJrVVloA+Pmrt5YOdTgdYbr3T8xl1qR08nc71ALqo+KUvVN3kCt39STMiPEbtlVEOurLlvW1uh5j2UdYWIzJpm/oPtgPC3USgrCGckAUNYenXHIhr4EMH4Ub2pGgMRE00mxICYlABpWgaK05TeGpClFghh2QYynpOISGGRBldzwhlhuD3IzizreoPlRqhaqExehrwg96VGoWLWRYRSWksZIeWuZzRbtS65fZy+tcbf1mpRmFe/krlpfuSJV3NPcNxhsH6tuGkl5FSsMNK1Wq/XlJUUFFbVOX23QGqMHWv1xH9/eaEGMYssuV1VnRee4RVjdWT1Y5/HUdGEe/ETxJC3k60EVuXrVC9aDknZ7uEr1J4/pnI5NP1cLBsWTfzRx2TmtSrbDt+M1UuYMVYRXSM1yTQvIe37VRSwAxO0mk88lkLIW1zlrLx7sU+T+YaKGZHz0pvkVGIm3pS60BhMMAROxn1y8FLP8Gzsnbw6yTLXFkX2HrVu8HDOxYbCnYqIkK9kI3cmzTYpfQexjxrU4xFroNfLqFplteo6UAiOs7xzpqCca+BlKdoVUFOfecLsoDZ+RrPOd9iBq9ZPthH4Bm4yWi5/ZTf/bv6/JimO7jl/comgbvmFDfNWp3yodp37L3JWavAXTcRz9GR2hvwV0RDBynWH1lAXcjPxCHg9C0VrJRfll8QMXWajjfGGJxRYqFITCkM1SUsjTG+bPgoU8D54DP++m7N3op+A1i6ijFMhmRk2UP60mi4Bq0k0OpCWcnDHJ3ssk9+/F7W89ub36sd91yjlKIcKJ/AmFZHKd4kTzCWqaF0xmktyDcD+/VV/A2aoCbF7VBaQlUq45FIGOpGNpMr4QjdykVWlZobDMXVPvirWXhpvdazcWxrrKyoeyf1Wk1xl0lSGX12Zgb9nCNzd6qn1mB4zpPrBTHcqjYEF7KHD8Myp5QjO4AzMelgrl7KWaJH0v0IRMWNSEDNMYF+JWb21cSOLJG7rvpw33ZK/4S8VX1Gqdmn39jbmRWIwuC16rRFpix8eZQfoJ9iWQo2fe/xQpiP+x5woXF/qVuuR+pSSz51rwP0X2T/E/NtlngzEZLx2YWtY51V9a2j/VuWxqoHTFnn27p6Z279ujONZ9cGU4vPJgd/718PXXH774hhtkXzMD+O6XgO8sVBkgPCSWk0BYG5sJyo41jOMFmItpJW9NkWqqZA1etMUdNZhgbU0LMluZULBk0cVQ/uKM6nUlXqBUvq4yuT/+2C0ghfo1+QpAPvnStE6PKnUGBcvpUIXOwGv47JVc9gpeI1zoBqZbQcFEYb/MPg/ydVKl4I0el3fmiP7czkhLXAryuHxB9MZnymThF8XSZUEs27JCTXhGpeSRIbygGMRzfZo24BXiAOh7eWzGn4NxMdKJJachYkBIuwrKsCvwk/1HUlmQtNzGu3YrU0v0BzfzyC+j+UsQvmMJI6u/1usjjcCSt/y08WvZK7F2aXSqx5i41mUJz35XV2hCZ9CuzmuFA63ZaQfdjkoYxYevz6ue5kyUvUEwn77UxJ1Cv856S/hvfYsvQWscRXLNKubbVI5v3dRjVNolr0FKHWwmz7mZsloX3phXBji3rJYwLEIY5lrCsOWfi2FSPbwhQKo4Ai6YVD3nsGzaGqttJUFohwu3WmoF9pUJaU+sPtc07kI88y4FDaoLgIZzGHmAqdE6rTIj6QGl+kOAE1Y7hhN9FqWVttIO7hqAE/U+gBOen5jLLMjlvAB/nWqeYIxmjDGE9hYzomnFlp0uDDK6W5sAZCidYayro0RX01Qb1UdNAKJ7jUq3Y66PxtOVmOPL4lKxIiONtRN9HYnPrJVZPBhLryUR/9oVwH5DU3slCAUAyozDjg9zIAWJm6JiwUmRj0kx3IwG56fr4CDGS6tBW9fFZkZlbV0RkzYD61fXwWzuH1iL9XRUELuB82vHQBr9KbFJEDem8pimLodpalNisSldUh5LfS5MU46X0s+Haj5d20fnMY+5pClS3lIOmKc/sX6tDTBPS79ZBbZDazIS1FPn7W3qW1GCUc+qOl9mYWYI6A9LZgZzXQ4SlQWLCsO1LoBEFoBEbf64V+hJWEBgzJZdzmqMiczCmo7qwZTbXds5+/iFphBIK3s7/Y8KHVjLBmoTlY7itZCUPgNIUbLjbfKNS3dja7jMtF1dzoWlGmtGaoIr5bgnP2sE7qoFXM6mMU3bS6IpMgdSdlw0pC4szpVHNytaUNyOQ7mFEnxbvgb/3E7TwXB1z+r+GlrXoYQD0gOopntze4lWo1G4SJ+g7qs31SEf5/JZFlZX2lbsG6yPJ/xPf4MNNyUS3Rs7kmONxYGKgEpZWhgvdZQPHlLUfqIfECP3i1FZSL+Y4k/tGOON4lzvZ3eMQfMbjT6td0z2Py922rn/6NEL2vO3kaHDGsOPFer/OzQyBPyycOnTaBzLcE7HRdl3tSb9+WlE7T82aH6uYvM0Kj8mNIY+lUZ59+fn4GMybifxE5zi5aVPJTU7++G6D/vUFtVxWkGrnlWZ1Rei+HvfY9kbYMKwN7ALdP+C0B2jDl6Qbgwo7HHJC2FiNCoVwksgRjrb2E/OxGS7FCNeYqZEznnglnKBmGB6AZnoQnM5mRW5IUtRL8wcD1n6vZCA5lc/E8mFxU/lp7Yj+jdzScLnb07VFoYrUdLkT/h9TfWJwnAFfQFeDPibI05vibeuItAYcXmD3vowwSQyT+YIT8qpRmrswlwJRnGfw0IwHJFYvoTRa82IXp4grriVlDBKYRjwNG1C5sVsuLDklwDEEnl5NX/6qXrwkcHu5nk5Q83jDDV6ttrHux0Gg8PNC3B+AV6c4D34PfhvbAaDzc37YovOqAW+qEpzfEl8mrYEozMR2fnVRGcKc/4tSbQlLGtLmKRZZ7yytuAvcKjGTb2ASYXBc9gk1URAW7z2z6Et50PUn8atLxVGmv3+lkhhYaTFD8pQmGivibe3x2vaL8ClB/2NYacz3OgPNIQdjnBDAL8bfggGP/s7ilL+hvTetFNfodL63P7AxU2LREtshjPpkbwAx6lwl4oZVq2fb2TkiOKSRRyLnbj24zOkIsQSETURHFooCk6JGl7Sw4uCn2YVGnN4Wo1/w81pgwV/+YgZ/2ZeUrBqjd5gtpz79R9+vAxnzv0AC5VwAfioMjPFzHuzb/bSR+a+MkA/Oqepn3s4Y3CjFrpySm3RzXdHQm9lx100x/QVRO2kd1H2btL3apC6lEr34dFG4ue0LwKJz7TLQWg7aUDc3oSjtaHFjYzwTqiYkXT7lLqceDuShXVHosn63j6iBe1J0IL6lNgniLHUf6t31sImpGBoSXQaoT9/U60dV9y9xp6PWAvOjWVLbs88te6zu21F+5NuNJCPbs2Lg95L1AfeQmoq34dL0QD+TkdZP7vzle2zOl/ZP9H5asFDL+qBNVe+yCHnBK6y5Hzw/wOa5j3yYpp+s9gD54hShnNOd4FX4Hd1VOFn01X0WXS5z0PXEi+8mLy6TzrdeSKX+FmZzjmg00NVUzs+nVLcNaoyLgngVvzgVmIXJJuYA5zCAZdj4/EWJKnUSha+458cyad7lcXjin62E8mP8/hn+g2awl/s8DjojgY8RxGV1uJqBB3p9sSRHLPBnMn3C5jXTLxUr5rXyMSunCqe+jZpwUVTb8EHr/t8nzmvWfgz31rQKP2uvCqdejfX2IsG7aboEdAnnmRSyB6XtIl8rhWnziRLrn2DRcBfg4F0ci7FvFRLcFrTulQ7Htx1rlrMPxb0Q4/HA/qB9+yV4V5WZNce+dIjYxRXP+E174JYLrGzeKkb99qx86RDeTHAjfB5M4iYHvO5AtcvFfKHu4bOlfInhHtqByZYefw8Mo4BNvhxrrfKjtyeJgG0myHJMtBuRBkZuegIAXh0w0h8UdFI9vsKZrzfLC0YyWaFYk04bRTwoRGvcAg82SGpsWRwz7tcMyyNXa44OqfZoFcwL7QbxEof+zktPDD30uTkS9n7536/Gz197D3cdPC9Y9lx9HB2C/1GO/3sQu9B+o25e/PtB+eea8/1Q6wFbGyiItQVn+jYhbEf+PAiGE04KjlYuS17dHHcaAaAE5HhToTMzhzcwfAw3+ELrx8WY4TjCKZSi3p9SeEivABRdoGuX+YLAOQl3cBOfQom/kSfMGXifICYkXuHwVzD62/V2Mqep3tY7Hzdw+K5NbhpI1taSbz5F2wgtuCpPruVGCqcNxefq6sY87Ts3P6/jm/eNn2O8Z1cMF2fa4D0m/OOMjdGsGt4jHUXGGPqfGOsXzTG8H9vjEts4+cYavlS0/k5B3yO01007l+QcXdQx84zblz8WBqXYiyp0qrE7Y5hHncu5kUpzNwOeeZ28FItnCXks8QCnzCOre2ACMbo9FeyDedySmqFSFiqav7cPLvA7P4crOu54Iz/fDz89vlsgCLHxznCxwZqgNp9Pk5CgNcTlyrBU7UAC1csYaEUs5JsJq627YTDzgXm4a9za4xhJXP62f+Wkn06uPkcfPN+Fub5fEal8TPxEKIeok4rGMUGwIKUWYOSGmTXIJUGPYSuyt6UQEfRpYnszejKmux12WtRFF2NjiazN6Ijyewt2WO16MrstbJe383+mn0fvG0llaI2UGkblkZ1XhpleD7Xy60+QQA+npQxCcDqBnj14UVZd0pMCC+pWZuT8wQjuPBEwFu3KamsWjC9RHGC06MuSeXDrFyVKymAtuUFEQypyN6hII647Uje0Wqe36orG+0r3h09pDdZ647vOIS5f8l3R240+ITKN/Yf3bN5DT3b89JezP//2f3N7VgeY0M5Pne23ccbf7Ml++sZwuzm+hmBp85uQSWvPXFmlYKtbwZuz/XUJDDzH/xoFcYgpM8c2HEn5cddWT/ZaS5wvk5zJblOc2mry5NDc+ftNreATc/Td+7jBd9zoQ507FbZ3/zfpnPBp5yHTiQtciIXolRxWd5x5GgFv+Gkys9Pa/h8tFYs0Fr06bQu8Q3nI1n5CWdwYcKXOAAmR/8c0F9JtVDrPjkCsSwqNsQlDxit6hgpD1kYDl7LDVjnC8MTcJhYGGRbrkZcsqo/TW0+3TKdZ8Bzn2mJLjj+P3+G9aHl/nSgexbK/ckOdZ75DnXFn79D3UIu/fy96poXx/Dna1vHvDuPUxb6vHIgsb5FfV5nDEYSHRs0mRnGKbcz1sx3JOeAZNoYi4kcj0soSCdouS25cb4t+QVavu5E3Pl7vmZ/Lnd9zf4zOkq6vk5j2/29sx8o2tjXqF7q8hx1xZTcuQkgg6TEBbx9hKReQ0bslb+Zlnyjs1xVWiBkpnUF1eqw1AIhQkuUhAD4K2rr8HeVlvlT+Ks0JWUnvLYAlLAVV9Q2En/YWYG/eajAH5K/oWzRt5coFm04X1LwrVj8rRNW4XsdR57esubmddGqnlU9Vb667r5lKV/NumsHd3y1ycZyOkOweW1r48Y2b+PEronG6r7VfdVFrbv6eq7enFSgHU8eaqwZ2R5v2diTqmsMlsRK3L7y5tHGZRevinTW5fast6yq6hquDcX722K9LY1do/XFvW3hiok7Ns0imIukxxz57qAk1UbdfZ4uc3X462E/q9Vc+2e2mus4p9XcDGfx1zVhB3ehZnNSHQBcsekLN51bcAlfuP3cjvkmfF+sEZ3i5lzLvs/Fz8b/T/xsxPys++L8nK9J+8L8/PV8EdsX4ydzcb7kLc/P44Sfy6kHzsPP1OfhZ89n8rP3HH6+gPlZ3zbPUNEliA3nZWvqv8tW7GWj+Ct0EfGyX5i7Vf+y5hftvP5RJUsr6cdYTvMFmXzF7Kz+aYVaoaSfZlWLdPdWwusR6t0v3HESW9m6uNQOdncoKjXBhS7w3qsWsx5M78yIHKeNLBbE9DJXTB2e6ZJvdUVnlslHC/IZXSSfOkHkUlLXCER2Fn9lkwavSkhFMeFCqj/UDldaV6S+uJQuEPN9YWElLKE6n78pUVNQUYkazcGk39dYV1MQrqS/oNSeLWmLunwhX11VSWu0wFfqa4iQdUBZdkeI7Hqp9dTbX1x63VFxIi41AegaArFtWCw2vPWuHZBW+zkyG8Uyk/rhej/Ix7p4Nm1cJK0UlpbYbpIqsSvtFySLBu/MMElDE3KZzP+RZqOftafoC4ss+VmbkL6g5H716VuW5mX4cyLDPmrNeWfgKMZdTfL63afLc2awm2syhGcGcyu9Y0vnYb88xfp5aRjO2uWz9guYx/Gl00/sN4n+lDgszFgqm7o1nzEDRwfhSnvdf38Gnm8Z+QuL9NbCqtZAoLWqqEh+LWzIry1/QYevKGmucDormktKGiudzsrGknhbW37NmdhRpVGhp9qpYZiJIpVuxlJMxKXlMMvKYqTdn1gQJ4vy47G0xjovvZFAs9UQFlfEpREF7gaVn4YdIIsOXhqQJRMAmDoSwxEQ/tL3Yj5DplsHRb4yRBwQ0py1GReYBUySA7+uEtIFZaSMvtgkRapxSjuwHNdCwTHZ0iiIxbhUSjLN73JfEFCu7s9mn68783uXdCzFXwO/WG5NcBXle5guFpLOyAqDz+299m571Ss3DtywpU7Lza2rnrh6Rc/2ZSEtp3Y6+tbtrL3x7SrLmv3/q7dzD46quuP4fe4z+7jZZ7J5bTbJ5r3Ze5MseUMChIQkBBLAPARDERGCgBgEX4hCK0lFKyhi29FSFehUu3fJjNba6YBV207/cqa0U1un49ROM+NMy1inLUjo+Z1z95l9JNX2D2DvJsy9v98595zfOef3+3wfWoaaxLeluG1YXHn/iATNx5xgtlf07GzvPTgs0prOAyMBrvvJFyrESr0GNdmxe+99vO3g6/c6zAdem2pxlxfrCgF++uQ3102uzC9cuWtd03opp2bzkfXH+YquMdqweXqr1HjHCWDwzp/GDN5u6igV6oK2KpNklyophjfo8802k9evGRedNjfA8fmaMJsXjvxwIpppDidjttnh+FzgXWVen9jZhdcNzT5SatolQLn20ji+dLqTczYj4Lf2h5M5Y3fkiasrKgdzdSodn51XkV/f4vJ3lpeOnNrVlIb72zLIrU96TH5Y1X/8J9DvMUcXxb7A0cX17hGSrp8JE9wScbotKXC6rQpOd5a3uv2g1pAGqCv7YZRpXAJYN7pIWBJidyayQFgUbJflo+uC1L5p+N/6pgF841+Cb+hIwL8k39DqSLS/KOfQ12LqWsL+uYj9syLOP2JK/3Sm8E9XrH/qM/hHXKp/FkTuS3LTcGLUvjhn/Ts+WOcUfx3C/uqiNlHT6bnVsIc2JMmNKLjrQbPK5gTPAby6xYZxyXBmMoA+DkT9eRukAbWgUcrqroaTAFnnhfraL0u3zhSxLcmvY5mitUX5mdmSPkhjKBSI0VtwPZeBqlRyHGCvDkMqI4kOBpLoIFN6BU8an0ThiYwj7RMK7/9GL4bzKnXBFP2HhHtwKe/B6SNlPuEXF+7xYuR1tE9EashujJG7MLc+hRvh3AAr1ajkVMCeXiibjkmsMMQlVmix3iedrdyPTXwR8GZrYv8+NcG9Ftt5bwwphrK3PkN2XsccATvJr8A7n1aa5FeUkfyKPJJfEUUJgHiUMtFCfoU7kl/BJPQfeJzEPmZI6CbvTNRkQAvc0MPzJn6L22ns1j/Yv/MvIv/1ArtHhPevVY21sjFrjWw6BtCzBsywMw0KwzXK3uKKAFq86vnc0nIRxwSgjB2ianRx2s6OWtqLtYU7YDMek0s6YKs34MBl3gtlsQME7jLWuv/VXY17dtzmNj29/4KgzjradmKtTkBNMj47+B0Lb7xvxe51VS33yVO3f/+B1RNNE492j57YIrGm1tHDA6NPjNfSH2x7/bG1ec2jbT/+V9/pfI1Ol7W3uM7MmIysnbMa28SZAo1Gb9hR9/C59w89+ZdXRjofkvdufW5H4+pjP7u/fucGqW3PM6QvEwb3NOWgJOpkCuIvnFc4JblYNRes8+HkDeDf1CdQgFFjz0pkkSKZ4eQlRt42TAhuiBKC5VIJ4qp8CzkgV0DBch2gAYpqm1Ijg1Ot+ReihL0pF/XJIMPch0mX7mjuw+xhRQfOTw3H0IfLI3MfRhCLyRDEaRIe5HKY3GoWUV8dHZ8yc4m/HRm9MhKK2U0kAkpnY/WXtLEabCxfhI3RwGYR7GVHZPjMaCTTGYlkwnZeVHI6Yu2siLezKZmdaRI75IrF2rkgQMls7vbEUTuz0b0J24cR26cT8zpiKNrhvA5VsrwOw+LyOgxLyuvI4KoU73pmj+1K+e5ndt2hFHt4xH+HsP+aY/M5Yj0Y8AV7ST7H8mg+B3FdRXw+xyr0cVXUaRnyOdI7KlOsltlhuzMFaJn99qMMO2jQB/dRH3N+DjTuLShWq6VAz0CdNRcGPbh9siNrDp/mc1eDVlHOskGIAdOJwrigY8+Cy4S4q33s5ZuXY/l5sZ+ZE2vXzr9ZvsycU2KxenJMAZaOuSDvxyXOwHXgeqlGaqOSH+ILbzSUw0FlANcI54uy24ArVqBkR0CtB2eW9W5AnfF2p7GglIyC5T6SFuIs0JQ0xu0fBBQsnqL0oSYoPDo2J8ROGpiM+KOnlo3orRbp6bbl0ISv3DNk8Aje6dXdW+tEhqs93D82vcX31Mj02PTtvg2kqcTa+03Gy6uuHIb2Wr9PML+16leP7brQwrxRVbvi4Pl5d/fyqVd3/HwKxwGYF43GfwflhhP/eGK0k1H46BgbXZwCG+1RsNEhixMSGBLQ0VBOmZ8aIB2d4JKgpN+NzmjJoNLcufA6PoMdeV+FHXkC4XcntyM6iSVDYq+IzlrJDGFPxqy5w7aAhmj5Qlty4mypSGFLZdQWVxJbctLasmCiSmLSyQUzU1LDnoufjVjFtkPItkqqDXh7SRnlQa8v2CzJ+WiAqBOxpGjUSqCUF9twnhakzjTYMEEoxnbQGsWkKYsKzTogirIolHmmoTSJE57NOHYmdcqNjOMlQxjVqD9DFSdaa7qYKC0do6rD1ZsKqjroEoKO1MBqNtI7U6OrhUgfTQ6x5o5EO6mib8F/gFnuir4biNoSonUBlrbAKivkZcsGfTeLKEJqh0vRd4PXzZUd0XcrsMfou1kS9d0SRS0mVob2pRC0UDffPDh6d1jbbbB/XhOvZ8Eqvj2EV7et1EAsAxwS1ZtIkaKPFCk644oU65UiRbeiQlwlyBo7PH4mZDiToXelbpefZupkKZrr0wy9DHSuP9PcjfpYEVVPPaEojtkkuYydC1pEgnU0hivU6ti5WVN2HmxbmaA8iDDg3FbsGUDA2KtEEdZ6wMA0YrivERiYWSL6IGircE6lDmpZebw/lQ2YCAfoxYQodxUMUcZsZZeKZLAyjph6HLeA96iSyDmPvfznma3nZ/aUsSPhkpwvzpftmTm/dfqTl8d2989cmTp4ebqvb/rywakrM/1KwqR//NgwvTFcqrdp+NhY3c4rtPnC2WvnR0bOXzv7/LWLo6MXr5HYWfUIp6dEajXq56epUC14CcXKy9RQY0KwugZJ7kSX/eJst70WXNQN26AbsIsk5BKJnD3A7ki3CBskayDTyTyH4ZdtaD0s1wIZyo46E3JFcE12yOAqbyL5TUWg5yTbl6GomiryVEk4maQbJIOCnUqPU0ILRSko+UEQnSx65MNbfiMt+87deer9KuuaOx7o7f/615bpTTdv948dGVh15+pKfZbG5ewbv6tx+r3aql88v/2lfS3bKzce2Tj8yHBlJfoLfaxkVcydFWt3tvdODYskCvnuzMrJgcqYg5/wtt7zz518KUkUaQmf+7Ak7051k7Ki+a+ZGorPvIMQsVGSc9EbWk1ovLarcqENk6ItOBMPJ5BBzO23kT35xSbnpc8+TJ6xt4ga4mR5fNzQInKf3dxrTAPeC6yJaqoKCodEwEQkBQWXHVFX1TaFK6xi5m934mQdv/UH9/Jyv2MCaI3oovqooMUHtbg6FJc7fTgFwSCCTgPc0EUWfS6c2hlm9oFkp8EF77YFOqsTk7nt8WTu+IVc6i2apNsxNLWDaWS6GOgdFKwGdtB/ZBqHhoif/tufnWGq2beZKaIhSxYi8CdGQxb+yxm2lKnu6SG/z7+f+ff5OuX3j3PNdAP/OerHzVQw2zfLZlE6jmziooFBb5oL6XGBoh64MZR51mSlJORN2NnVk0NjigBsYVtRDaKAZH+xlj4+0J6nUXmlEt603G7lfjN4qs2i0qhV9XcFWjs0WqPK5e0nNu7namk3/1f0DG34GbKiz8BflU2muaDJPKvFNw5qfSEtrivTAr4OHsMEextZ5DECQDwhm56E3uwt208eocNhHejIU3PrNCppZ6ClQ6MxqnO9fd7B060WFTzD/HXaTc1+6WdwZH6GTxY+QrYK5jrUFkwPbosKtBZFTxH0SkqDBJ2RUsFUbRLUk1zZIvTzIpwWUORCP7eZZ0usVL2CjFLaTLaZUPdnIZemSAh6U7ZhaeaGpa39HXBZDwamamdvisZnoO2Zetz2FdTusM3E+UE3sTm9/+EICud1I7NzS+DbXBuwzXMLtMRkpW0gC88LeQ0gYJOir5SGv/SmbDzagi49PG1uR9ft+Sk6lCZpL8P2zl9n6nE/+//a6/iK7E3aebXJezToeZTSy9hH2G/hmsugETPz1ISZp4bXy4IHbK0Nf0n+wSJLdX6oAIqZ2ehS34bJh/Zu8Pk27G1v27PBx2xr3wvMzns62ibh20myhzN56xpvp16nBMpDNQAvEO+CuSUJnwjJjgpRJF/xsJXTGFt8iyYoOQ+2dAgdqxbNzAHC4ozn+ZSmvZw05hTbojs79OemnGKrpSTHbM7xWNH1PzHnJ3K9Lo7hU57mioyVL1In6Hcx99dNhd1nslFGDmf3QP0w6L+hKDU58DeR7psC50vuNYvu9SFm0MG9bGECnYBvh8c9gSj/paLPLQDNXUoDj6OpolvXuGn+DbTaOUaFeqCRmrVzIROE9oUotKfoHpOhKuiTZIqbC9aLs1oN/qJCAiI05tesw2+PbgCF+dWWObmkAbV2Nc6/qfbDS1JdBmDWagxmhXdJI8qDeIXajIbDFSvRUrwQ9EmtTqUcGY7NAp4GiYStSmINplKoieqBymbFwrjoIwZvcdGzam/R92iGO3fBPH7yrf2de7cOlRVxOq3G7hFXjbWMv3Bfn4nZaRJuhliaZgSzad5i6D1wdrxjW29Daa5Wpy0r3bTzwTX3vT29ych0t1rL7aK/9Ru/fXbQUdNVXcKrbYVlhbblD795uFCfXSfZvbbCLOHI5aMrnGXVZTk6j68/kD949qOn8JjTy47zpShGU6N34gCJ0mStTSJ+ZMUwixnAihqHiBZDVAHkJaEgVnVV5o1odYXRjDyLnKfC3lSB83hS9OwxYgVROGJzkFALKpucHkAl5pNCmgYC28SEY4fF0aioy3mEAOqanmIv6xB66Y9/vYY+3azTqT/S89rf81pdy3L+TxohS9B8ouL3tLbe/BsjoD/9nGZ+psBspKc03M1L9Hs18w+aaYF+vGq+GfoQDAI32BtoJPDGaCcqMkIQisJAQ/5R4iG/4Bbgv8DBMta3Zh/lf4n+3aqsNh2SInFti0pcqxLlra0ihJtwpuwwzIUVFSiidC07UdgZ0giYLSBrQGRP35Sgfu0B9WtVPu1WmKQgfx3YdWaiuMfJ0QZ9dfG5ILNx27yJqF9v3nLm7qYsnV+nfvUHw1+Uss+E1a/J81/i36GKQY28kMLLkZABWlxAMbJghmefzc0v1JDa/VxsExYNLMTGgPhtjhgqKMRigXmgCWGWzTCsGObwsGguQMboNValDCxsBEhIoecm28OxIt4NO85u86ztbrP1TgQe8PcfHqqmvfMfEju6Rl/Yv5xXcdf7+H2Mpm7s6GBXRMj7P61y/VcAAHjaY2BkYGBgZOo//7DZK57f5iuDPAcDCFz2z/KA0f/P/mvhyGTXAHI5GJhAogBrnAx3AAB42mNgZGBg1/gXzcDA8eL/2f/PODIZgCIo4CUAogoHhnjabZNfSJNRGMaf7/z5VjD6A6bQjctWClFgEV1LiVR2FTHnMCjXruY/hCCCRdCwUApyYEWyZDUsKKUspJuI6MYKuggGIl5Eky4WXgQjarGe92uLJX7w4znnPd855z3vc44q4AhqPmcUUCkU1CrmTQZd5K7bhLC9ij7nLeZVDE9IVB9AgmODTgpDahoxalwtln8xdpyUyJUKbeQWGSVJcpHMOitICWzfJ49MxnFUEU3uTQzYZmy2AeTsPVxy65AzL8k4+yX2/cipKH7rKURsB4qmATlfO3ISd88wp1coilo/x/YhbB4jaJexIGv68thq3nlst1twnud4ppbKP6j9zOGj3s2zh9Clv7B/GrM6g25q2NSjW42j0WzECXMSWeZ9x/lc/qBXvXO8cXuQlTgJmw4q5+i9yOpBRNQiDjI+pvPcM48GPYOgFp1EJ/dtUzHHT41z/xtSf6k92xnSXtGQ/GMUrjO3FneY/Rn06QTSHJuWOV4shDodRI94oh6gl0QZ+yR72004pAJ4yP4I47dVifklMGef4prHC5xi7fd4dV8HX2/5m3jh+VADffCR12Qb8bud2F/1YS3Ma9LzRbyoQbwQz8wU3kvd18MdoIoX9f/D2u8kaWelXCDfzVFE/vmwFtal0h6rRbwQz0Q3fGWuy/yHObFWO0izTgG+FqCq6izfyAJp/Qvy1H7qOY7xHVTh2hO8FxN8F0l5I5V3kiSiQ7zvu+xlxGWuuoA0mZN1mWfAPscx/ZPtw7xzI2j8AyV25OAAAAB42mNgYNCBwxaGI4wnmBYxZ7AosXix1LEcYTVhLWPdw3qLjYdNi62L7RK7F/snDgeOT5wpnFO4EriucCtwt3Gv4D7F/YanhDeFdwWfHF8T3yl+Nn4b/kP8vwQkBBIEtgncETQSLBC8ICQl1Cf0RbhOeJ3wJxEVkVuiKqIpon2i+0RviXGJOYlFiTWIC4kXiV+QMJFYI/FPSkEqTWqNNJt0hHSJ9CsZM5lJMj9k42SXySXInZOXkQ9SkFBIUJilcETxjuIPZQnlIiA8ppKk8k41Q/WWGoPaGXU59ScaBRrHNN5pvNPcoHlOS0urQuuBdpJ2l/YzHS2dJJ0zuny6Cbp79CL0hfR/GNQYnDNUMKwxYjOaZKxkPMvEzWSCyR1TA9N1pjfMWMwczBaYc5n3mf+zKLB4YznByswqwuqRtZl1j/UbmxKbI7YitpvsouyZ7Hc4THOscIpxNnG+4ZLm8s21z83LrcZtndsH9wD3Rx4lHs88ozxveFV4S3lneD/z8fLZ4Cvnu8mPyS/B74l/WYBBwJaAV4FWOKBHYFhgSmBN4JTAa0ESQVFBV4J9go8E/wnJAcJFIbdCboW2hf4JkwmrCXsEAOI0m6EAAQAAAOkAZQAFAAAAAAACAAEAAgAWAAABAAGCAAAAAHja1VbNbuNkFL1OO5BJSwUIzYLFyKpYtFJJU9RBqKwQaMRI/GkG0SWT2E5iNYkzsd1MEQsegSUPwBKxYsWCNT9bNrwDj8CCc8+9jpOmw0yRWKAo9vX33d/znXttEbkV7MiGBJs3RYJtEZcDeQVPJjdkJwhd3pD7QdvlTXkt+MrlG/J+8K3Lz8H2T5efl4eNymdTOo2HLt+U242vXW7d+LHxvctb0mkOXd6WuPmNyy8EXzb/cnlHjluPXX5Rmq3vXH5JWq0fXP5ZbrV+cvkX6bR+d/lX2dnadPk32d562eQ/NuTVrdvyrmQylQuZSSoDGUohoexJJPu4vyEdOcI/lB40QuxdyCfQH0lXJhJj5QMp5QxPuXyBp/dwTSXBjt4jrMxxL+A1lPtYz/GfyTk1QrkLTxPG+wgexlgNZRceu1jLILXpX/0k0MvdqmRk9RPSs1o9kHvQDOVjVKK6y75XPRxg5TNa51jPqHuESEcezWKblaGheQ8QVWuePQWBy/WfPMHnyRK2V+2Hl6JelbFZv42nUyJbUEd3I/hQqy6kwpHS2otFrNeXYtXxU2iFeFJc1VpRHtPTGdYy6f8LBrSvbfG03fVsc3o2bqWLLJUJfWKgDOmTYSmyUB7HREwRmDirUiJX86mE9tixu9wFp8REo86BZI+5mpdVv7Nn6I+9FcaHjGnVaC8s57G7yNLQ1PqH6FLl7T1ypmD9CW0No4iZKg7KJKtd87WzMGRyaFrvTSEV7JQCfroLi4is6zNmxL0JKlT9GRk5Y49b5BNmWdDvEHsaN3b+KZtCeYS1lHG0QmOa1jv1XDX6LifH0Hu5XOBr9ffgN/Z5lMhjRutBq6BVHTMmRlNWe7FSaebTTv1pnRXjNa/8H2NbPw4WXZXiJLVuPYVPnT0RtXLuRu5fscqI8IxYZaz5gDtdX4sW/W64nzP/FLWN6HeVoyUsp8wjcgaqN63pnPuV3oidb3Ogz/hj1lh3RMqYoU+NMXO7YG9Zvyb0MVhwRmt9xxk3dA5V81vrGHsuFZo57RNOkfVeHSFexj2dNWfO34TVx86HOlLfp5qtdH3CVzNhTiSe3N9VJx94hGSBqLJmwPeUsTfGimUyYVeExG7EbOeOjfVGiUpmS3maHK8wIif3U0yLGSPZG6yaGAWZN2K0asqun12+crp1zV3mlvCUqs40L3M/T/V24KxOnUv1yRXMyezsqSTCJSupmFudRu5aXbDSuFOscKU62YydM6GFdceQlUwxIQ7xm/PX9kldvx3anDZjaFxX//LszbG2PH0/X5u+h//xt8/etWvY/199Ma1XmMNOsZyy89u0GOGecWYeItpdeN+/gg/PZllVWn+96LdPj71puduX0alX/qFP/lCO8e/geiJ35C1cj3GtzvhNoqOTRedvQXaX7IN8CZUH/uaybh/9DeeiFNJ42m3QV0xTcRTH8e+B0kLZe+Peq/eWMtwt5br3wK0o0FYRsFgVFxrBrdGY+KZxvahxz2jUBzXuFUfUB5/d8UF91cL9++Z5+eT3/+ecnBwiaK8/FZTzv/oEEiGRYiESC1FYsRFNDHZiiSOeBBJJIpkUUkkjnQwyySKbHHLJI58COtCRTnSmC13pRnd60JNe9KYPfelHfwbgQEPHSSEuiiimhFIGMojBDGEowxiOGw9leMM7GoxgJKMYzRjGMo7xTGAik5jMFKYyjelUMIOZzGI2c5jLPOazgEqJ4igttHKD/XxkM7vZwQGOc0ysbOc9m9gnNolml8Swldt8EDsHOcEvfvKbI5ziAfc4zUIWsYcqHlHNfR7yjMc84Wn4TjW85DkvOIOPH+zlDa94jZ8vfGMbiwmwhKXUUsch6llGA0EaCbGcFazkM6tYTRNrWMdarnKYZtazgY185TvXOMs5rvOWdxIrcRIvCZIoSZIsKZIqaZIuGZIpWZznApe5wh0ucom7bOGkZHOTW5IjueyUPMmXAquvtqnBr9lCdQGHw+E1o9OMbofSa+rRlerf41KWtqmH+5WaUlc6lYVKl7JIWawsUf6b5zbV1FxNs9cEfKFgdVVlo9980g1Tl2EpDwXr24PLKGvT8Jh7hNX/AtbOnHEAeNpFzqsOwkAQBdDdlr7pu6SKpOjVCIKlNTUETJuQ4JEILBgkWBzfMEsQhA/iN8qUbhc3507mZl60OQO9kBLMZcUpvda80Fk1gaAuIVnhcKrHoLNNRUDNclDZAqwsfxOV+kRhP5tZ/rC4gIEwdwI6wlgLaAh9LjBAaB8Buyv0+kIHl/ZNYIhw0g4UXPFDiKn7VBhXiwMyQIZbSR8ZTCW9tt+nMyKTqE3cY/NPYjyJ7pIJMt5LjpBJ2rOGhH0Bs3VX7QAAAAABVym5yAAA) format('woff');font-weight:400;font-style:normal}.joint-link.joint-theme-material .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-material .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-material .connection{stroke-linejoin:round}.joint-link.joint-theme-material .link-tools .tool-remove circle{fill:#C64242}.joint-link.joint-theme-material .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-material .marker-vertex{fill:#d0d8e8}.joint-link.joint-theme-material .marker-vertex:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-arrowhead{fill:#d0d8e8}.joint-link.joint-theme-material .marker-arrowhead:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-vertex-remove-area{fill:#5fa9ee}.joint-link.joint-theme-material .marker-vertex-remove{fill:#fff}.joint-link.joint-theme-modern .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-modern .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-modern .connection{stroke-linejoin:round}.joint-link.joint-theme-modern .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-modern .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-modern .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-modern .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-modern .marker-vertex-remove{fill:#fff} \ No newline at end of file +.joint-viewport{-webkit-user-select:none;-moz-user-select:none;user-select:none}.joint-paper-background,.joint-paper-grid,.joint-paper>svg{position:absolute;top:0;left:0;right:0;bottom:0}[magnet=true]:not(.joint-element){cursor:crosshair}.marker-arrowheads,.marker-vertices{cursor:move;opacity:0}[magnet=true]:not(.joint-element):hover{opacity:.7}.joint-element{cursor:move}.joint-element *{user-drag:none}.joint-element .scalable *,.marker-source,.marker-target{vector-effect:non-scaling-stroke}.joint-paper{position:relative}.joint-highlight-opacity{opacity:.3}.joint-link .connection,.joint-link .connection-wrap{fill:none}.marker-arrowheads{cursor:-webkit-grab;cursor:-moz-grab}.link-tools{opacity:0;cursor:pointer}.link-tools .tool-options{display:none}.joint-link:hover .link-tools,.joint-link:hover .marker-arrowheads,.joint-link:hover .marker-vertices{opacity:1}.marker-vertex-remove{cursor:pointer;opacity:.1}.marker-vertex-group:hover .marker-vertex-remove{opacity:1}.marker-vertex-remove-area{opacity:.1;cursor:pointer}.marker-vertex-group:hover .marker-vertex-remove-area{opacity:1}.joint-element .fobj{overflow:hidden}.joint-element .fobj body{background-color:transparent;margin:0;position:static}.joint-element .fobj div{text-align:center;vertical-align:middle;display:table-cell;padding:0 5px}.joint-paper.joint-theme-dark{background-color:#18191b}.joint-link.joint-theme-dark .connection-wrap{stroke:#8F8FF3;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-dark .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-dark .connection{stroke-linejoin:round}.joint-link.joint-theme-dark .link-tools .tool-remove circle{fill:#F33636}.joint-link.joint-theme-dark .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-dark .link-tools [event="link:options"] circle{fill:green}.joint-link.joint-theme-dark .marker-vertex{fill:#5652DB}.joint-link.joint-theme-dark .marker-vertex:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-arrowhead{fill:#5652DB}.joint-link.joint-theme-dark .marker-arrowhead:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-vertex-remove-area{fill:green;stroke:#006400}.joint-link.joint-theme-dark .marker-vertex-remove{fill:#fff;stroke:#fff}.joint-paper.joint-theme-default{background-color:#FFF}.joint-link.joint-theme-default .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-default .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-default .connection{stroke-linejoin:round}.joint-link.joint-theme-default .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-default .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-default .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-default .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-default .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-default .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-default .marker-vertex-remove{fill:#FFF}@font-face{font-family:lato-light;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAHhgABMAAAAA3HwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcaLe9KEdERUYAAAHEAAAAHgAAACABFgAER1BPUwAAAeQAAAo1AAARwtKX0BJHU1VCAAAMHAAAACwAAAAwuP+4/k9TLzIAAAxIAAAAWQAAAGDX0nerY21hcAAADKQAAAGJAAAB4hcJdWJjdnQgAAAOMAAAADoAAAA6DvoItmZwZ20AAA5sAAABsQAAAmVTtC+nZ2FzcAAAECAAAAAIAAAACAAAABBnbHlmAAAQKAAAXMoAAK3EsE/AsWhlYWQAAGz0AAAAMgAAADYOCCHIaGhlYQAAbSgAAAAgAAAAJA9hCBNobXR4AABtSAAAAkEAAAOkn9Zh6WxvY2EAAG+MAAAByAAAAdTkvg14bWF4cAAAcVQAAAAgAAAAIAIGAetuYW1lAABxdAAABDAAAAxGYqFiYXBvc3QAAHWkAAAB7wAAAtpTFoINcHJlcAAAd5QAAADBAAABOUVnCXh3ZWJmAAB4WAAAAAYAAAAGuclXKQAAAAEAAAAAzD2izwAAAADJKrAQAAAAANNPakh42mNgZGBg4ANiCQYQYGJgBMIXQMwC5jEAAA5CARsAAHjafddrjFTlHcfxP+KCAl1XbKLhRWnqUmpp1Yba4GXV1ktXK21dby0erZumiWmFZLuNMaQQElgWJ00mtNxRQMXLcntz3GUIjsYcNiEmE5PNhoFl2GQgzKvJvOnLJk4/M4DiGzL57v/szJzn/P6/53ee80zMiIg5cXc8GNc9+vhTz0bna/3/WBUL4nrvR7MZrc+vPp7xt7/8fVXc0Dpqc31c1643xIyu/e1vvhpTMTWjHlPX/XXmbXi3o7tjbNY/O7pnvTv7ldm7bvh9R/eNKzq658Sc385+Zea7c9+avWvens7bZtQ7xjq/uOl6r+fVLZ1fXP5vuqur6983benqao0587aO7tbf9tHYN6/W+N+8XKf9mreno7s1zpVXe7z26+rjS695e2be1hq3pfvS39b/7XcejTnNvuhqdsTNzZ6Yr97i/+7ml7FIXawuwVLcg/tiWdyPHi4+rD7W/Dx+3RyJXjyBZ/AcVhlrNdZivXE2YAgbMYxNeBM5Y27FNmzHDuzEbuxzjfeMvx/v4wN8iI8wggOucxCHcBhHkGIUYziKAo7hODJjnlDHjXuKrjKm9HsO046rOI+Fui/rvKzzss7LOi/rsqbLmi5ruqzpskZ9mfoy9WXqy9SXqS9TX6auRl2Nuhp1Nepq1NWoq1FXo65GXY26GnU16srU1WJJzKJnLjrbczJIzTg149SMUzNOzXgsa/bGfbi/mY+e5uvxsOMVzXXxYrMUL6krnbvKuYPqanWNulbNOXcrtmE7dmAndmOfcTJ1XD3lu2Wcdt4ZnEWl7dMgnwb5NBgX/f8DanskqEJxD8U9kjQoRYNSVJGgymWlWyitxQPNk9Qm8WBzkuItVPZQ2ENdKyUVKalISUVKKlJSkZKKlFQoS6hKqOmhpjVrgxT1UNRj9lpKeuKVmCWPc5p7Y67aia7mI/zbQs0j1OyN7zVHYyFul97u5gR1e/k6wdeJuLP5Gm8neDsh05vN9mazvdlsb44nm9X4TfONeNq5fXjGe8+qz6nPqy80t8cfqPyj4xXN6Ugcv6S+3CzESjpW0TCovuHz1Y7XOF6rrnf9DRjCRgxjE95Ejo6t2Ibt2IGd2I33XHc/3scH+BAfYQQHcBCHcBhHkOJj1x5Vx3AUBRzDcXzisyI+xWfIXOOE90/RWMZpes9gio9nVXPK9UdkYYssbJGFLXHRe92y8KUZqMrCl/Edee5UuyRqPm7x/iIsaw7Jw4QsVGXhiCyksjARv/T9fqx0ziDWYL3vbMAQNmIYm/Am9jl3HKd97wymXOOsWsE5xxfVn1HUR00fJX2yUInbvdvt7MVYgju9lqr3tJXl4l5n3sf/+5sZdQOU7TWnBfNpLo2xyhiD6mp1jbpWzTl3K7ZhO3ZgJ3bjLeO9jT3Y277HBvhbpXyAvxX+VnTQp4M+6vuo7+Nrha8VvlZ00Rc3Ut7vyv2u2u+K/c7sd2a/b/b7Zr9v9sddnM9xu5fbvdzOyXsm75m8L+R8TsbvkOtUrlO5TuU5k+dMnlN5zuQ5ledMjjNZzbif436O+znu57if436O+zk5S+UslbNUzlI5S+UslbNMzlI5S+UslbNUzlI5S+Usk7NMzjI5y2QsNWu9ZqvX/TqHO11Wr/m4xfEirMcGDGEjhrEJb2LK987hp9w5+a05vTKfv25e0OsFvV5wD0/o84IeL7hXC+Z03Fo5bl7HOXuSsyc5e/Kac3nAuQdxCIdxBClGMYajKOAYjqM1zyfUU8YtYxpVnMevYtZXEzEXneiKe3SxMOart+upW64XYwmW4h4sa74gmX2S+bpkLpPMPh1O63Bah9O6m9bdtM7e0dkRnb0TK429yriD6mp1jbpWzfl8K7ZhO3ZgJ3Zjn7EPGOcgDuEwjiDFKMZwFAUcw3Fkzjuhjjv3lPHLOO1aZzClp7NqBeccT/usivO46L07zPywmb/VzN9q5ofN/LCs9lmHSzqs6rCqw6oOqzqsSsWwVAxLxbBUDEvFsFQMS8WwtbFkbSxZG0vWxpK1sWRtLFkbS7qq6qqqq6quqrqq6qqqq6quqrqq6qqqq6quWnNXlbJbpYwuczJpTibNyaQ5mTQnk+ZkwopR5eckPyf5OcnPSX5O8nOSn5NWgKoVoGoFqFoBqryajGe+vldv/tb9mrhfE1caat+vi9UluLO51BWHXHEoHvvqfzzp5kk3T7o9l+51Hyfu44Q/3e7jhEfd7uPEc+kh93IiEb0SMeC59Gep6PVcGpKKXvd4IhW9EtF7zXs95/tbsQ3bsQM7sRvv0bMf7+MDfIiPMIIDdBzEIRzGEaT42HVH1TEcRQHHcByf+KyIT/EZMtc44f1TNJZxZb2YRhXn8fDlJ3/xqid/nrM1zuY5W7QC/pCjRU7ul6pRDtY5WOdgnYO7OVfnWp1jZy4/sWvtJ/Zq9dLTusahIoeKHCpyqMihIoeKHCpK3ajUjUrdqNSNSt2o1I1K3SgX6lyoc6HOhToX6lyoc6DOgToH6hyoc6DOgbpu67qt6bZ21ZM3f9WTN6/7mu5ruq+1n7wvc2ABBwY4sIADCzjwOgcSDrzOgQHZystWvu1Ea3VZ5L0rK8ylfF1aZS7tfRLuJNxJuPOCfOXlK8+lRL7ynErkK8+tf8lXXr52ydeIfK2Tr10cXMDBhIMLZCzPxYSLC7iYcHGAiwNcHODiABcHuDjAxYFrrkrX3vMkHE44nHA44XDC4UTO8lxOuJxwOeFywuWEy4mc5eUsL2d5OctfXsESziect9Ok9wym+HdWreCc42mfVXEeF733Ey6nl10tcLTA0QI3C9wscLLEyRInS9wrca7EtTLHJjjVWptT7qScSXVf0H1B9wXdF3Rf0H1B9wUdlnRY0mFJhyUdlnRY0l1JdyXdlXRX0l1JdyXdFHRT0k2qm5TqlOqU6lQ6ZrXuFHRihQS92PwvNTX7m6K9TdG+pmhPUrQnKdqTFO1JivYhxfiuM0ecOWJvV3P2iOfRZs+jumfRZvu3mtEaUpAZrWEv1xpxxIgjRhwx4ogRR4w4YsQRI47ETXK7XGaXU7W8ndlWXlc6HsQanMYZXJqH5eZheXseLqrz+ZvxN+NvaxfT2sFkvMp4lfEq41XGq4xXrV1JxquMVxmvMl5lvGrtQrKY59rrXHtd+5lzrWfIlO+cw/fdbYWvz7rF8aL2fDfoadDToKdBT0PiCxJfkPiCxBckviDxBYlvzWuD1gatDVobtDZobdDaoLVBa4PWBq0NWhu0Nr5WcP3Xu6UrO6EZ8So/5+qm047iZv54asWiWBw/ih/b594Vd8fS+Lln8C+sGff6LX9/POC30IPxkDX0sXg8nogn46n4XTwdfZ5Rz8bzsSJejCReij+ZlVUxYF5Wm5e1sT42xFBsDE/eyMV/Ymtsi+2xI3bGW/F27Im9fr2/E+/F/ng/PogP46PwWz0OxeE4Eh/HaIzF0SjEsTgen8cJv8hPRdlcn7FbOGuOz8V0VON8XPw/fppwigAAAHjaY2BkYGDgYtBh0GNgcnHzCWHgy0ksyWOQYGABijP8/w8kECwgAACeygdreNpjYGYRZtRhYGVgYZ3FaszAwCgPoZkvMrgxMXAwM/EzMzExsTAzMTcwMKx3YEjwYoCCksoAHyDF+5uJrfBfIQMDuwbjUgWgASA55t+sK4GUAgMTABvCDMIAAAB42mNgYGBmgGAZBkYGELgD5DGC+SwMB4C0DoMCkMUDZPEy1DH8ZwxmrGA6xnRHgUtBREFKQU5BSUFNQV/BSiFeYY2ikuqf30z//4PN4QXqW8AYBFXNoCCgIKEgA1VtCVfNCFTN/P/r/yf/D/8v/O/7j+Hv6wcnHhx+cODB/gd7Hux8sPHBigctDyzuH771ivUZ1IVEA0Y2iNfAbCYgwYSugIGBhZWNnYOTi5uHl49fQFBIWERUTFxCUkpaRlZOXkFRSVlFVU1dQ1NLW0dXT9/A0MjYxNTM3MLSytrG1s7ewdHJ2cXVzd3D08vbx9fPPyAwKDgkNCw8IjIqOiY2Lj4hMYmhvaOrZ8rM+UsWL12+bMWqNavXrtuwfuOmLdu2bt+5Y++effsZilPTsu5VLirMeVqezdA5m6GEgSGjAuy63FqGlbubUvJB7Ly6+8nNbTMOH7l2/fadGzd3MRw6yvDk4aPnLxiqbt1laO1t6eueMHFS/7TpDFPnzpvDcOx4EVBTNRADAEXYio8AAAAAAAP7BakAVwA+AEMASQBNAFEAUwBbAF8AtABhAEgATQBVAFsAYQBoAGwAtQBPAEAAZQBZADsAYwURAAB42l1Ru05bQRDdDQ8DgcTYIDnaFLOZkMZ7oQUJxNWNYmQ7heUIaTdykYtxAR9AgUQN2q8ZoKGkSJsGIRdIfEI+IRIza4iiNDs7s3POmTNLypGqd+lrz1PnJJDC3QbNNv1OSLWzAPek6+uNjLSDB1psZvTKdfv+Cwab0ZQ7agDlPW8pDxlNO4FatKf+0fwKhvv8H/M7GLQ00/TUOgnpIQTmm3FLg+8ZzbrLD/qC1eFiMDCkmKbiLj+mUv63NOdqy7C1kdG8gzMR+ck0QFNrbQSa/tQh1fNxFEuQy6axNpiYsv4kE8GFyXRVU7XM+NrBXbKz6GCDKs2BB9jDVnkMHg4PJhTStyTKLA0R9mKrxAgRkxwKOeXcyf6kQPlIEsa8SUo744a1BsaR18CgNk+z/zybTW1vHcL4WRzBd78ZSzr4yIbaGBFiO2IpgAlEQkZV+YYaz70sBuRS+89AlIDl8Y9/nQi07thEPJe1dQ4xVgh6ftvc8suKu1a5zotCd2+qaqjSKc37Xs6+xwOeHgvDQWPBm8/7/kqB+jwsrjRoDgRDejd6/6K16oirvBc+sifTv7FaAAAAAAEAAf//AA942sR9B2Ab15H2vl0sOha76ABJgCgESIIESIAECPYqik2kSFEiqS5Rnaq2bMndlnvNJU7c27nKjpNdkO7lZPtK2uXSLOfuklxyyd0f3O9c7DgXRxIJ/fPeAiRFSy73N9kktoDYeTPzZr6ZN29A0VQnRdGT7CjFUCoqIiEq2phWKdjfxSQl+7PGNEPDISUx+DKLL6dVysLZxjTC1+OCVyjxCt5OujgbQPdmd7Kjp5/rVPw9BR9JvX/2Q3ScPU4JlIdaQaWNFBWWWH0mbaapMBKLoyJ1UtJaM/hn2qql1GHJZMiIpqhYEJescOSKSV4UlqwmwSQZ2VSKksysYBJdqarqZE0zHY+5aauFo/2+oFmIC3Ck8keY9zmnz2r2u4xGl99cmohtpBkl0wE/9GD+qsXn4hJMHd0792JkeHRDKrVhdBjT+zLzOp0AerWUlaqiYIBUWNTHZ1R6SqMIi6YYEm2EZobPiAwv6YA2js9IdhSmqqoxCSoOATGhkoXDl0c1NGfieBp5ckeM4ioUzr77kGCxCA/NHxF+jVGUYjU8P0HVoyEqHQN+iSXxtBHokHhzPD5To4gZDeFp1pOsC9jjUo0yMx2oqIwH7LEZrYrcUrpT9fiWFm7pBJMTbiGxISqWnZRKjJl0SZk2PN1a4tPAB/OSGQZgM2akRhQWE65Xmx/7ww8pa1grxiKcqD8hRdSnWJE/8WrzbX+YItdNcB3+LIyvm3jJqT4lxvhpNqY3w4PJbx3+LUb4aSHCm/Ezpt0lTrjuIb8D+LcY5qcrwib5bZXkbfAh8fwfJskVeE8dfs90Kv/OenydodL6cAT+oVYrq9TpeRih2xMIV1RGYvFkXao+cr5/YqsLy6cRtaC42ZtM2OPmZtSAGK85HrNaVExcpQz5GThWeRmQWW1N0uxlOBRGZjgr8Zq9YzTzL6uyc0pF+T+NK5ym8GZUvTlcjMb/XcmWvbHqf3jY7H9tKufMaCz7D2OsUwhveo0TUAJVr8r+A/oNq9Xy6K6QD6GHzZZsA/obj1qR3Q7n2YOuymy9IKgU6L7sVrsJ/a2hHt1FwSx8MHtK4VceoxqoZdRK6m+ptBVrIkyKdk1GDIJAh6Mif1JqFDJiIy/VgRRrOBB3TZ06PLOSo4pBWUMxsYaX+uFWRMhII7KAW/5j9hksSIUYAkm6Tkht7CnRdoKdtrbZgMshfrog5AKmB/FvsY2fbsfXGWra5gq1Eba/aLW5CoJt7QuclRpBCKIyJenq4FWbklbWwGt3SuwXRH9KjJgkrxtmblV1C0rAhFXYzRGmFiZvC8IyULmRXaX0+yJ0iHGzeDIbEeZ8MoLMFjdtN3MMaob3w/0HC/SCpjBU2z2R8i67fkdr7c57tmiQ0Vii3/Fgm13L68taN3a4q7aM99cVN+5/fKceGQ0l+mPvjFau2J4qWnHxihBKDl+zprJm9f7m50uNNl9pwMXQt9lqR46u7z62s4X5Omf+vmqg1S94y4Ls3EtGX1nt8g1NYw9e0s3+1GD+s3KS+X3L2taIha5VVA9sOfPXbN3aI12d69srzBTFUuNnf89+m32FMlMhsB2dMJe/TKVLYQanW7HZ62Uz6QqQYprFk9nPZmZWJVpZQ1haBYdOIzl0shkkjhMLYzFmRAsvuUF+WjjU8lI1HHbBYRcvDcJhA0zbCXh1WwRT2siWplIpabALjhOtlSlsKVf1gtFsqIbLficcaakUWE3zOVYzQieBx/FYM40Z7PdxtJkIBSn96DPeOB4dPtDSsn+kqnrVvuaWA8PRwUDTcCQy0hIItIxEIsNNgTKFUWnius783mCjV1atPNAK745Wj+xvajm4smpFoHk4GhlpCgSa4N0jzQHFwMQtayORtbdMjN+MX28eHzzQ7fN1HxgcPNDj8/UcODPJ3qPWnt5lQmMTt6yLRNbhd05EIhPwzv3Lvd7l+wcHDy33+ZYfAju69+wH7GGQRSs1TF1HpeNYCo1YCstUmbQBC8ANB24D2ELKbdOALxohXG8Dn9PGS2rgqx/mlh9MHByawNqDtSvHcwms/Sp4dfoF04yBbVy2ImBPiSZB7EuJ5aZ0qDpJeO9eBrcpdXUS35a5Dgpdm+OpXYk1PhiKMJiTVovNDlxPYsZzSIWdRhRxzGKmJ1EwxDF7a9dd3dvTU7P5xpGuy9YmaU7vMKg5RuVvHG9s2ra8dPVa9K1IUk3r9Sm6qwVVrzU5+B9F9l37lZUDX71k+dbGzYfrl199YH0oW65kO/f2l6GLem/cP1Y4fP/Y8ssm4tGhXSlGwRp0BV3N4WDXhrpV949lm3of7TMYN31vffZdtfHvayfaAvGtf7Fl8PBgyNswWI3+nlUVDW0+CK6LQth3IgPxnX7Zc+bcJhJ1eZ9JfvRLneW8h1zkF+HzvpH9kEbKAsoJMwqJLvIZBvj7AvnvMUvtNrDeSuCgCR8ZUYT5hrttajBsUF12xRWXq7jw4FSbm77hyL/+8tdHC1RGre5vsmv//d+ya/9apzWqXUf/9Ze/gudMZj9EL5HnJOTnaE+KVGzGIJtRAy+xsgrgB0sGLcwwWm0HKYusIDLYrtlrkglTbQ0dCoZqWpCbwVNGFQpOqi+//IqjKsSFV0y1FxW1T60Ic7/Q6v4aPflv/46e/BudllMXHP31L//1yJFf/fLXR1wqzMOrmHvoNHuKqqWSlFgSndHoKRXmYCIqlpyU1LFYbCZA6JK09lhMSgJFgRLBNM1yxWWgaZgvSTtY1AhqQnGrRalqBpdnBz6DmfUgVSiCQm5UhPy1NYkkh4woBFoHihm6quAt3sKpVbWsWm/l33KdMBaYTC7+Lec7RqtBiS/rbMYTrrc4l9ns4tiByEGt2WR2m/75n0xus2DRHIgc0GhpRqM+ED2oEQRTgfDP/yQUCEZBs7/ygFrDMFo10ZED1CuKasVfUjqYlyIVFVVxCSkzIhtLUwjjEkqrCacRhQ8Rg6elnoiDjkkasHyKWFqjxfc0KnibVoMPtZQGpCKrRK0XlMpr9Qp+4QB6eQi9ku0eom/pQ9/PxvqyVegHsp4ezM6hIPUNqoCKU2knNgqMHsxuIVYwkQPIC3gU/xQBc5UUuDIbTGjGSXwchp3gxGw5EWM2NjNJosYHq0srqmxlKb9RrVRoi4udCqVRE6xaE4g3VpePjazwGtVaVqvQlibbSmg6LtOynU7QHfQt4PF9mB8S0mTwDxIVUYlC4RnGimcQ1kB5fNbt6Od0YmQE/+0UYOsyGIdAlS1C1vkDhFH0ArrGSI/6BGieOhcpnwuP4Rlnz5x9lv5H9keUmjJSIhNFoiYqacknqVAC/ASMnKWvNJaWz12v9gqrlXTwNGWxUATL9p39UDGe84edOQqdmkzO/6mBwlLZ0xkWPJ05I5XlfFoO75/ju0zNCKhHJquFxjyPoE+4pb6Vd7w+NfXGHcPDd7y5Z+r1O1ZOdh66d9Wqew915l/pd99E9hfHx1/MZt58M5vBR8j+pnTqkeXLHzkliacf6el55DTm7yxg8RD7TYqnAIkrMfUqFaD+GLFt05wSqUE/haioBtNmyKQZNVZHhgXNVDP4UK0EzTTBaBg16A6CsSAODnR4JIjoKehrTRJ8rS80ix7vQ01zVjTAZN/SwrRRNKFDpx/q71fc4w9lfwNmAFHXAz1h4GeMWk+lKUxPpTaT9mBuGrHKxKOiS+ZmeSztsmASXDA5MG+12E4YMlIN5jHmLevBvK0E7ZYU5WDKjMI0a3MFiLOKY63OYS7MUuKr/KFmJq84KvBWcW/MVoSu12nQfzbtGqioHb+4teui8Xq91kMr6Wr9wOH7xkfuuagjtvpQc7be2x2gD/IWv86hRv/VfPjSK7qHLukPlPfubAog9fovT9ZUbf7y1uHbr72sJVutVpv5FJkb15/9QBGF8S6nbqfSnXi8HGgP14kHxoFxSMeIImkAPTk6Y3n01BMVK09KpcCFUlmnkiAbdxL/kdsB3HDzorn4pCC1ADt64XZpJfCAUQMP3MI0F2vsxGZUcoCkJKoFrjoFsTEl+k3p8krs2rGBxQbAg9zsvN7VnsusKFrEKzfKI6jrQ3q9zsKqlbZA7cDOjnW3rY+Ub3nskg1f2lQdX31Rc9dFYw2c2q1iY4b+w/ePj3zlQGvFwM6mRx9ffuXxySue3N2Atgis1mgxJesbIoVNGy9Jdlw0XL2Mjgztbx842Osr69nZkmMnxkbdh1bXG92v3TF+7/7m9j3Xw3xsA/05yj4H+myjeqm0DmMi4qYNgg4ZwiITlwyg4GqILuxRUXcSwl1JC8gHjK8D640up8WCAQ6olIgEsIx5XbYowwjMrhfceRK0OpFso3+6BmkMxt+NzY0aBWYzvZdm0G+Zd2Y7EjpDdhN61KBL0H8SSi1E1veCrBWAHaLUP1HpMJa1msmk7VjARdrMjNcUtgOF5rjkVWfEYqCwKioaTkpBEGJ1LnSd+yOJbEQ7BDYQ0UhFmlOc6D7xquFXb92Ib7BicURyF6nhGiuZbXDTekK08tMWq9kcflX7lRO/gnfpQD+mPe5iczgNv4tvLb7VrwRVSKXhXfBCzVhtbosnIgegGqvNXuQ2WzzFiwNNBFSB8jiceIaZYOqnKSZINEeOfxaZK6UqZMas83sZYtjmwfa9hVqLITY41b3qy3uaIuvv2lR/fU/rIfq2AvfcH9d0XVZ38OsXNwzd/OKOxr2bhg6WGj0l7sT2ezauOLa+BpvG68othdkiwdh68aMbLnrh6g5rIIrt8W3A4yrgcSFEJ2DRHJjLPnUmrcQ6wFU4lDCFOCVMoWpilotgChXxUghEbwY2x+A1VARQQ8c5VGSOVPjw2Mw6eVZgmyF7BNW5Y1lqoW9bvRXdJvhXZ4eKa22NT29Z//Ch1u4rpV3bnjnSvjG+7oaRsTsma2s2HRuauHNLDfr70ZM30BbH3PfKewPN3U0HHt665amjHW2XS2Mrb9maTG6+cXDkxvXxlq1Xy/70BtDxHpJvci3ScMmoJf4w5wSxHwVoRMJMlEiCzt7A/LVKObdTXWhvpx8ymGbf0PHs7pYKwaU5/TPeynoKrDz+fIa6HHhYBjYpBJH5IPUmlfYTOwyxBEnR9CkzM21JvxF0tS4utangqUOEmbI9Ehux5dHCsTYqNcomCvPVbchMW9wxNYQncHFZFBtxaaWs18Lzb1+J1ZcTWV7sOCGl7KdEJwTsdSknCcxZZ6qDqOMM66yTD0lQvqwRZGX0VyaJrJLYyrnBi0p9bXBk0abmoxKmdhEmUMno9byR4ZLzyyOrLu5q2drur9/7wOZND+xt8HduaVl20arosiue37nzG5cvm6zdcsvIyM1bEsv2Hmtqun5qWTQ4dNmqkcuGSsLDRwYGjo6E0dVDV65r4k2tY3uaB26aTKUmb+5vmhprNRmb1105tO7uncnkzrvX91wyGo2OXtKz8er+4uL+q+md9XtHY7HRqYbmqaHKyqEprNsiyD0GcnGDdwTdNlP5ODuizsy4AmYcXLtUspMEcXiAzR6eQA1tzi2WeTCMtrvMhF+RAOi2lrKnlsbMKgSGDkdrBH98gkli1+XHJzc9dnGrPdJenr3e6B9DX/fUWBuObxq/Z2/z5tj4Vf1rbtlQFV93Vd/QjRsTCuX6Rw63tx15envdju1TTXM/dtCrwwOB9uUNU/dNDl0zHm3cdKRpEKZ1fN01BFPdDZhvmPkF6LefqlxAfaI3Ktkx5gsQEIsNtzUjFpIXqeR8yE849/Ru42IgmDz3bEnWdGwJSiR0AaaW6aqkOnIW3Ap0GaMyFo1ERdNJiSqGmMUBlGnJixQFvjtM8+kLSrKGwbU4PpGmCJovBLqX0K08PwZnrj6H5DnqUzH5E8jIPKEYBD9JmWsRsRRKFYToOHB6gqH0/Nx3fKVhD50wGugHytGtHTpek/1XQavhs79UC7oOzI9n0X8yp5jLSD7dJSN7CHMA1LNYCdVRSTNviRD8PMsMzkrMIPrPvj7U2t9P6IB/RgWS6UAEkiVwpIaCTQhZEdIb6WRxmSUgzH27gKGQsUNnUqFiXsNyauTmbB3ZS8qBDt/ZD+kfwLwopeqpKSpdh+US0ecwuBdj8IaoaD4pmTic4Zi2m+IcTAWQUFlUiltJ1qMQTxKBpIglkxlPEm+kDic94oLIp8RCAOrE1XkjcI/SmoJyxmMeAimMyB8CG6PIzxGAu0vE6yvsGtlSv/yqTXVVvav7amh9B1vdM9pTHe7dVNu5pTOkMqpf5FzeRZEKGy6Ml9rDQxctX3FgtK2u3vfMN9nylsamgcmu5Jomj78ioD8zcB493X9WryxlR6gV1Gbq25TYG5Va2Ey6pRfDw5ZOgIfGqGiNS2FFRlwVE9dHJQ+bEWtBbBhabiG2ox5YVc9LLmDHIMSkgzzG+DNBOVsQ5KUqzC8uI22V7XdT5vffku33OC9OnJD8ylOi7wQ17fOPTxC7PX9EsINpUDC9yFo9tS2964GRUlUQT4/2bjI9jC0ksSqth2nygpZymarqc+klUyKwiJ8h2TjJht1mZzjQ4nPsFMIpE5siHktgMOtBSoXfFwjSJfl0kzmCsKT2H/khsj9yy+xbFzfsvG1wYi2d+otVqVV1Be3XvHZJYlNwvV5vD1a76vcMV2197tfX3D77xoGL/w5pvnrvme0qHafkL8q+/8zx7M/+8Ur0nqWssaxksKfFNuys8a+7Z1c9HXsOlbx32ejx008eePn6no3jG0dLuzYk13zz9jGTKftQtM9dWefVNR36y8l7//VrPVPvZD967IXs+69sXNbOcsH+4anvo4o1Zd1xt7N13yhqUqn7jn4NyxcMIusC/28AjFshR0mAa2WYq+EogLmSBs9AexRj2lxEZsZBD4qTXBSD8/5+sxfBVAMoY6RX7qJXruTM7HNzdc8qLMYP6VuyP1VxahWnYo+fXmM0oCeza3UCzdE/EyqdTpwJxjjhPfBHXwM6LJSHKqf25OI1K8QvBI+UQ9BS7CHkFGNywkSzrGaMbQGTkqSj0ZyZVhmdAAqCcD0YlVQQHFfAjaAVaNaDOnjwgTElFgtwKpabRBUeiOBdEnqUeGMJIneIN4kKBP3e99BjV7xwaX1p/97u515pv/LFi7NfRlN/9U7Nli+tzX4FNUzetTb86lvZv2OPV2+8dU1qz0S7yfXNv1j3lR2JVU9+tWtff9lAfNWeui/fQ+zl1Wc/YCMkLo1T6Qgep1ubszAW7bzLdVqIn6Uki1swzWgpQ7DsXN2VVwEUckY0p4cYSXrkXCiir97xOmIfHjx2cFtVsdqkKapoXn2w+/pfPDIx/sBPrlhx2faxMKtValVllbuvumfintMzk/S7TyL+r/fYK9rDEb21OFhsXXv8w6/e/+HT46COIYVSVVE1kCza9TYyEdsAMmMfAJnpKSdVl5OYgclJzMlk5nOQIA6DvHSmssjpSMmJY6J59ucTFCXe/JTzvkfzD2Rf3LbtxewD2Qn01LGf4mTET49lJ9jjk29k//j0M9k/vjE5uvqJ39137++eWE34inWoAejRUd05ajR5ahRMZoZVE/1hMWF6QpjGLKfISPpMowNrRsfkXFkuQSYnx+Sf95jJOSV92dyN9Gn2+Jq5F0fnnlhDnfNcDdUqP3fhmWqWPFONn6k9zzMhKs89ULfkgfLj7p6bwg97ZM3cdmped7aC7tRQ+6l0FdEdZkF3ZkrKqjByK8GOqjavRqKTl/zA/DAE9v4wfq6/FJ6YwDl7J1hLga3C2dmwIBm02GqWgMKJ4ZRkKSMOyuA8j97Np+JziocD2SbkFbDqgWG8evsbyPD0yO1Hd1UVagSN2tiw9Wu77/jNo2PjD//LjX2X7d5Ylf0PHY++lDh8w33rHspmX91Ov/sMEt7eZatoK680KpSV1aGJZz685/6Pjk8YPRUF6CZOk5qbCzaUWnPqJ/OdrSXybslZLpVsuUQ2PsNoCecZ1by0dWYcmos6sloBMiD2IS9nvCgfx/G48N5u5rZdu2YPs8fn1tFPnF5DvzjXKz9vDn5th+cxlHeRnHHqkWTr4dPwDzv/iXO7sMWT/3bt2Q/o78LfuiAOkiNJHZMBWkQljnAoiCoF8lkFZJnSDJ9TiKeJDqdTmZSoFEQFzqWSVY/5mFhewQcrvJZmEK3nNK5AxL3iyrHI7qb9j01GNhq4IqOGU6lV1dse2Ml8a7b+slevbuUIPX8C3vnY5ygflcrxzpbjnQF455V5h7XITwbnI7yTApgmxgs0mVLyGOXFFrIERnLmduIUUIQJI+FPO1ebixwWPb2cL7SOzt1kdpttPoF+cLTAZph7QGe2e53rwU1sZrScjh7nublLLKBbLuvccgCKh3SCjp1blpMz83vgHZv3UBKTm9dIVOZ5n2aofDpRUi0I1freTloEMYjj8zqj3A+f5cnPVVHIjdsYz9dXeAQS7OBMpAA4DtdTmCDYEdU4I4kzgOrClDx8wArIZgehEA6A+uDsZBj5QshmFd5bzgkaerlRrzRo6JRa4HrWK+b+hivgXca5Fxn2uNIwyxd5eS/H/N6gPL1G8eOColl9QQHzX+6CM5WL9duUt66iLkerBmg1E1pNAsGceP1NB7RaiI/GNCqNi2gMYlXx58iKA1nMs8y6mIObHQY6VPozDk+h4sTpNRbFf3gKzjRi237V2Q/ZXy/NRee9lF+7kIu2LOSiLf+7ueirtr2UvRes/uQkWP375l7atmf0gZPXHnvvvlWr7nvv2LUnHxil330arMTuXe9kfw8e4Pdv7wJrIDxz3wfPjI0988F99374zPj4Mx9i+kG/FfuIb7JT7Yutsh2QhM5A9FuHk8AOMgw9dlExUS97KRamnxNz0o69FCt7qWIFAQdeJ5oHBX9Cl1BnEdN9w19dmv0D4jbds7vu+9/N/oE9/i//sPHRi1vnXqYfrN1wTf/TMzKWvir7ltIDPMX5pMF8PinP0wrtQiLJMp9IwjydTySxVoeRBNs+B5BlTYkVQlprpFJL2YuDbjILP4vNFcOHe9HRMYtPn/1u211Dn8nxfW89fm0ku1fHoRUFhefnfJ73Pwfe28G6rM1prkHWXMkH7Lc5CPttqnnzYgf2O2KiXVYkzP4AViQ7aI9JKy8cCjjJbCP1EqJPyAslF+Pa8mYHhZETxRfkc/DMn1NT92xymtFHa3mHLlsllJa/Obvpvl113307+zF7/O3XRm7Z2a41uubugPiwz26aO0j/PLL6aP8DX5XtxfjZD5h3QWZN1D4q3YAlpgXbo20gK2k4p16ER1UK10qL8LVSP16Ea46KjpNSpSEjVvKSEYaSMGSkFnitdJBVMdEovKC1FJXEGnBcmDCJxTC6Ui12t47iBHG3udqPnNyU+dBEpVT5ZCmC61XmwpfxIj2vKSqr79vavPqmDdUt26+75bodzcndD00enO51agRD+fKpwcFLV5Y37yB3mi/9+v67/uH5SqMjUB5w1Exc0T2wtb0ynBi+YkPPjTubu3ujAgpGQpUrttf1buqMVCaGj4yvfezSzm0yTwIg31tAviqIkck6jyxaisGLPThYF5UnsRDTrBKzhMVsUrL4UInXHhciebzuGFBsyzI72aHx8dMiO0Q+/ztnf8+a4fOdVJJKW0luWyvbe5GL50ElmHxcUAb+W+LNuaVmhkyL3Fq5ZYmTjNDf2dV08KmdO5+8qHFn313fvfrq793ZT5cx18xeu+2b1/Usv1bcBsfXHPnB/WNj9/8A04FjIyfQwWN/z+NxUrKDxKtY2D1QEsXnYKw55wsSOWfoN45ADIT+02zQmdDvWLNxeO7ZDexxo+HMimhtslKR1gkADcBSU5Tqx/CMEPVzKh3Cz/AUB+PxOHmUxLnjcWxpsV3FsfHbH79/guTsqQgnKniR4iXGcYqFQynkOPVq4+/e30VuB3HV2QlJy58SdSdefcf3fiqf0OdE7wnJrD0lmk682lTxuyr5ugfXNvHY6Tl18HEumIe6UwwFGq7Q6kxmp8tbslAbhlp5Kn/d7Sn2lgRD5ysfk6gQYEuVzS/bp3gMJ4TmfWXMds4p8qNgSAlmS1jjVqN9Sg3L6lTofoWFK8JsvF+lY1m1Cu1lbNxQtm5DdpVaqdRkR9azxwvPjFuiLlfUonhaJwB7xy2VLmeEnIFPzTgLC51n7LLeAq8Vr5B8fnDB99N5tSqKYuNDSTT2niob8Z4aRMSap1IjWxmSCfcLtD6r38FxLHqZUbPouJLTTWZ1tGYHJ7DZpEKbbVWZ9fT/oN/Wa+ZuVBvV9ISam+ucMwMmeMDIzV2nETBNLqApTeLeqlwWlsqDEaucaALltuUySQSBUPJBXuUWMxGmk2steHf0MGdVq60celhp5tbNZXazxw2GuR2OCps97KDv0xlnn597ll6Nn38JPP9pEv+7c9gKcClZ4ZADJS6K7RdFFjmTyIsXAlTIa71Ez9w/e7HCzs3uZB4Omk2sak3AZjk9uwZ/5jQ4w1NKAT4zSjJ5ajYjqqISYsnn4cmr5jNpNcFragOJunIPMecXxuJ4sXQaLTNxP/4xZ8r+QeUJGIRT23hDCYXO/vnss/TJ/Bo7tXiNncFahmWkLi810leWCl41+6PgqazZiunaB3Sl83QZohIDdCnhT3N0KQAGAF0KPaZLgenS5Omy1yQwvJNDHO8+HlPFo87s6xkDr3yA5wJ/xnUxP2DizLcIXsvX81CkGoVYRXN0AZzll7TlBIqcOMFZlB+g9U1owzKdif1Yw7Esp/kTyxuYOH3J3K2cFr0peAS+WMi2q3lZn6nsb5nQ2QjEI3ZcayBRbAb/kFoIOQqxgo1lQrP/+COCo8cUT6KvgC/TgF8majaj1FNGXC1DQtMZ1koZFPlI1EzWbDGBYxucDv2jSb1Jzb7Cmf6o0mIfvw/84hqFHuxWkrqBShfg2eSN51Z32EzagiiSOUpryLq6htOEZ9i434IDcExi3aJVHoxwRDYmuXD9Mi8VGTN4MqbwWjNmlpASY0Kas2BDIhaZRDdMgjhenqHcqZSkYclb5Hx9Ert9kjGNotyimoCPlxSHQZS6r+ehj5+/7EjvjuWVRotOGBL3D1++sizkUXHlIxO7mmu29kU2+JK9pQ1bR3sDf/Hjm1s/bts3XK3Yc8e9ZdVl5qKh4ZrNt47O7Sy6rqy90u5u3dob76uyuyItJUirCDSPEhwknv1IwYKeWkAfVlJpDvOIiksO4IoSs6dYlRFRNLcGgau3JVqIkXQWrqTRGMhKhFRkxWiew3C6GNBDWiMwqRy0F/AYTbkYMARhedI9D358SpW4pTN94LUf1R96cs/u++uUjCNYf+e6iZvXRp55aNsTbeyP5i6d2Jmdy84eeOvO4ZGVV7p+MdbdfuTpyV+f3Lme6NfE2Y+YvQodRF1Ncl2mVACks5h0AQ4E4tIFPQY8lWQINiA5gpVcKAAoo6aK/fPFfAS7yFnWxXmD+WwVPdF8+Ln9Wx9IOVmtWhtoGG8du3l9LL7u2FDv1tagzqAucCyf2FW/+bGL2lD28InbBloSflZd6C1oPvzUjqknDzX6y/xar6c2ZF124zvA+3Gg/Rs53q+h0iY5eiK8JwPwAO81i3mP2Y5BhJqLxSRdjvcFmPesCfROJ4hGnEHEEqDUxkXLXDY7ia2iBG3TZosNJ4kFOR88Dryf2nFP3ZaES6HtfOHgaz+aJLxvuGti4qa1UXQGs36gh153OlLw6LoppEAKzH3ataa77cjTWIewDF4EGZSAf5ik0l4sBUt+EBXKzEyQ8+KMT1AxHz4YDbjiWTTmIgg+F0EYgXLW4sWTSCtIzkKsUBwuhaXwcUoMCgCtFy8kKf3eT4op6c0FERMth5/bu/rLU40Gbs6T2HLb6oGD/ZU6g6rAuXLrodTOr1/eMUk/Wjl8aNnglWvraNO+V27sbzj01B47b7no+UsavOU+LK2gbfnt3/7J8HUT1bF11xKd88Cgr2Rfg9c2Kl2IpQZwrygu2ZUwV2IYd6lVGUmHRwvBeiGpdCuAAdti6YJCrI8FToCY3hzEjC+GzcQyFCEZdoaCnucrhy9aVtzqZJBZX+6JjTb5UF/2pc1fcjPTpdeuuX6sQqeN4pxG+66Bq3pm9zFf0tJyrnogez3zM7B99dQQNYni4LexMDYpM9N28yZ1WHIpMmIiKrUCyX1RqQI0LRyDQEdajQ3fNiKjBj4jNvCSUgc2jicr3StxHoiDaB487kqBmMW1OAaCQzcvdcFhtZBJV3fhMVY7YIzbZUj4pw9OPCkvl/Tz4vITUrn6lBg5wU6HyyPm8KunzCc24SqN6Up8Cm+Z7ulfbg6n4XRRrQZcw7UaL/SXV0aW9+RQ3ov95eGFU3mxZW2pYGrVMGabX5doXb0JBy9uQSwATeprBU2qbsDBKISlOGXlB6tVCmerBUlXAq8u0zTnXrmWWATwp7nq3vkiX5vdiwtS89U/IbIEozzP2roixDFLl9YHdq+PN/LeiKdnZc2mm4Y7DlYituj+InftxhtWji0PVzdtv+7G67Y1tx55dtfUY/uSayLj165acePWVHzV3iNHa0LtVa6Wku7tbe3buwIly7a3tm3vLplaebhYaK+3RSNlfPltG3ovXR0tdvtctC60Odl7ZDRa4Oz0VERtSpU5MtLZcslEoqJvS0flQJ3X3zJWU9XgNQBANZbGGhkqtbGzpKRzQ738ulH23U+BIv0d2Ccr1ZXDovq47BWEnFewzVsmmvgEHOnoDWTrjGSwkjASDK2cH1zwBsTjCbL9F57a3P3CwVXXrApvOXbT5Nc7weJfvmZH7eSd43OH6dvuenzHxJwC25j7gaBB9gXKDDiimUpb5msBjPpM2opwms1xzsYjC9l4ZDeQLIlkn8/3fLJaHgdi93POYrPJ6+B5h9dk8jq5ss3shMnn5Dinz2Qqxq/Fp19mzsyyFH3277M35mgJ4ayuk6SbgAwtwnAdMJsGMFuMZJ80JzE/pu0aCwfzxConn/QaIMbpJ8QwpPAMzPFConQpfXEWGdRu18jQZk/j2mZ39KWltGYfrNarJ0YUV545VjvREdQqv7OEcpClCLJ8E2Tpns+lWuJpHRA8wxRROpxIZWWReggX3USkUjHJpRaB/Pj5XGrifKlUBHhY3FLFOXl0r85hXp1t1pp1vF2PfjrK2fTZVUKRO8r+aPZitRFdrzNmR7UmpdpumMvqDOg7Jm4uS/TtHfgVABoZsKwyjZigXOYaBIl/FjLX72xmf3Q6ktNT9ocEA+zLxQcOP0SnCEYny8QUl0pBY4tieRBQYcALHGIFT3I4fsP8pgCHjA6kCook1cQAdjhgJkQDKRo04RQIjr1YQz5z6SF1gTZ7bmk8p9jcOSpeW6DQuDsG1lQduMFh6li9rbb/6GjllmuP1G7pq9h86cGRO5PMGddXyrviBddd1LKuqSi25UvrsPp/7cHgwEX9+Ojuh7eOzWbzcxLGaqcGcjziciNV44lpVs2nC+3yGO1ycofLT4TcwIwCCdTM1HzykAzlE7MTk77slUMLExQovW9sz5IJKmOZ00DXObnYPAbwq85bF2z49FzsZ2xVabn0+X37nr+kpeUS/Hppy2R07c1r18rbTPBrFGWPvHVrb++tbx05cuLWnp5bTxzZ/uThlpbDT27f9hT+s6ewXXkqey/QrQcbF6DGqbSQp5uwVIOJ94Lm4ACuZB4BszYZAbtz1i6INzNSctLMLUgagVRO4FUrvUUpozCBRCrnQGEnOgcIP1VrEJAG8NfrP2w48OTUznuT9XetxQDs6Ye3PdmavZfdqjM+tG4qOytj4b6+rJHuHlsug+FdG/BYxmEs34CxYDw5LuNJAibxNF9AlNxSRMlhIF8AiNKQQ5TcPKI0yFpyXkSZJOGmcCFEueuBpAYVJbZ0Tu/PI8rkl9cuIMqhgUOu0w/RRRM75xFlwaoegihzc5r+PYzFga29nBmfl4hFlwEbyhefiMo10k4yGpi6JEDDJstIVhfs86sLMusXMpNYs+MCj9TVTxyJrPBzjKC0+6qLL747wpzhTO9dcbvZ3MEjjVZ9101zu/JrYwwL+t1I/ZBK15N1WyUEjvUkcFRowulCTFkIroUIxAv5cMjRFBXtYG0AH1XIfK4VMlKzDIren3zHIoMiMy8KJ6So85RYfQJOpk1mAXBQlJ+uilYDDoLfi3AQ3CQ4SDCZo1XVORx0zhlBQRU4L61UgAw5YVpTGMA1JWKtSfL4sHKGNDiNa/fU5tK4i9brzsnj+j+Zx13rYPU6Q2nz+q62LW2+6qFtU9uGqqNrrlyx/ktNNpVRV1I/2pRc1xqAO3vgTtXaG0anHpjyqTXeoDfQPBKJd0S93lDDaGtisr+yNukD9+Qqru0OVbVWFntLG1c3dRxaVd1JeF579gP6QXYT5aMOydG7HNIVkJDOpgnjLUieuKQmsDut1uXr80nG3k08r6iKpfVufEOPN6G4Sd7EjQvo9bzEcBmcksAugMHLyTRwRifki9Vqk2Q7KVnoztkeHGFgh1eL0yy133Aigz6CWrMnrMG4u6Q25ODVBaEjbTsu/rLOyDwb1KO9Gi57ec/cQHljyGxzWbXhcM2hI/TLBhjb7aBP32DOyHbcgPUbJ9YkZc70iNp43o6D18NJZA1ojTFG7A224xqG1LiIelyvRUlImfPRJKssT8aFiC9C37712I1bv961JVGENN2vHBq9elUYHaBvmzt81xPbJ+jsLFtwz9huMOpULt/HfA9oM+Gcsonk+1Au35fPEFGmCyb4/K5+zqRAQ1ody+o0aJg16Xuzw6uZM0bt7M8c5TZbhY0J6DhAUvhZdvDd/wAIr5z6M5Uux/6sME4eJ3EFOK8cjuLyGDxf3tG+f2w+r8ySvLLCcIqFQ6nccOrVt3/4u5Q8nXy86DkhCcpTouXEq43Z9x+S88eF8GcOXizkJTve6OyAUFp96tV3yt8vJiXiAsw7wQLzzsdPF/s85vC0F/9Ow8VFsw/uwIvoTVGtOgUrmCx2h6fY64sszjwbqdydgkJPcfk5N/PTExhYjtdo/amlLASjGsuv1+LKa7wgKiff8KKtvZczMwipNApWr0YmlbXUrkIGo1ahUSNaXbA8+9xyXpX9LatmGDWb/XeluXOB7WE7E7bbZ9+NhG0VdibgnGVtTIPRY4T/Z//GllszYW4DuRfM5575eJpGueWEwihO+eRzz9bFuefEeVLPAXQg+/B6nHoOKzhkZ3ntRPZBdGg9zjx/l9Vm31PxOlqD/qDXZIcEC7pVY8ia5/4gaNDbFmN2o8aIdQP82feBHhvBg7IKitboQqEXZb2gFpJ93vYhI2jiGqVWweqUaIQ16/rmXlRaTMtmCFt+aywW+GKecei4029wJnQnPKMfeLACnrko15xPhZEqzwvkmvuN9DVzX6F/aZw7Rh8KCVZm80CZTZj9ywHM17bsH9AZpUAtR4cosT4q1bAZUjwKIbgtKvG5DS4tELu0gheO8hmpMBKLpVuipIARacLTndEWCGZUHfG4VA63PWG4XU72zJSnwJYJMbzrhWyYeOOjdfJW8NaIGAZd46WI5pQY5qUOzalX31r1kYZMIW1E9ETw9uNCuOnhJRW+WfxHA5kJWn5arVXBBNDg3zBhposK8Xxw49+vNs/+8XHytgg/XREJw/VK/BueNN3W2gGn7fh3Go4Xpo3YnkrDu/BRRSoNn7boljuVhufgI0AarbxKrdEWFrk9eO9/a1t7x9JVG/SSWlPkrqic36uen081oJXleG8PBCIlKdFmknTFZHbV5kAj9moNiKTuc8m9RbXx+BQv+BTN11jiP2kLNJTbzHZzqGeqs86k9lUsr3Gb7CZnebLInSh3wqG7ZnmFT22q65zqCcEbbeWN9JYWW3nKW7dnz5765j0rKsI6vSc1HKvfP7UnGWyJFquUxVXNwcTU3n31seGUR68LVwzubknB2+t8deV4HiJ99l40DvrCyFXG8yGQMUN+5BAIgX1H+oHsvaqjf75JxkxT2T/QJUTPrqPE5fLaQV1USoKe+aNSKKdnEJJqC0HP2kGRIm2gSO1ky2V7HehZU7tGTZpfYD03OEHdmuBd1c3wLq6JbNFaDuoWXFC3b390j6xuzogIonDyUjVoVIQo1qtvRT/6K6JuhojYFsHldc1ws42XtPim4Y8XET0y8NM6gxYUR49/v9r84R93k+tOftrlLITrBfi3WM1PR6sjcFqFf7/6VtlHPydva+anW5rb4Hor/p2GP1mkXAWpNLwdH0VTaXjbolutqbQe7/tNiTqsd1qd3uB0FRRGAEY1t7S2fVLvdHpXQbSqpfVcvasDPyxx7aB3SQH7Y79JclSmUrnlmEWql9uTgU9BAYNN89tpSP7Sukglw2iK1/gqemrcZpvZWZ5wY12DQ3dNT4VPw9d17ukNWWwWe3l9IFBfbofDUO9UR92vZUVL7d8LitZcVaxUFUdbSxJTU/sa8oq2Yk9zamrP7hRWNNBSUDhQu1TznsEKoj93odcVFnoOrO1qCuyspFVn0layNdeKEZMrKrFwhXWRBXNeM9/rxWMktUg4zOSNci2S0YNDCCvGmi4t9nSOxTEdAZrxXGBHNtjd5W0eT9Xu272tItgcdgwWN0+kavbt2VYRagw7EHq9bvPystLq0oLqztK6zd34sBAOSS8amCvHAZdzVCHY7jSDDbVenwFvhVdLyTqeNYN/pgvUOCFUaMD3REucZGStMRLEFRQCiXoGU6uHQ9Ei733CpC6kZJJxMBWC//1E6aIuNPNNaDYyz5cmOJevFO7VzS2b7z8TmZN75jyenWPOKLJUlKqnbpL3UoglcakWAjJ7LF1LKh5rCzVynIZXARIqnDAmpfwwiCogtkpuVhAE1FpbfFIQw3HJDsdBXlLK1eliAudnbXCgi5HK/mCCRPeSHaPDEhhdohZwP0cJxfNrHov6dXCI9Osg6QycSs+37GCSuZYdj7dd9fJhHTJyJfrxWxMOVmPy1Q2nKgZ2dpXq1GqF07FsYk+DfH/LXx5u2VS19pqhyg1fnqxB2Yv+6tZB+kcGy5/UDVEfq3a4C9jZa2l/qVfBFrtjQTv9Hm7F0X/Da5dOPnKoTcVcybRe/ATWyS6KUkyxLwPXLpI7PkiVTEY+ADea1uHcm0uTmaEUcZ0hLBbH8eqiWCIzLnUSR4QhvC8olg6l8nFZOhXChykKF7am4powZhYlVeIOJ+UpyaUAbeDNsvMgi6r5Dg+Li0oFeY+fQLbjx+UTvGVU6DILxxO7Htm54tLxVltIYxA4S7RlrHno0uEy9B+CIVvT22oPO5ig0zrr8bfHi+ibvEYrqtz4xJHOYNtYtZ0VipuiBbUbb1yZ/XGpzpT99torKhSKMmNRh6GsYagWrZD1CVEQNm+ASD9JraAwIiqDMCgOU1Qpr1wWn5QCoAkBnuSzOC5DFivxFqiXaLVgcRX5daROK14GV9Q6coWW1SJpl6PlpJ1UmytVdlVIbuqgCpFceCKpWpKNeTz2cORAW8uByMOxh0rC5SUPxx+OHGyB80diD5eUl5WwFX3bU6ntfRX5V0V5/GF4Y+Ch+EO5P4yTNz6cP/95altvRUXvNnh3f0VF/3bQhTWgC+3scaqYuliuTMvXusy4ChyUvJUUr2tYYzNuD7lgjEtuuCCAOnhxuRPePYXzYqZY2u7AOmC3gmHjY2mHHZ85XHgvcUzy4USZg1TNALLwLJTPEIyZT4B6reQ/XJBbS/5bs7LAgLaoOVYjoC24nCa7Ak1mb0GXZm/ZLL/A5eOuuTWWgOAL0cd1xtnvNx5pzB5FN8ELqUtb5PtVME7i/dVk+5cihp2/qIxJKrCxmnkMwMg4YACQAFMw+2+K9Uzh7G/kGrc7z17GXEP2Wq+jHqHkuWJTZtI2EinbBBhsNCo1wJUGAjUbEtimrycGp4fPTCt7sMUsADTQw+NeQ1IALpYHRuBiK1xsjWIwipsrbMg3VYilxB5BTIDjNYl14GOFVr3OzHhC0YauwaHxCZyDGDGRMjlbg2B6QcmVx4YmcrYosWiZZWnmQTm/4zoYSp6brADjpAB9lRdd0J0bdtV1L8pGBBpGm1Ib2gLxVXv271kVX70q2UUyEg822VmDzhBq3bCsZWuHv3bswMX7xxJrSrsmtmyP9LSUNI+s21Sxtp/+58GrgsFt/cmtA5WJhN/g9LiKE8tLo8vqotWp7k0to1cFQpPdJGNR51ervcFiX/NIVc2KxupYbffavvL2RCRc4fJuaY4sT1WWl9pDm7FcShU/pKPsEYivS6gaCu9O8sXJhj9HDL9IjC0GChuMiogsZ2CcbiGL7Bm8WgpyN52bG0WBJeelBkcRRDZ2jrMX87zbgVYaHO75C4LbwZp8HnziEXi33WCwF517Ctq35uwflEVgdwvAY63DPY9IjZtXkUmrcFFGWEEFFOGZsX6ryhCWxkCF+sewCvWvxCjSqlKHZ2rbyb1abI+ITs0UytupCuXtVN1CRuzmcfJ0hpO7n2A1CnaDObJ6VeHa+tExYqCa+gXTi1xhsIrqHsUK1C6I9bLzUuDiQ7wZDW8xWZofti822osX9BO5rf5yYmRN7aabnnh9+/Y3nrxpYyKx8aYnX9+x7Y0nbtpU27j75Y/vuOPUK7t3v/LnO+/4+OXdH3Rd/uy22vH+do9DxWl9DeuXjd42mUhsvn5wzVVJvY7V0MWNT16y5anD7fS7297EH4E/+s1t29/IH7+x/c5Tr+7e/eqpO+889dqePa+dumP7s5d18kXlhT5dgacgse2u8XVf2lpTDngaPmt5x9Fn5Xm8lxmmO0AWQdCWq6m0Bc9jjWJx2Yroi85UEJGIsegMS47ymytC4AVCcqMpFuN+B7gCvK0ihON4TgDkWi3AR/nwqqjDJBblNoFLToBsYkyQqKLFFSzm81Sw2HAByyfbG9VyaG944z1Ty/oqGssKdUaVoXpv1449Xp2O1bpiiZaArzlauMziDTt8qViF7esPML8raY8V0zUrVtqdds5eHbl0W/Zqtb7LEXAaTMGGisJSl87o9FvuZJcRvjxC3UJ/h3mYzKMglZsxMy4rpQY+FMdIaYEL4aJks6Mo10in1my32S0qBm/+NMORES25hBd4H/nYzSP1awaNVv+aCgluDp+rXsfnr6sEN23g0DFea9Trsz+xaNWW7I91BqOWR9ef97Icmz2D1jKn6J9QLFWV3zma746j0Mh7BBSkm1JaQfqMKKj5PQK4A45feIZZuYq+pS97E4qAGzxnfi6jBqknLzBDu7rJLOwCrNTVjT+4qwrUpTE2Uz1IblSz+e3sS6bnMjDt3TFxGS/14bw1nNWeM1lXwtW+ZWDErd6wqo3sHa0VIKoSgyaxEXSou0swzcC0pcitQUGs/RyTlhTVyeZ+SbV0AnQujD7/bEVfnXvo0euP6C0aFBjWGpXZ/6l2FRy894qj+44+9bnn59zzzG2XHN1+TFCZjdmbVFq0Q8dl96MfTa7fsBpkamFpmJddC31+2IxcQLjQ50d9Tp8fC5h9uoPsJV7PjNF/y75K1svaqfn2cXhvNel4klst4xZWy7j/ndWy9VUjB1vbDo5UwWtb24GRqp6SltXV1WuaS0qaV8eqV7eUKG5pOTASjY7sxx3d4G37W/BV8q7VbSUlbatlW3SAGlZUKx6CMRupjYv2QOOQBaCnqImlFaTmSsHhYEZBYkUV1nA+KnInMX4xGHE/krSBw/cMDKijNpbmDCS9gONMQDqCvLtd3ki90P6JeWu2Jd8Carivj97Uhx7NburLbkMP4Dm2lbmf7lFeRVVSvYSyMuCnJSpq45irBQp5x7r2pFTMZdLa4vk+U1EM/stI15wgmDyLIClZ3D0HV7zLIUDLfOMcucfbfOEeaWxI+uYUoa1KzQdFsaDNUVpb1NJrVVloA+Pmrt5YOdTgdYbr3T8xl1qR08nc71ALqo+KUvVN3kCt39STMiPEbtlVEOurLlvW1uh5j2UdYWIzJpm/oPtgPC3USgrCGckAUNYenXHIhr4EMH4Ub2pGgMRE00mxICYlABpWgaK05TeGpClFghh2QYynpOISGGRBldzwhlhuD3IzizreoPlRqhaqExehrwg96VGoWLWRYRSWksZIeWuZzRbtS65fZy+tcbf1mpRmFe/krlpfuSJV3NPcNxhsH6tuGkl5FSsMNK1Wq/XlJUUFFbVOX23QGqMHWv1xH9/eaEGMYssuV1VnRee4RVjdWT1Y5/HUdGEe/ETxJC3k60EVuXrVC9aDknZ7uEr1J4/pnI5NP1cLBsWTfzRx2TmtSrbDt+M1UuYMVYRXSM1yTQvIe37VRSwAxO0mk88lkLIW1zlrLx7sU+T+YaKGZHz0pvkVGIm3pS60BhMMAROxn1y8FLP8Gzsnbw6yTLXFkX2HrVu8HDOxYbCnYqIkK9kI3cmzTYpfQexjxrU4xFroNfLqFplteo6UAiOs7xzpqCca+BlKdoVUFOfecLsoDZ+RrPOd9iBq9ZPthH4Bm4yWi5/ZTf/bv6/JimO7jl/comgbvmFDfNWp3yodp37L3JWavAXTcRz9GR2hvwV0RDBynWH1lAXcjPxCHg9C0VrJRfll8QMXWajjfGGJxRYqFITCkM1SUsjTG+bPgoU8D54DP++m7N3op+A1i6ijFMhmRk2UP60mi4Bq0k0OpCWcnDHJ3ssk9+/F7W89ub36sd91yjlKIcKJ/AmFZHKd4kTzCWqaF0xmktyDcD+/VV/A2aoCbF7VBaQlUq45FIGOpGNpMr4QjdykVWlZobDMXVPvirWXhpvdazcWxrrKyoeyf1Wk1xl0lSGX12Zgb9nCNzd6qn1mB4zpPrBTHcqjYEF7KHD8Myp5QjO4AzMelgrl7KWaJH0v0IRMWNSEDNMYF+JWb21cSOLJG7rvpw33ZK/4S8VX1Gqdmn39jbmRWIwuC16rRFpix8eZQfoJ9iWQo2fe/xQpiP+x5woXF/qVuuR+pSSz51rwP0X2T/E/NtlngzEZLx2YWtY51V9a2j/VuWxqoHTFnn27p6Z279ujONZ9cGU4vPJgd/718PXXH774hhtkXzMD+O6XgO8sVBkgPCSWk0BYG5sJyo41jOMFmItpJW9NkWqqZA1etMUdNZhgbU0LMluZULBk0cVQ/uKM6nUlXqBUvq4yuT/+2C0ghfo1+QpAPvnStE6PKnUGBcvpUIXOwGv47JVc9gpeI1zoBqZbQcFEYb/MPg/ydVKl4I0el3fmiP7czkhLXAryuHxB9MZnymThF8XSZUEs27JCTXhGpeSRIbygGMRzfZo24BXiAOh7eWzGn4NxMdKJJachYkBIuwrKsCvwk/1HUlmQtNzGu3YrU0v0BzfzyC+j+UsQvmMJI6u/1usjjcCSt/y08WvZK7F2aXSqx5i41mUJz35XV2hCZ9CuzmuFA63ZaQfdjkoYxYevz6ue5kyUvUEwn77UxJ1Cv856S/hvfYsvQWscRXLNKubbVI5v3dRjVNolr0FKHWwmz7mZsloX3phXBji3rJYwLEIY5lrCsOWfi2FSPbwhQKo4Ai6YVD3nsGzaGqttJUFohwu3WmoF9pUJaU+sPtc07kI88y4FDaoLgIZzGHmAqdE6rTIj6QGl+kOAE1Y7hhN9FqWVttIO7hqAE/U+gBOen5jLLMjlvAB/nWqeYIxmjDGE9hYzomnFlp0uDDK6W5sAZCidYayro0RX01Qb1UdNAKJ7jUq3Y66PxtOVmOPL4lKxIiONtRN9HYnPrJVZPBhLryUR/9oVwH5DU3slCAUAyozDjg9zIAWJm6JiwUmRj0kx3IwG56fr4CDGS6tBW9fFZkZlbV0RkzYD61fXwWzuH1iL9XRUELuB82vHQBr9KbFJEDem8pimLodpalNisSldUh5LfS5MU46X0s+Haj5d20fnMY+5pClS3lIOmKc/sX6tDTBPS79ZBbZDazIS1FPn7W3qW1GCUc+qOl9mYWYI6A9LZgZzXQ4SlQWLCsO1LoBEFoBEbf64V+hJWEBgzJZdzmqMiczCmo7qwZTbXds5+/iFphBIK3s7/Y8KHVjLBmoTlY7itZCUPgNIUbLjbfKNS3dja7jMtF1dzoWlGmtGaoIr5bgnP2sE7qoFXM6mMU3bS6IpMgdSdlw0pC4szpVHNytaUNyOQ7mFEnxbvgb/3E7TwXB1z+r+GlrXoYQD0gOopntze4lWo1G4SJ+g7qs31SEf5/JZFlZX2lbsG6yPJ/xPf4MNNyUS3Rs7kmONxYGKgEpZWhgvdZQPHlLUfqIfECP3i1FZSL+Y4k/tGOON4lzvZ3eMQfMbjT6td0z2Py922rn/6NEL2vO3kaHDGsOPFer/OzQyBPyycOnTaBzLcE7HRdl3tSb9+WlE7T82aH6uYvM0Kj8mNIY+lUZ59+fn4GMybifxE5zi5aVPJTU7++G6D/vUFtVxWkGrnlWZ1Rei+HvfY9kbYMKwN7ALdP+C0B2jDl6Qbgwo7HHJC2FiNCoVwksgRjrb2E/OxGS7FCNeYqZEznnglnKBmGB6AZnoQnM5mRW5IUtRL8wcD1n6vZCA5lc/E8mFxU/lp7Yj+jdzScLnb07VFoYrUdLkT/h9TfWJwnAFfQFeDPibI05vibeuItAYcXmD3vowwSQyT+YIT8qpRmrswlwJRnGfw0IwHJFYvoTRa82IXp4grriVlDBKYRjwNG1C5sVsuLDklwDEEnl5NX/6qXrwkcHu5nk5Q83jDDV6ttrHux0Gg8PNC3B+AV6c4D34PfhvbAaDzc37YovOqAW+qEpzfEl8mrYEozMR2fnVRGcKc/4tSbQlLGtLmKRZZ7yytuAvcKjGTb2ASYXBc9gk1URAW7z2z6Et50PUn8atLxVGmv3+lkhhYaTFD8pQmGivibe3x2vaL8ClB/2NYacz3OgPNIQdjnBDAL8bfggGP/s7ilL+hvTetFNfodL63P7AxU2LREtshjPpkbwAx6lwl4oZVq2fb2TkiOKSRRyLnbj24zOkIsQSETURHFooCk6JGl7Sw4uCn2YVGnN4Wo1/w81pgwV/+YgZ/2ZeUrBqjd5gtpz79R9+vAxnzv0AC5VwAfioMjPFzHuzb/bSR+a+MkA/Oqepn3s4Y3CjFrpySm3RzXdHQm9lx100x/QVRO2kd1H2btL3apC6lEr34dFG4ue0LwKJz7TLQWg7aUDc3oSjtaHFjYzwTqiYkXT7lLqceDuShXVHosn63j6iBe1J0IL6lNgniLHUf6t31sImpGBoSXQaoT9/U60dV9y9xp6PWAvOjWVLbs88te6zu21F+5NuNJCPbs2Lg95L1AfeQmoq34dL0QD+TkdZP7vzle2zOl/ZP9H5asFDL+qBNVe+yCHnBK6y5Hzw/wOa5j3yYpp+s9gD54hShnNOd4FX4Hd1VOFn01X0WXS5z0PXEi+8mLy6TzrdeSKX+FmZzjmg00NVUzs+nVLcNaoyLgngVvzgVmIXJJuYA5zCAZdj4/EWJKnUSha+458cyad7lcXjin62E8mP8/hn+g2awl/s8DjojgY8RxGV1uJqBB3p9sSRHLPBnMn3C5jXTLxUr5rXyMSunCqe+jZpwUVTb8EHr/t8nzmvWfgz31rQKP2uvCqdejfX2IsG7aboEdAnnmRSyB6XtIl8rhWnziRLrn2DRcBfg4F0ci7FvFRLcFrTulQ7Htx1rlrMPxb0Q4/HA/qB9+yV4V5WZNce+dIjYxRXP+E174JYLrGzeKkb99qx86RDeTHAjfB5M4iYHvO5AtcvFfKHu4bOlfInhHtqByZYefw8Mo4BNvhxrrfKjtyeJgG0myHJMtBuRBkZuegIAXh0w0h8UdFI9vsKZrzfLC0YyWaFYk04bRTwoRGvcAg82SGpsWRwz7tcMyyNXa44OqfZoFcwL7QbxEof+zktPDD30uTkS9n7536/Gz197D3cdPC9Y9lx9HB2C/1GO/3sQu9B+o25e/PtB+eea8/1Q6wFbGyiItQVn+jYhbEf+PAiGE04KjlYuS17dHHcaAaAE5HhToTMzhzcwfAw3+ELrx8WY4TjCKZSi3p9SeEivABRdoGuX+YLAOQl3cBOfQom/kSfMGXifICYkXuHwVzD62/V2Mqep3tY7Hzdw+K5NbhpI1taSbz5F2wgtuCpPruVGCqcNxefq6sY87Ts3P6/jm/eNn2O8Z1cMF2fa4D0m/OOMjdGsGt4jHUXGGPqfGOsXzTG8H9vjEts4+cYavlS0/k5B3yO01007l+QcXdQx84zblz8WBqXYiyp0qrE7Y5hHncu5kUpzNwOeeZ28FItnCXks8QCnzCOre2ACMbo9FeyDedySmqFSFiqav7cPLvA7P4crOu54Iz/fDz89vlsgCLHxznCxwZqgNp9Pk5CgNcTlyrBU7UAC1csYaEUs5JsJq627YTDzgXm4a9za4xhJXP62f+Wkn06uPkcfPN+Fub5fEal8TPxEKIeok4rGMUGwIKUWYOSGmTXIJUGPYSuyt6UQEfRpYnszejKmux12WtRFF2NjiazN6Ijyewt2WO16MrstbJe383+mn0fvG0llaI2UGkblkZ1XhpleD7Xy60+QQA+npQxCcDqBnj14UVZd0pMCC+pWZuT8wQjuPBEwFu3KamsWjC9RHGC06MuSeXDrFyVKymAtuUFEQypyN6hII647Uje0Wqe36orG+0r3h09pDdZ647vOIS5f8l3R240+ITKN/Yf3bN5DT3b89JezP//2f3N7VgeY0M5Pne23ccbf7Ml++sZwuzm+hmBp85uQSWvPXFmlYKtbwZuz/XUJDDzH/xoFcYgpM8c2HEn5cddWT/ZaS5wvk5zJblOc2mry5NDc+ftNreATc/Td+7jBd9zoQ507FbZ3/zfpnPBp5yHTiQtciIXolRxWd5x5GgFv+Gkys9Pa/h8tFYs0Fr06bQu8Q3nI1n5CWdwYcKXOAAmR/8c0F9JtVDrPjkCsSwqNsQlDxit6hgpD1kYDl7LDVjnC8MTcJhYGGRbrkZcsqo/TW0+3TKdZ8Bzn2mJLjj+P3+G9aHl/nSgexbK/ckOdZ75DnXFn79D3UIu/fy96poXx/Dna1vHvDuPUxb6vHIgsb5FfV5nDEYSHRs0mRnGKbcz1sx3JOeAZNoYi4kcj0soSCdouS25cb4t+QVavu5E3Pl7vmZ/Lnd9zf4zOkq6vk5j2/29sx8o2tjXqF7q8hx1xZTcuQkgg6TEBbx9hKReQ0bslb+Zlnyjs1xVWiBkpnUF1eqw1AIhQkuUhAD4K2rr8HeVlvlT+Ks0JWUnvLYAlLAVV9Q2En/YWYG/eajAH5K/oWzRt5coFm04X1LwrVj8rRNW4XsdR57esubmddGqnlU9Vb667r5lKV/NumsHd3y1ycZyOkOweW1r48Y2b+PEronG6r7VfdVFrbv6eq7enFSgHU8eaqwZ2R5v2diTqmsMlsRK3L7y5tHGZRevinTW5fast6yq6hquDcX722K9LY1do/XFvW3hiok7Ns0imIukxxz57qAk1UbdfZ4uc3X462E/q9Vc+2e2mus4p9XcDGfx1zVhB3ehZnNSHQBcsekLN51bcAlfuP3cjvkmfF+sEZ3i5lzLvs/Fz8b/T/xsxPys++L8nK9J+8L8/PV8EdsX4ydzcb7kLc/P44Sfy6kHzsPP1OfhZ89n8rP3HH6+gPlZ3zbPUNEliA3nZWvqv8tW7GWj+Ct0EfGyX5i7Vf+y5hftvP5RJUsr6cdYTvMFmXzF7Kz+aYVaoaSfZlWLdPdWwusR6t0v3HESW9m6uNQOdncoKjXBhS7w3qsWsx5M78yIHKeNLBbE9DJXTB2e6ZJvdUVnlslHC/IZXSSfOkHkUlLXCER2Fn9lkwavSkhFMeFCqj/UDldaV6S+uJQuEPN9YWElLKE6n78pUVNQUYkazcGk39dYV1MQrqS/oNSeLWmLunwhX11VSWu0wFfqa4iQdUBZdkeI7Hqp9dTbX1x63VFxIi41AegaArFtWCw2vPWuHZBW+zkyG8Uyk/rhej/Ix7p4Nm1cJK0UlpbYbpIqsSvtFySLBu/MMElDE3KZzP+RZqOftafoC4ss+VmbkL6g5H716VuW5mX4cyLDPmrNeWfgKMZdTfL63afLc2awm2syhGcGcyu9Y0vnYb88xfp5aRjO2uWz9guYx/Gl00/sN4n+lDgszFgqm7o1nzEDRwfhSnvdf38Gnm8Z+QuL9NbCqtZAoLWqqEh+LWzIry1/QYevKGmucDormktKGiudzsrGknhbW37NmdhRpVGhp9qpYZiJIpVuxlJMxKXlMMvKYqTdn1gQJ4vy47G0xjovvZFAs9UQFlfEpREF7gaVn4YdIIsOXhqQJRMAmDoSwxEQ/tL3Yj5DplsHRb4yRBwQ0py1GReYBUySA7+uEtIFZaSMvtgkRapxSjuwHNdCwTHZ0iiIxbhUSjLN73JfEFCu7s9mn68783uXdCzFXwO/WG5NcBXle5guFpLOyAqDz+299m571Ss3DtywpU7Lza2rnrh6Rc/2ZSEtp3Y6+tbtrL3x7SrLmv3/q7dzD46quuP4fe4z+7jZZ7J5bTbJ5r3Ze5MseUMChIQkBBLAPARDERGCgBgEX4hCK0lFKyhi29FSFehUu3fJjNba6YBV207/cqa0U1un49ROM+NMy1inLUjo+Z1z95l9JNX2D2DvJsy9v98595zfOef3+3wfWoaaxLeluG1YXHn/iATNx5xgtlf07GzvPTgs0prOAyMBrvvJFyrESr0GNdmxe+99vO3g6/c6zAdem2pxlxfrCgF++uQ3102uzC9cuWtd03opp2bzkfXH+YquMdqweXqr1HjHCWDwzp/GDN5u6igV6oK2KpNklyophjfo8802k9evGRedNjfA8fmaMJsXjvxwIpppDidjttnh+FzgXWVen9jZhdcNzT5SatolQLn20ji+dLqTczYj4Lf2h5M5Y3fkiasrKgdzdSodn51XkV/f4vJ3lpeOnNrVlIb72zLIrU96TH5Y1X/8J9DvMUcXxb7A0cX17hGSrp8JE9wScbotKXC6rQpOd5a3uv2g1pAGqCv7YZRpXAJYN7pIWBJidyayQFgUbJflo+uC1L5p+N/6pgF841+Cb+hIwL8k39DqSLS/KOfQ12LqWsL+uYj9syLOP2JK/3Sm8E9XrH/qM/hHXKp/FkTuS3LTcGLUvjhn/Ts+WOcUfx3C/uqiNlHT6bnVsIc2JMmNKLjrQbPK5gTPAby6xYZxyXBmMoA+DkT9eRukAbWgUcrqroaTAFnnhfraL0u3zhSxLcmvY5mitUX5mdmSPkhjKBSI0VtwPZeBqlRyHGCvDkMqI4kOBpLoIFN6BU8an0ThiYwj7RMK7/9GL4bzKnXBFP2HhHtwKe/B6SNlPuEXF+7xYuR1tE9EashujJG7MLc+hRvh3AAr1ajkVMCeXiibjkmsMMQlVmix3iedrdyPTXwR8GZrYv8+NcG9Ftt5bwwphrK3PkN2XsccATvJr8A7n1aa5FeUkfyKPJJfEUUJgHiUMtFCfoU7kl/BJPQfeJzEPmZI6CbvTNRkQAvc0MPzJn6L22ns1j/Yv/MvIv/1ArtHhPevVY21sjFrjWw6BtCzBsywMw0KwzXK3uKKAFq86vnc0nIRxwSgjB2ianRx2s6OWtqLtYU7YDMek0s6YKs34MBl3gtlsQME7jLWuv/VXY17dtzmNj29/4KgzjradmKtTkBNMj47+B0Lb7xvxe51VS33yVO3f/+B1RNNE492j57YIrGm1tHDA6NPjNfSH2x7/bG1ec2jbT/+V9/pfI1Ol7W3uM7MmIysnbMa28SZAo1Gb9hR9/C59w89+ZdXRjofkvdufW5H4+pjP7u/fucGqW3PM6QvEwb3NOWgJOpkCuIvnFc4JblYNRes8+HkDeDf1CdQgFFjz0pkkSKZ4eQlRt42TAhuiBKC5VIJ4qp8CzkgV0DBch2gAYpqm1Ijg1Ot+ReihL0pF/XJIMPch0mX7mjuw+xhRQfOTw3H0IfLI3MfRhCLyRDEaRIe5HKY3GoWUV8dHZ8yc4m/HRm9MhKK2U0kAkpnY/WXtLEabCxfhI3RwGYR7GVHZPjMaCTTGYlkwnZeVHI6Yu2siLezKZmdaRI75IrF2rkgQMls7vbEUTuz0b0J24cR26cT8zpiKNrhvA5VsrwOw+LyOgxLyuvI4KoU73pmj+1K+e5ndt2hFHt4xH+HsP+aY/M5Yj0Y8AV7ST7H8mg+B3FdRXw+xyr0cVXUaRnyOdI7KlOsltlhuzMFaJn99qMMO2jQB/dRH3N+DjTuLShWq6VAz0CdNRcGPbh9siNrDp/mc1eDVlHOskGIAdOJwrigY8+Cy4S4q33s5ZuXY/l5sZ+ZE2vXzr9ZvsycU2KxenJMAZaOuSDvxyXOwHXgeqlGaqOSH+ILbzSUw0FlANcI54uy24ArVqBkR0CtB2eW9W5AnfF2p7GglIyC5T6SFuIs0JQ0xu0fBBQsnqL0oSYoPDo2J8ROGpiM+KOnlo3orRbp6bbl0ISv3DNk8Aje6dXdW+tEhqs93D82vcX31Mj02PTtvg2kqcTa+03Gy6uuHIb2Wr9PML+16leP7brQwrxRVbvi4Pl5d/fyqVd3/HwKxwGYF43GfwflhhP/eGK0k1H46BgbXZwCG+1RsNEhixMSGBLQ0VBOmZ8aIB2d4JKgpN+NzmjJoNLcufA6PoMdeV+FHXkC4XcntyM6iSVDYq+IzlrJDGFPxqy5w7aAhmj5Qlty4mypSGFLZdQWVxJbctLasmCiSmLSyQUzU1LDnoufjVjFtkPItkqqDXh7SRnlQa8v2CzJ+WiAqBOxpGjUSqCUF9twnhakzjTYMEEoxnbQGsWkKYsKzTogirIolHmmoTSJE57NOHYmdcqNjOMlQxjVqD9DFSdaa7qYKC0do6rD1ZsKqjroEoKO1MBqNtI7U6OrhUgfTQ6x5o5EO6mib8F/gFnuir4biNoSonUBlrbAKivkZcsGfTeLKEJqh0vRd4PXzZUd0XcrsMfou1kS9d0SRS0mVob2pRC0UDffPDh6d1jbbbB/XhOvZ8Eqvj2EV7et1EAsAxwS1ZtIkaKPFCk644oU65UiRbeiQlwlyBo7PH4mZDiToXelbpefZupkKZrr0wy9DHSuP9PcjfpYEVVPPaEojtkkuYydC1pEgnU0hivU6ti5WVN2HmxbmaA8iDDg3FbsGUDA2KtEEdZ6wMA0YrivERiYWSL6IGircE6lDmpZebw/lQ2YCAfoxYQodxUMUcZsZZeKZLAyjph6HLeA96iSyDmPvfznma3nZ/aUsSPhkpwvzpftmTm/dfqTl8d2989cmTp4ebqvb/rywakrM/1KwqR//NgwvTFcqrdp+NhY3c4rtPnC2WvnR0bOXzv7/LWLo6MXr5HYWfUIp6dEajXq56epUC14CcXKy9RQY0KwugZJ7kSX/eJst70WXNQN26AbsIsk5BKJnD3A7ki3CBskayDTyTyH4ZdtaD0s1wIZyo46E3JFcE12yOAqbyL5TUWg5yTbl6GomiryVEk4maQbJIOCnUqPU0ILRSko+UEQnSx65MNbfiMt+87deer9KuuaOx7o7f/615bpTTdv948dGVh15+pKfZbG5ewbv6tx+r3aql88v/2lfS3bKzce2Tj8yHBlJfoLfaxkVcydFWt3tvdODYskCvnuzMrJgcqYg5/wtt7zz518KUkUaQmf+7Ak7051k7Ki+a+ZGorPvIMQsVGSc9EbWk1ovLarcqENk6ItOBMPJ5BBzO23kT35xSbnpc8+TJ6xt4ga4mR5fNzQInKf3dxrTAPeC6yJaqoKCodEwEQkBQWXHVFX1TaFK6xi5m934mQdv/UH9/Jyv2MCaI3oovqooMUHtbg6FJc7fTgFwSCCTgPc0EUWfS6c2hlm9oFkp8EF77YFOqsTk7nt8WTu+IVc6i2apNsxNLWDaWS6GOgdFKwGdtB/ZBqHhoif/tufnWGq2beZKaIhSxYi8CdGQxb+yxm2lKnu6SG/z7+f+ff5OuX3j3PNdAP/OerHzVQw2zfLZlE6jmziooFBb5oL6XGBoh64MZR51mSlJORN2NnVk0NjigBsYVtRDaKAZH+xlj4+0J6nUXmlEt603G7lfjN4qs2i0qhV9XcFWjs0WqPK5e0nNu7namk3/1f0DG34GbKiz8BflU2muaDJPKvFNw5qfSEtrivTAr4OHsMEextZ5DECQDwhm56E3uwt208eocNhHejIU3PrNCppZ6ClQ6MxqnO9fd7B060WFTzD/HXaTc1+6WdwZH6GTxY+QrYK5jrUFkwPbosKtBZFTxH0SkqDBJ2RUsFUbRLUk1zZIvTzIpwWUORCP7eZZ0usVL2CjFLaTLaZUPdnIZemSAh6U7ZhaeaGpa39HXBZDwamamdvisZnoO2Zetz2FdTusM3E+UE3sTm9/+EICud1I7NzS+DbXBuwzXMLtMRkpW0gC88LeQ0gYJOir5SGv/SmbDzagi49PG1uR9ft+Sk6lCZpL8P2zl9n6nE/+//a6/iK7E3aebXJezToeZTSy9hH2G/hmsugETPz1ISZp4bXy4IHbK0Nf0n+wSJLdX6oAIqZ2ehS34bJh/Zu8Pk27G1v27PBx2xr3wvMzns62ibh20myhzN56xpvp16nBMpDNQAvEO+CuSUJnwjJjgpRJF/xsJXTGFt8iyYoOQ+2dAgdqxbNzAHC4ozn+ZSmvZw05hTbojs79OemnGKrpSTHbM7xWNH1PzHnJ3K9Lo7hU57mioyVL1In6Hcx99dNhd1nslFGDmf3QP0w6L+hKDU58DeR7psC50vuNYvu9SFm0MG9bGECnYBvh8c9gSj/paLPLQDNXUoDj6OpolvXuGn+DbTaOUaFeqCRmrVzIROE9oUotKfoHpOhKuiTZIqbC9aLs1oN/qJCAiI05tesw2+PbgCF+dWWObmkAbV2Nc6/qfbDS1JdBmDWagxmhXdJI8qDeIXajIbDFSvRUrwQ9EmtTqUcGY7NAp4GiYStSmINplKoieqBymbFwrjoIwZvcdGzam/R92iGO3fBPH7yrf2de7cOlRVxOq3G7hFXjbWMv3Bfn4nZaRJuhliaZgSzad5i6D1wdrxjW29Daa5Wpy0r3bTzwTX3vT29ych0t1rL7aK/9Ru/fXbQUdNVXcKrbYVlhbblD795uFCfXSfZvbbCLOHI5aMrnGXVZTk6j68/kD949qOn8JjTy47zpShGU6N34gCJ0mStTSJ+ZMUwixnAihqHiBZDVAHkJaEgVnVV5o1odYXRjDyLnKfC3lSB83hS9OwxYgVROGJzkFALKpucHkAl5pNCmgYC28SEY4fF0aioy3mEAOqanmIv6xB66Y9/vYY+3azTqT/S89rf81pdy3L+TxohS9B8ouL3tLbe/BsjoD/9nGZ+psBspKc03M1L9Hs18w+aaYF+vGq+GfoQDAI32BtoJPDGaCcqMkIQisJAQ/5R4iG/4Bbgv8DBMta3Zh/lf4n+3aqsNh2SInFti0pcqxLlra0ihJtwpuwwzIUVFSiidC07UdgZ0giYLSBrQGRP35Sgfu0B9WtVPu1WmKQgfx3YdWaiuMfJ0QZ9dfG5ILNx27yJqF9v3nLm7qYsnV+nfvUHw1+Uss+E1a/J81/i36GKQY28kMLLkZABWlxAMbJghmefzc0v1JDa/VxsExYNLMTGgPhtjhgqKMRigXmgCWGWzTCsGObwsGguQMboNValDCxsBEhIoecm28OxIt4NO85u86ztbrP1TgQe8PcfHqqmvfMfEju6Rl/Yv5xXcdf7+H2Mpm7s6GBXRMj7P61y/VcAAHjaY2BkYGBgZOo//7DZK57f5iuDPAcDCFz2z/KA0f/P/mvhyGTXAHI5GJhAogBrnAx3AAB42mNgZGBg1/gXzcDA8eL/2f/PODIZgCIo4CUAogoHhnjabZNfSJNRGMaf7/z5VjD6A6bQjctWClFgEV1LiVR2FTHnMCjXruY/hCCCRdCwUApyYEWyZDUsKKUspJuI6MYKuggGIl5Eky4WXgQjarGe92uLJX7w4znnPd855z3vc44q4AhqPmcUUCkU1CrmTQZd5K7bhLC9ij7nLeZVDE9IVB9AgmODTgpDahoxalwtln8xdpyUyJUKbeQWGSVJcpHMOitICWzfJ49MxnFUEU3uTQzYZmy2AeTsPVxy65AzL8k4+yX2/cipKH7rKURsB4qmATlfO3ISd88wp1coilo/x/YhbB4jaJexIGv68thq3nlst1twnud4ppbKP6j9zOGj3s2zh9Clv7B/GrM6g25q2NSjW42j0WzECXMSWeZ9x/lc/qBXvXO8cXuQlTgJmw4q5+i9yOpBRNQiDjI+pvPcM48GPYOgFp1EJ/dtUzHHT41z/xtSf6k92xnSXtGQ/GMUrjO3FneY/Rn06QTSHJuWOV4shDodRI94oh6gl0QZ+yR72004pAJ4yP4I47dVifklMGef4prHC5xi7fd4dV8HX2/5m3jh+VADffCR12Qb8bud2F/1YS3Ma9LzRbyoQbwQz8wU3kvd18MdoIoX9f/D2u8kaWelXCDfzVFE/vmwFtal0h6rRbwQz0Q3fGWuy/yHObFWO0izTgG+FqCq6izfyAJp/Qvy1H7qOY7xHVTh2hO8FxN8F0l5I5V3kiSiQ7zvu+xlxGWuuoA0mZN1mWfAPscx/ZPtw7xzI2j8AyV25OAAAAB42mNgYNCBwxaGI4wnmBYxZ7AosXix1LEcYTVhLWPdw3qLjYdNi62L7RK7F/snDgeOT5wpnFO4EriucCtwt3Gv4D7F/YanhDeFdwWfHF8T3yl+Nn4b/kP8vwQkBBIEtgncETQSLBC8ICQl1Cf0RbhOeJ3wJxEVkVuiKqIpon2i+0RviXGJOYlFiTWIC4kXiV+QMJFYI/FPSkEqTWqNNJt0hHSJ9CsZM5lJMj9k42SXySXInZOXkQ9SkFBIUJilcETxjuIPZQnlIiA8ppKk8k41Q/WWGoPaGXU59ScaBRrHNN5pvNPcoHlOS0urQuuBdpJ2l/YzHS2dJJ0zuny6Cbp79CL0hfR/GNQYnDNUMKwxYjOaZKxkPMvEzWSCyR1TA9N1pjfMWMwczBaYc5n3mf+zKLB4YznByswqwuqRtZl1j/UbmxKbI7YitpvsouyZ7Hc4THOscIpxNnG+4ZLm8s21z83LrcZtndsH9wD3Rx4lHs88ozxveFV4S3lneD/z8fLZ4Cvnu8mPyS/B74l/WYBBwJaAV4FWOKBHYFhgSmBN4JTAa0ESQVFBV4J9go8E/wnJAcJFIbdCboW2hf4JkwmrCXsEAOI0m6EAAQAAAOkAZQAFAAAAAAACAAEAAgAWAAABAAGCAAAAAHja1VbNbuNkFL1OO5BJSwUIzYLFyKpYtFJJU9RBqKwQaMRI/GkG0SWT2E5iNYkzsd1MEQsegSUPwBKxYsWCNT9bNrwDj8CCc8+9jpOmw0yRWKAo9vX33d/znXttEbkV7MiGBJs3RYJtEZcDeQVPJjdkJwhd3pD7QdvlTXkt+MrlG/J+8K3Lz8H2T5efl4eNymdTOo2HLt+U242vXW7d+LHxvctb0mkOXd6WuPmNyy8EXzb/cnlHjluPXX5Rmq3vXH5JWq0fXP5ZbrV+cvkX6bR+d/lX2dnadPk32d562eQ/NuTVrdvyrmQylQuZSSoDGUohoexJJPu4vyEdOcI/lB40QuxdyCfQH0lXJhJj5QMp5QxPuXyBp/dwTSXBjt4jrMxxL+A1lPtYz/GfyTk1QrkLTxPG+wgexlgNZRceu1jLILXpX/0k0MvdqmRk9RPSs1o9kHvQDOVjVKK6y75XPRxg5TNa51jPqHuESEcezWKblaGheQ8QVWuePQWBy/WfPMHnyRK2V+2Hl6JelbFZv42nUyJbUEd3I/hQqy6kwpHS2otFrNeXYtXxU2iFeFJc1VpRHtPTGdYy6f8LBrSvbfG03fVsc3o2bqWLLJUJfWKgDOmTYSmyUB7HREwRmDirUiJX86mE9tixu9wFp8REo86BZI+5mpdVv7Nn6I+9FcaHjGnVaC8s57G7yNLQ1PqH6FLl7T1ypmD9CW0No4iZKg7KJKtd87WzMGRyaFrvTSEV7JQCfroLi4is6zNmxL0JKlT9GRk5Y49b5BNmWdDvEHsaN3b+KZtCeYS1lHG0QmOa1jv1XDX6LifH0Hu5XOBr9ffgN/Z5lMhjRutBq6BVHTMmRlNWe7FSaebTTv1pnRXjNa/8H2NbPw4WXZXiJLVuPYVPnT0RtXLuRu5fscqI8IxYZaz5gDtdX4sW/W64nzP/FLWN6HeVoyUsp8wjcgaqN63pnPuV3oidb3Ogz/hj1lh3RMqYoU+NMXO7YG9Zvyb0MVhwRmt9xxk3dA5V81vrGHsuFZo57RNOkfVeHSFexj2dNWfO34TVx86HOlLfp5qtdH3CVzNhTiSe3N9VJx94hGSBqLJmwPeUsTfGimUyYVeExG7EbOeOjfVGiUpmS3maHK8wIif3U0yLGSPZG6yaGAWZN2K0asqun12+crp1zV3mlvCUqs40L3M/T/V24KxOnUv1yRXMyezsqSTCJSupmFudRu5aXbDSuFOscKU62YydM6GFdceQlUwxIQ7xm/PX9kldvx3anDZjaFxX//LszbG2PH0/X5u+h//xt8/etWvY/199Ma1XmMNOsZyy89u0GOGecWYeItpdeN+/gg/PZllVWn+96LdPj71puduX0alX/qFP/lCO8e/geiJ35C1cj3GtzvhNoqOTRedvQXaX7IN8CZUH/uaybh/9DeeiFNJ42m3QV0xTcRTH8e+B0kLZe+Peq/eWMtwt5br3wK0o0FYRsFgVFxrBrdGY+KZxvahxz2jUBzXuFUfUB5/d8UF91cL9++Z5+eT3/+ecnBwiaK8/FZTzv/oEEiGRYiESC1FYsRFNDHZiiSOeBBJJIpkUUkkjnQwyySKbHHLJI58COtCRTnSmC13pRnd60JNe9KYPfelHfwbgQEPHSSEuiiimhFIGMojBDGEowxiOGw9leMM7GoxgJKMYzRjGMo7xTGAik5jMFKYyjelUMIOZzGI2c5jLPOazgEqJ4igttHKD/XxkM7vZwQGOc0ysbOc9m9gnNolml8Swldt8EDsHOcEvfvKbI5ziAfc4zUIWsYcqHlHNfR7yjMc84Wn4TjW85DkvOIOPH+zlDa94jZ8vfGMbiwmwhKXUUsch6llGA0EaCbGcFazkM6tYTRNrWMdarnKYZtazgY185TvXOMs5rvOWdxIrcRIvCZIoSZIsKZIqaZIuGZIpWZznApe5wh0ucom7bOGkZHOTW5IjueyUPMmXAquvtqnBr9lCdQGHw+E1o9OMbofSa+rRlerf41KWtqmH+5WaUlc6lYVKl7JIWawsUf6b5zbV1FxNs9cEfKFgdVVlo9980g1Tl2EpDwXr24PLKGvT8Jh7hNX/AtbOnHEAeNpFzqsOwkAQBdDdlr7pu6SKpOjVCIKlNTUETJuQ4JEILBgkWBzfMEsQhA/iN8qUbhc3507mZl60OQO9kBLMZcUpvda80Fk1gaAuIVnhcKrHoLNNRUDNclDZAqwsfxOV+kRhP5tZ/rC4gIEwdwI6wlgLaAh9LjBAaB8Buyv0+kIHl/ZNYIhw0g4UXPFDiKn7VBhXiwMyQIZbSR8ZTCW9tt+nMyKTqE3cY/NPYjyJ7pIJMt5LjpBJ2rOGhH0Bs3VX7QAAAAABVym5yAAA) format('woff');font-weight:400;font-style:normal}.joint-link.joint-theme-material .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-material .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-material .connection{stroke-linejoin:round}.joint-link.joint-theme-material .link-tools .tool-remove circle{fill:#C64242}.joint-link.joint-theme-material .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-material .marker-vertex{fill:#d0d8e8}.joint-link.joint-theme-material .marker-vertex:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-arrowhead{fill:#d0d8e8}.joint-link.joint-theme-material .marker-arrowhead:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-vertex-remove-area{fill:#5fa9ee}.joint-link.joint-theme-material .marker-vertex-remove{fill:#fff}.joint-link.joint-theme-modern .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-modern .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-modern .connection{stroke-linejoin:round}.joint-link.joint-theme-modern .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-modern .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-modern .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-modern .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-modern .marker-vertex-remove{fill:#fff} \ No newline at end of file diff --git a/dist/joint.core.min.js b/dist/joint.core.min.js index 58ceb9285..606950987 100644 --- a/dist/joint.core.min.js +++ b/dist/joint.core.min.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -44,13 +44,16 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. }(this, function(root, Backbone, _, $) { !function(){function a(a){this.message=a}var b="undefined"!=typeof exports?exports:this,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a.prototype=new Error,a.prototype.name="InvalidCharacterError",b.btoa||(b.btoa=function(b){for(var d,e,f=String(b),g=0,h=c,i="";f.charAt(0|g)||(h="=",g%1);i+=h.charAt(63&d>>8-g%1*8)){if(e=f.charCodeAt(g+=.75),e>255)throw new a("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");d=d<<8|e}return i}),b.atob||(b.atob=function(b){var d=String(b).replace(/=+$/,"");if(d.length%4==1)throw new a("'atob' failed: The string to be decoded is not correctly encoded.");for(var e,f,g=0,h=0,i="";f=d.charAt(h++);~f&&(e=g%4?64*e+f:f,g++%4)?i+=String.fromCharCode(255&e>>(-2*g&6)):0)f=c.indexOf(f);return i})}(),function(){function a(a,b){return this.slice(a,b)}function b(a,b){arguments.length<2&&(b=0);for(var c=0,d=a.length;c>>0;if(0===e)return!1;for(var f=0|b,g=Math.max(f>=0?f:e-Math.abs(f),0);g>>0;if("function"!=typeof a)throw new TypeError("predicate must be a function");for(var d=arguments[1],e=0;e0?1:-1)*Math.floor(Math.abs(b)):b},d=Math.pow(2,53)-1,e=function(a){var b=c(a);return Math.min(Math.max(b,0),d)};return function(a){var c=this,d=Object(a);if(null==a)throw new TypeError("Array.from requires an array-like object - not null or undefined");var f,g=arguments.length>1?arguments[1]:void 0;if("undefined"!=typeof g){if(!b(g))throw new TypeError("Array.from: when provided, the second argument must be a function");arguments.length>2&&(f=arguments[2])}for(var h,i=e(d.length),j=b(c)?Object(new c(i)):new Array(i),k=0;k>>0;if("function"!=typeof a)throw new TypeError("predicate must be a function");for(var d=arguments[1],e=0;ethis.length)&&this.indexOf(a,b)!==-1}),String.prototype.startsWith||(String.prototype.startsWith=function(a,b){return this.substr(b||0,a.length)===a}),Number.isFinite=Number.isFinite||function(a){return"number"==typeof a&&isFinite(a)},Number.isNaN=Number.isNaN||function(a){return a!==a}; -var g=function(){var a={},b=Math,c=b.abs,d=b.cos,e=b.sin,f=b.sqrt,g=b.min,h=b.max,i=b.atan2,j=b.round,k=b.floor,l=b.PI,m=b.random,n=b.pow;a.bezier={curveThroughPoints:function(a){for(var b=this.getCurveControlPoints(a),c=["M",a[0].x,a[0].y],d=0;dj.x+h/2,n=fj.x?g-e:g+e,d=h*h/(f-k)-h*h*(g-l)*(c-l)/(i*i*(f-k))+k):(d=g>j.y?f+e:f-e,c=i*i/(g-l)-i*i*(f-k)*(d-k)/(h*h*(g-l))+l),a.point(d,c).theta(b)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a),b&&a.rotate(q(this.x,this.y),b);var c,d=a.x-this.x,e=a.y-this.y;if(0===d)return c=this.bbox().pointNearestToPoint(a),b?c.rotate(q(this.x,this.y),-b):c;var g=e/d,h=g*g,i=this.a*this.a,j=this.b*this.b,k=f(1/(1/i+h/j));k=d<0?-k:k;var l=g*k;return c=q(this.x+k,this.y+l),b?c.rotate(q(this.x,this.y),-b):c},toString:function(){return q(this.x,this.y).toString()+" "+this.a+" "+this.b}};var p=a.Line=function(a,b){return this instanceof p?a instanceof p?p(a.start,a.end):(this.start=q(a),void(this.end=q(b))):new p(a,b)};a.Line.prototype={bearing:function(){var a=w(this.start.y),b=w(this.end.y),c=this.start.x,f=this.end.x,g=w(f-c),h=e(g)*d(b),j=d(a)*e(b)-e(a)*d(b)*d(g),k=v(i(h,j)),l=["NE","E","SE","S","SW","W","NW","N"],m=k-22.5;return m<0&&(m+=360),m=parseInt(m/45),l[m]},clone:function(){return p(this.start,this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.end.x===a.end.x&&this.end.y===a.end.y},intersect:function(a){if(a instanceof p){var b=q(this.end.x-this.start.x,this.end.y-this.start.y),c=q(a.end.x-a.start.x,a.end.y-a.start.y),d=b.x*c.y-b.y*c.x,e=q(a.start.x-this.start.x,a.start.y-this.start.y),f=e.x*c.y-e.y*c.x,g=e.x*b.y-e.y*b.x;if(0===d||f*d<0||g*d<0)return null;if(d>0){if(f>d||g>d)return null}else if(f0?l:null}return null},length:function(){return f(this.squaredLength())},midpoint:function(){return q((this.start.x+this.end.x)/2,(this.start.y+this.end.y)/2)},pointAt:function(a){var b=(1-a)*this.start.x+a*this.end.x,c=(1-a)*this.start.y+a*this.end.y;return q(b,c)},pointOffset:function(a){return((this.end.x-this.start.x)*(a.y-this.start.y)-(this.end.y-this.start.y)*(a.x-this.start.x))/2},vector:function(){return q(this.end.x-this.start.x,this.end.y-this.start.y)},closestPoint:function(a){return this.pointAt(this.closestPointNormalizedLength(a))},closestPointNormalizedLength:function(a){var b=this.vector().dot(p(this.start,a).vector());return Math.min(1,Math.max(0,b/this.squaredLength()))},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},toString:function(){return this.start.toString()+" "+this.end.toString()}},a.Line.prototype.intersection=a.Line.prototype.intersect;var q=a.Point=function(a,b){if(!(this instanceof q))return new q(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseInt(c[0],10),b=parseInt(c[1],10)}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};a.Point.fromPolar=function(a,b,f){f=f&&q(f)||q(0,0);var g=c(a*d(b)),h=c(a*e(b)),i=t(v(b));return i<90?h=-h:i<180?(g=-g,h=-h):i<270&&(g=-g),q(f.x+g,f.y+h)},a.Point.random=function(a,b,c,d){return q(k(m()*(b-a+1)+a),k(m()*(d-c+1)+c))},a.Point.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=g(h(this.x,a.x),a.x+a.width),this.y=g(h(this.y,a.y),a.y+a.height),this)},bearing:function(a){return p(this,a).bearing()},changeInAngle:function(a,b,c){return q(this).offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return q(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),q(this.x-(a||0),this.y-(b||0))},distance:function(a){return p(this,a).length()},squaredDistance:function(a){return p(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return f(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return c(a.x-this.x)+c(a.y-this.y)},move:function(a,b){var c=w(q(a).theta(this));return this.offset(d(c)*b,-e(c)*b)},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return q(a).move(this,this.distance(a))},rotate:function(a,b){b=(b+360)%360,this.toPolar(a),this.y+=w(b);var c=q.fromPolar(this.x,this.y,a);return this.x=c.x,this.y=c.y,this},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this},scale:function(a,b,c){return c=c&&q(c)||q(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=u(this.x,a),this.y=u(this.y,b||a),this},theta:function(a){a=q(a);var b=-(a.y-this.y),c=a.x-this.x,d=i(b,c);return d<0&&(d=2*l+d),180*d/l},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=q(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&q(a)||q(0,0);var b=this.x,c=this.y;return this.x=f((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=w(a.theta(q(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN}};var r=a.Rect=function(a,b,c,d){return this instanceof r?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new r(a,b,c,d)};a.Rect.fromEllipse=function(a){return a=o(a),r(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},a.Rect.prototype={bbox:function(a){var b=w(a||0),f=c(e(b)),g=c(d(b)),h=this.width*g+this.height*f,i=this.width*f+this.height*g;return r(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return q(this.x,this.y+this.height)},bottomLine:function(){return p(this.bottomLeft(),this.corner())},bottomMiddle:function(){return q(this.x+this.width/2,this.y+this.height)},center:function(){return q(this.x+this.width/2,this.y+this.height/2)},clone:function(){return r(this)},containsPoint:function(a){return a=q(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=r(this).normalize(),c=r(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return q(this.x+this.width,this.y+this.height)},equals:function(a){var b=r(this).normalize(),c=r(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=Math.max(b.x,d.x),g=Math.max(b.y,d.y);return r(f,g,Math.min(c.x,e.x)-f,Math.min(c.y,e.y)-g)},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a);var c,d=q(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[p(this.origin(),this.topRight()),p(this.topRight(),this.corner()),p(this.corner(),this.bottomLeft()),p(this.bottomLeft(),this.origin())],f=p(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return p(this.origin(),this.bottomLeft())},leftMiddle:function(){return q(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return q.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return q(this.x,this.y)},pointNearestToPoint:function(a){if(a=q(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return q(this.x+this.width,a.y);case"left":return q(this.x,a.y);case"bottom":return q(a.x,this.y+this.height);case"top":return q(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return p(this.topRight(),this.corner())},rightMiddle:function(){return q(this.x+this.width,this.y+this.height/2)},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this.width=j(this.width*b)/b,this.height=j(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(b,c){b=a.Rect(b),c||(c=b.center());var d,e,f,g,h,i,j,k,l=c.x,m=c.y;d=e=f=g=h=i=j=k=1/0;var n=b.origin();n.xl&&(e=(this.x+this.width-l)/(o.x-l)),o.y>m&&(i=(this.y+this.height-m)/(o.y-m));var p=b.topRight();p.x>l&&(f=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:Math.min(d,e,f,g),sy:Math.min(h,i,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return Math.min(c.sx,c.sy)},sideNearestToPoint:function(a){a=q(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cc.x&&(c=d[a]);var e=[];for(b=d.length,a=0;a2){var h=e[e.length-1];e.unshift(h)}for(var i,j,k,l,m,n,o={},p=[];0!==e.length;)if(i=e.pop(),j=i[0],!o.hasOwnProperty(i[0]+"@@"+i[1]))for(var q=!1;!q;)if(p.length<2)p.push(i),q=!0;else{k=p.pop(),l=k[0],m=p.pop(),n=m[0];var r=n.cross(l,j);if(r<0)p.push(m),p.push(k),p.push(i),q=!0;else if(0===r){var t=1e-10,u=l.angleBetween(n,j);Math.abs(u-180)2&&p.pop();var v,w=-1;for(b=p.length,a=0;a0){var z=p.slice(w),A=p.slice(0,w);y=z.concat(A)}else y=p;var B=[];for(b=y.length,a=0;a1){var g,h,i=[];for(g=0,h=f.childNodes.length;gu&&(u=z),c=d("tspan",y.attrs),b.includeAnnotationIndices&&c.attr("annotations",y.annotations),y.attrs.class&&c.addClass(y.attrs.class),e&&x===w&&p!==s&&(y.t+=e),c.node.textContent=y.t}else e&&x===w&&p!==s&&(y+=e),c=document.createTextNode(y||" ");r.append(c)}"auto"===b.lineHeight&&u&&0!==p&&r.attr("dy",1.2*u+"px")}else e&&p!==s&&(t+=e),r.node.textContent=t;0===p&&(o=u)}else r.addClass("v-empty-line"),r.node.style.fillOpacity=0,r.node.style.strokeOpacity=0,r.node.textContent="-";d(g).append(r),l+=t.length+1}var A=this.attr("y");return null===A&&this.attr("y",o||"0.8em"),this},d.prototype.removeAttr=function(a){var b=d.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},d.prototype.attr=function(a,b){if(d.isUndefined(a)){for(var c=this.node.attributes,e={},f=0;f'+(a||"")+"",f=d.parseXML(e,{async:!1});return f.documentElement},d.idCounter=0,d.uniqueId=function(){return"v-"+ ++d.idCounter},d.toNode=function(a){return d.isV(a)?a.node:a.nodeName&&a||a[0]},d.ensureId=function(a){return a=d.toNode(a),a.id||(a.id=d.uniqueId())},d.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},d.isUndefined=function(a){return"undefined"==typeof a},d.isString=function(a){return"string"==typeof a},d.isObject=function(a){return a&&"object"==typeof a},d.isArray=Array.isArray,d.parseXML=function(a,b){b=b||{};var c;try{var e=new DOMParser;d.isUndefined(b.async)||(e.async=b.async),c=e.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},d.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var c=a.split(":");return{ns:b[c[0]],local:c[1]}}return{ns:null,local:a}},d.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,d.transformSeparatorRegex=/[ ,]+/,d.transformationListRegex=/^(\w+)\((.*)\)/,d.transformStringToMatrix=function(a){var b=d.createSVGMatrix(),c=a&&a.match(d.transformRegex);if(!c)return b;for(var e=0,f=c.length;e=0){var g=d.transformStringToMatrix(a),h=d.decomposeMatrix(g);b=[h.translateX,h.translateY],e=[h.scaleX,h.scaleY],c=[h.rotation];var i=[];0===b[0]&&0===b[0]||i.push("translate("+b+")"),1===e[0]&&1===e[1]||i.push("scale("+e+")"),0!==c[0]&&i.push("rotate("+c+")"),a=i.join(" ")}else{var j=a.match(/translate\((.*?)\)/);j&&(b=j[1].split(f));var k=a.match(/rotate\((.*?)\)/);k&&(c=k[1].split(f));var l=a.match(/scale\((.*?)\)/);l&&(e=l[1].split(f))}}var m=e&&e[0]?parseFloat(e[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:m,sy:e&&e[1]?parseFloat(e[1]):m}}},d.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},d.decomposeMatrix=function(a){var b=d.deltaTransformPoint(a,{x:0,y:1}),c=d.deltaTransformPoint(a,{x:1,y:0}),e=180/Math.PI*Math.atan2(b.y,b.x)-90,f=180/Math.PI*Math.atan2(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:Math.sqrt(a.a*a.a+a.b*a.b),scaleY:Math.sqrt(a.c*a.c+a.d*a.d),skewX:e,skewY:f,rotation:e}},d.matrixToScale=function(a){var b,c,e,f;return a?(b=d.isUndefined(a.a)?1:a.a,f=d.isUndefined(a.d)?1:a.d,c=a.b,e=a.c):b=f=1,{sx:c?Math.sqrt(b*b+c*c):b,sy:e?Math.sqrt(e*e+f*f):f}},d.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=d.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(Math.atan2(b.y,b.x))-90)}},d.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},d.isV=function(a){return a instanceof d},d.isVElement=d.isV;var e=d("svg").node;return d.createSVGMatrix=function(a){var b=e.createSVGMatrix();for(var c in a)b[c]=a[c];return b},d.createSVGTransform=function(a){return d.isUndefined(a)?e.createSVGTransform():(a instanceof SVGMatrix||(a=d.createSVGMatrix(a)),e.createSVGTransformFromMatrix(a))},d.createSVGPoint=function(a,b){var c=e.createSVGPoint();return c.x=a,c.y=b,c},d.transformRect=function(a,b){var c=e.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var f=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var h=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var i=c.matrixTransform(b),j=Math.min(d.x,f.x,h.x,i.x),k=Math.max(d.x,f.x,h.x,i.x),l=Math.min(d.y,f.y,h.y,i.y),m=Math.max(d.y,f.y,h.y,i.y);return g.Rect(j,l,k-j,m-l)},d.transformPoint=function(a,b){return g.Point(d.createSVGPoint(a.x,a.y).matrixTransform(b))},d.styleToObject=function(a){for(var b={},c=a.split(";"),d=0;d=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L"+f*n+","+f*o+"A"+f+","+f+" 0 "+k+",0 "+f*l+","+f*m+"Z":"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L0,0Z"},d.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?d.isObject(a[c])&&d.isObject(b[c])?a[c]=d.mergeAttrs(a[c],b[c]):d.isObject(a[c])?a[c]=d.mergeAttrs(a[c],d.styleToObject(b[c])):d.isObject(b[c])?a[c]=d.mergeAttrs(d.styleToObject(a[c]),b[c]):a[c]=d.mergeAttrs(d.styleToObject(a[c]),d.styleToObject(b[c])):a[c]=b[c];return a},d.annotateString=function(a,b,c){b=b||[],c=c||{};for(var e,f,g,h=c.offset||0,i=[],j=[],k=0;k=n&&k=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},d.convertLineToPathData=function(a){a=d(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},d.convertPolygonToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b)+" Z":null},d.convertPolylineToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b):null},d.svgPointsToPath=function(a){var b;for(b=0;b0){var f=joint.util.getByPath(a,d,c);f&&delete f[e]}else delete a[e];return a},flattenObject:function(a,b,c){b=b||"/";var d={};for(var e in a)if(a.hasOwnProperty(e)){var f="object"==typeof a[e];if(f&&c&&c(a[e])&&(f=!1),f){var g=this.flattenObject(a[e],b,c);for(var h in g)g.hasOwnProperty(h)&&(d[e+b+h]=g[h])}else d[e]=a[e]}return d},uuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"==a?b:3&b|8;return c.toString(16)})},guid:function(a){return this.guid.id=this.guid.id||1,a.id=void 0===a.id?"j_"+this.guid.id++:a.id,a.id},toKebabCase:function(a){return a.replace(/[A-Z]/g,"-$&").toLowerCase()},mixin:_.assign,supplement:_.defaults,deepMixin:_.mixin,deepSupplement:_.defaultsDeep,normalizeEvent:function(a){var b=a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0];if(b){for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return a},nextFrame:function(){var a;if("undefined"!=typeof window&&(a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame),!a){var b=0;a=function(a){var c=(new Date).getTime(),d=Math.max(0,16-(c-b)),e=setTimeout(function(){a(c+d)},d);return b=c+d,e}}return function(b,c){return a(c?b.bind(c):b)}}(),cancelFrame:function(){var a,b="undefined"!=typeof window;return b&&(a=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame),a=a||clearTimeout,b?a.bind(window):a}(),shapePerimeterConnectionPoint:function(a,b,c,d){var e,f;if(!c){var g=b.$(".scalable")[0],h=b.$(".rotatable")[0];g&&g.firstChild?c=g.firstChild:h&&h.firstChild&&(c=h.firstChild)}return c?(f=V(c).findIntersection(d,a.paper.viewport),f||(e=V(c).getBBox({target:a.paper.viewport}))):(e=b.model.getBBox(),f=e.intersectionWithLineFromCenterToPoint(d)),f||e.center()},parseCssNumeric:function(a,b){b=b||[];var c={value:parseFloat(a)};if(Number.isNaN(c.value))return null;var d=b.join("|");if(joint.util.isString(a)){var e=new RegExp("(\\d+)("+d+")$").exec(a);if(!e)return null;e[2]&&(c.unit=e[2])}return c},breakText:function(a,b,c,d){d=d||{};var e=b.width,f=b.height,g=d.svgDocument||V("svg").node,h=V("").attr(c||{}).node,i=h.firstChild,j=document.createTextNode("");h.style.opacity=0,h.style.display="block",i.style.display="block",i.appendChild(j),g.appendChild(h),d.svgDocument||document.body.appendChild(g);for(var k,l,m=a.split(" "),n=[],o=[],p=0,q=0,r=m.length;pf){o.splice(Math.floor(f/l));break}}}return d.svgDocument?g.removeChild(h):document.body.removeChild(g),o.join("\n")},imageToDataUri:function(a,b){if(!a||"data:"===a.substr(0,"data:".length))return setTimeout(function(){b(null,a)},0);var c=function(b,c){if(200===b.status){var d=new FileReader;d.onload=function(a){var b=a.target.result;c(null,b)},d.onerror=function(){c(new Error("Failed to load image "+a))},d.readAsDataURL(b.response)}else c(new Error("Failed to load image "+a))},d=function(b,c){var d=function(a){for(var b=32768,c=[],d=0;d=1)return 1;var b=a*a,c=b*a;return 4*(a<.5?c:3*(a-b)+c-.75)},exponential:function(a){return Math.pow(2,10*(a-1))},bounce:function(a){for(var b=0,c=1;1;b+=c,c/=2)if(a>=(7-4*b)/11){var d=(11-6*b-11*a)/4;return-d*d+c*c}},reverse:function(a){return function(b){return 1-a(1-b)}},reflect:function(a){return function(b){return.5*(b<.5?a(2*b):2-a(2-2*b))}},clamp:function(a,b,c){return b=b||0,c=c||1,function(d){var e=a(d);return ec?c:e}},back:function(a){return a||(a=1.70158),function(b){return b*b*((a+1)*b-a)}},elastic:function(a){return a||(a=1.5),function(b){return Math.pow(2,10*(b-1))*Math.cos(20*Math.PI*a/3*b)}}},interpolate:{number:function(a,b){var c=b-a;return function(b){return a+c*b}},object:function(a,b){var c=Object.keys(a);return function(d){var e,f,g={};for(e=c.length-1;e!=-1;e--)f=c[e],g[f]=a[f]+(b[f]-a[f])*d;return g}},hexColor:function(a,b){var c=parseInt(a.slice(1),16),d=parseInt(b.slice(1),16),e=255&c,f=(255&d)-e,g=65280&c,h=(65280&d)-g,i=16711680&c,j=(16711680&d)-i;return function(a){var b=e+f*a&255,c=g+h*a&65280,d=i+j*a&16711680;return"#"+(1<<24|b|c|d).toString(16).slice(1)}},unit:function(a,b){var c=/(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/,d=c.exec(a),e=c.exec(b),f=e[1].indexOf("."),g=f>0?e[1].length-f-1:0;a=+d[1];var h=+e[1]-a,i=d[2];return function(b){return(a+h*b).toFixed(g)+i}}},filter:{outline:function(a){var b='',c=Number.isFinite(a.margin)?a.margin:2,d=Number.isFinite(a.width)?a.width:1;return joint.util.template(b)({color:a.color||"blue",opacity:Number.isFinite(a.opacity)?a.opacity:1,outerRadius:c+d,innerRadius:c})},highlight:function(a){var b='';return joint.util.template(b)({color:a.color||"red",width:Number.isFinite(a.width)?a.width:1,blur:Number.isFinite(a.blur)?a.blur:0,opacity:Number.isFinite(a.opacity)?a.opacity:1})},blur:function(a){var b=Number.isFinite(a.x)?a.x:2;return joint.util.template('')({stdDeviation:Number.isFinite(a.y)?[b,a.y]:b})},dropShadow:function(a){var b="SVGFEDropShadowElement"in window?'':'';return joint.util.template(b)({dx:a.dx||0,dy:a.dy||0,opacity:Number.isFinite(a.opacity)?a.opacity:1,color:a.color||"black",blur:Number.isFinite(a.blur)?a.blur:4})},grayscale:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},sepia:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},saturate:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:1-b})},hueRotate:function(a){return joint.util.template('')({angle:a.angle||0})},invert:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:1-b})},brightness:function(a){return joint.util.template('')({amount:Number.isFinite(a.amount)?a.amount:1})},contrast:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:.5-b/2})}},format:{number:function(a,b,c){function d(a){for(var b=a.length,d=[],e=0,f=c.grouping[0];b>0&&f>0;)d.push(a.substring(b-=f,b+f)),f=c.grouping[e=(e+1)%c.grouping.length];return d.reverse().join(c.thousands)}c=c||{currency:["$",""],decimal:".",thousands:",",grouping:[3]};var e=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,f=e.exec(a),g=f[1]||" ",h=f[2]||">",i=f[3]||"",j=f[4]||"",k=f[5],l=+f[6],m=f[7],n=f[8],o=f[9],p=1,q="",r="",s=!1;switch(n&&(n=+n.substring(1)),(k||"0"===g&&"="===h)&&(k=g="0",h="=",m&&(l-=Math.floor((l-1)/4))),o){case"n":m=!0,o="g";break;case"%":p=100,r="%",o="f";break;case"p":p=100,r="%",o="r";break;case"b":case"o":case"x":case"X":"#"===j&&(q="0"+o.toLowerCase());break;case"c":case"d":s=!0,n=0;break;case"s":p=-1,o="r"}"$"===j&&(q=c.currency[0],r=c.currency[1]),"r"!=o||n||(o="g"),null!=n&&("g"==o?n=Math.max(1,Math.min(21,n)):"e"!=o&&"f"!=o||(n=Math.max(0,Math.min(20,n))));var t=k&&m;if(s&&b%1)return"";var u=b<0||0===b&&1/b<0?(b=-b,"-"):i,v=r;if(p<0){var w=this.prefix(b,n);b=w.scale(b),v=w.symbol+r}else b*=p;b=this.convert(o,b,n);var x=b.lastIndexOf("."),y=x<0?b:b.substring(0,x),z=x<0?"":c.decimal+b.substring(x+1);!k&&m&&c.grouping&&(y=d(y));var A=q.length+y.length+z.length+(t?0:u.length),B=A"===h?B+u+b:"^"===h?B.substring(0,A>>=1)+u+b+B.substring(A):u+(t?b:B+b))+v},string:function(a,b){for(var c,d="{",e=!1,f=[];(c=a.indexOf(d))!==-1;){var g,h,i;if(g=a.slice(0,c),e){h=g.split(":"),i=h.shift().split("."),g=b;for(var j=0;j8?function(a){return a/c}:function(a){return a*c},symbol:a}}),d=0;return a&&(a<0&&(a*=-1),b&&(a=this.round(a,this.precision(a,b))),d=1+Math.floor(1e-12+Math.log(a)/Math.LN10),d=Math.max(-24,Math.min(24,3*Math.floor((d<=0?d+1:d-1)/3)))),c[8+d/3]}},template:function(a){var b=/<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g;return function(c){return c=c||{},a.replace(b,function(a){for(var b=Array.from(arguments),d=b.slice(1,4).find(function(a){return!!a}),e=d.split("."),f=c[e.shift()];void 0!==f&&e.length;)f=f[e.shift()];return void 0!==f?f:""})}},toggleFullScreen:function(a){function b(a,b){for(var c=["webkit","moz","ms","o",""],d=0;d0&&b[0]||[],e=c>1&&b[c-1]||{};return Array.isArray(d)||(e instanceof joint.dia.Cell?d=b:d instanceof joint.dia.Cell&&(b.length>1&&b.pop(),d=b)),e instanceof joint.dia.Cell&&(e={}),a.call(this,d,e)}}},sortedIndex:_.sortedIndexBy||_.sortedIndex,uniq:_.uniqBy||_.uniq,uniqueId:_.uniqueId,sortBy:_.sortBy,isFunction:_.isFunction,result:_.result,union:_.union,invoke:_.invokeMap||_.invoke,difference:_.difference,intersection:_.intersection,omit:_.omit,pick:_.pick,has:_.has,bindAll:_.bindAll,assign:_.assign,defaults:_.defaults,defaultsDeep:_.defaultsDeep,isPlainObject:_.isPlainObject,isEmpty:_.isEmpty,isEqual:_.isEqual,noop:function(){},cloneDeep:_.cloneDeep,toArray:_.toArray,flattenDeep:_.flattenDeep,camelCase:_.camelCase,groupBy:_.groupBy,forIn:_.forIn,without:_.without,debounce:_.debounce,clone:_.clone,isBoolean:function(a){var b=Object.prototype.toString;return a===!0||a===!1||!!a&&"object"==typeof a&&"[object Boolean]"===b.call(a)},isObject:function(a){return!!a&&("object"==typeof a||"function"==typeof a)},isNumber:function(a){var b=Object.prototype.toString;return"number"==typeof a||!!a&&"object"==typeof a&&"[object Number]"===b.call(a)},isString:function(a){var b=Object.prototype.toString;return"string"==typeof a||!!a&&"object"==typeof a&&"[object String]"===b.call(a)},merge:function(){if(_.mergeWith){var a=Array.from(arguments),b=a[a.length-1],c=this.isFunction(b)?b:this.noop;return a.push(function(a,b){var d=c(a,b);return void 0!==d?d:Array.isArray(a)&&!Array.isArray(b)?b:void 0}),_.mergeWith.apply(this,a)}return _.merge.apply(this,arguments)}}};joint.mvc.View=Backbone.View.extend({options:{},theme:null,themeClassNamePrefix:joint.util.addClassNamePrefix("theme-"),requireSetThemeOverride:!1,defaultTheme:joint.config.defaultTheme,constructor:function(a){this.requireSetThemeOverride=a&&!!a.theme,this.options=joint.util.assign({},this.options,a),Backbone.View.call(this,a)},initialize:function(a){joint.util.bindAll(this,"setTheme","onSetTheme","remove","onRemove"),joint.mvc.views[this.cid]=this,this.setTheme(this.options.theme||this.defaultTheme),this.init()},_ensureElement:function(){if(this.el)this.setElement(joint.util.result(this,"el"));else{var a=joint.util.result(this,"tagName"),b=joint.util.assign({},joint.util.result(this,"attributes"));this.id&&(b.id=joint.util.result(this,"id")),this.setElement(this._createElement(a)),this._setAttributes(b)}this._ensureElClassName()},_setAttributes:function(a){this.svgElement?this.vel.attr(a):this.$el.attr(a)},_createElement:function(a){return this.svgElement?document.createElementNS(V.namespace.xmlns,a):document.createElement(a)},_setElement:function(a){this.$el=a instanceof Backbone.$?a:Backbone.$(a),this.el=this.$el[0],this.svgElement&&(this.vel=V(this.el))},_ensureElClassName:function(){var a=joint.util.result(this,"className"),b=joint.util.addClassNamePrefix(a);this.svgElement?this.vel.removeClass(a).addClass(b):this.$el.removeClass(a).addClass(b)},init:function(){},onRender:function(){},setTheme:function(a,b){return b=b||{},this.theme&&this.requireSetThemeOverride&&!b.override?this:(this.removeThemeClassName(),this.addThemeClassName(a),this.onSetTheme(this.theme,a),this.theme=a,this)},addThemeClassName:function(a){a=a||this.theme;var b=this.themeClassNamePrefix+a;return this.$el.addClass(b),this},removeThemeClassName:function(a){a=a||this.theme;var b=this.themeClassNamePrefix+a;return this.$el.removeClass(b),this},onSetTheme:function(a,b){},remove:function(){return this.onRemove(),joint.mvc.views[this.cid]=null,Backbone.View.prototype.remove.apply(this,arguments),this},onRemove:function(){},getEventNamespace:function(){return".joint-event-ns-"+this.cid}},{extend:function(){var a=Array.from(arguments),b=a[0]&&joint.util.assign({},a[0])||{},c=a[1]&&joint.util.assign({},a[1])||{},d=b.render||this.prototype&&this.prototype.render||null;return b.render=function(){return d&&d.apply(this,arguments),this.onRender(),this},Backbone.View.extend.call(this,b,c)}}),joint.dia.GraphCells=Backbone.Collection.extend({cellNamespace:joint.shapes,initialize:function(a,b){b.cellNamespace&&(this.cellNamespace=b.cellNamespace),this.graph=b.graph},model:function(a,b){var c=b.collection,d=c.cellNamespace,e="link"===a.type?joint.dia.Link:joint.util.getByPath(d,a.type,".")||joint.dia.Element,f=new e(a,b);return f.graph=c.graph,f},comparator:function(a){return a.get("z")||0}}),joint.dia.Graph=Backbone.Model.extend({_batches:{},initialize:function(a,b){b=b||{};var c=new joint.dia.GraphCells([],{model:b.cellModel,cellNamespace:b.cellNamespace,graph:this});Backbone.Model.prototype.set.call(this,"cells",c),c.on("all",this.trigger,this),this.on("change:z",this._sortOnChangeZ,this),this.on("batch:stop",this._onBatchStop,this),this._out={},this._in={},this._nodes={},this._edges={},c.on("add",this._restructureOnAdd,this),c.on("remove",this._restructureOnRemove,this),c.on("reset",this._restructureOnReset,this),c.on("change:source",this._restructureOnChangeSource,this),c.on("change:target",this._restructureOnChangeTarget,this),c.on("remove",this._removeCell,this)},_sortOnChangeZ:function(){this.hasActiveBatch("to-front")||this.hasActiveBatch("to-back")||this.get("cells").sort()},_onBatchStop:function(a){var b=a&&a.batchName;"to-front"!==b&&"to-back"!==b||this.hasActiveBatch(b)||this.get("cells").sort()},_restructureOnAdd:function(a){if(a.isLink()){this._edges[a.id]=!0;var b=a.get("source"),c=a.get("target");b.id&&((this._out[b.id]||(this._out[b.id]={}))[a.id]=!0),c.id&&((this._in[c.id]||(this._in[c.id]={}))[a.id]=!0)}else this._nodes[a.id]=!0},_restructureOnRemove:function(a){if(a.isLink()){delete this._edges[a.id];var b=a.get("source"),c=a.get("target");b.id&&this._out[b.id]&&this._out[b.id][a.id]&&delete this._out[b.id][a.id],c.id&&this._in[c.id]&&this._in[c.id][a.id]&&delete this._in[c.id][a.id]}else delete this._nodes[a.id]},_restructureOnReset:function(a){a=a.models,this._out={},this._in={},this._nodes={},this._edges={},a.forEach(this._restructureOnAdd,this)},_restructureOnChangeSource:function(a){var b=a.previous("source");b.id&&this._out[b.id]&&delete this._out[b.id][a.id];var c=a.get("source");c.id&&((this._out[c.id]||(this._out[c.id]={}))[a.id]=!0)},_restructureOnChangeTarget:function(a){var b=a.previous("target");b.id&&this._in[b.id]&&delete this._in[b.id][a.id];var c=a.get("target");c.id&&((this._in[c.id]||(this._in[c.id]={}))[a.id]=!0)},getOutboundEdges:function(a){return this._out&&this._out[a]||{}},getInboundEdges:function(a){return this._in&&this._in[a]||{}},toJSON:function(){var a=Backbone.Model.prototype.toJSON.apply(this,arguments);return a.cells=this.get("cells").toJSON(),a},fromJSON:function(a,b){if(!a.cells)throw new Error("Graph JSON must contain cells array.");return this.set(a,b)},set:function(a,b,c){var d;return"object"==typeof a?(d=a,c=b):(d={})[a]=b,d.hasOwnProperty("cells")&&(this.resetCells(d.cells,c),d=joint.util.omit(d,"cells")),Backbone.Model.prototype.set.call(this,d,c)},clear:function(a){a=joint.util.assign({},a,{clear:!0});var b=this.get("cells");if(0===b.length)return this;this.startBatch("clear",a);var c=b.sortBy(function(a){return a.isLink()?1:2});do c.shift().remove(a);while(c.length>0);return this.stopBatch("clear"),this},_prepareCell:function(a,b){var c;if(a instanceof Backbone.Model?(c=a.attributes,a.graph||b&&b.dry||(a.graph=this)):c=a,!joint.util.isString(c.type))throw new TypeError("dia.Graph: cell type must be a string.");return a},maxZIndex:function(){var a=this.get("cells").last();return a?a.get("z")||0:0},addCell:function(a,b){return Array.isArray(a)?this.addCells(a,b):(a instanceof Backbone.Model?a.has("z")||a.set("z",this.maxZIndex()+1):void 0===a.z&&(a.z=this.maxZIndex()+1),this.get("cells").add(this._prepareCell(a,b),b||{}),this)},addCells:function(a,b){return a.length&&(a=joint.util.flattenDeep(a),b.position=a.length,this.startBatch("add"),a.forEach(function(a){b.position--,this.addCell(a,b)},this),this.stopBatch("add")),this},resetCells:function(a,b){var c=joint.util.toArray(a).map(function(a){return this._prepareCell(a,b)},this);return this.get("cells").reset(c,b),this},removeCells:function(a,b){return a.length&&(this.startBatch("remove"),joint.util.invoke(a,"remove",b),this.stopBatch("remove")),this},_removeCell:function(a,b,c){c=c||{},c.clear||(c.disconnectLinks?this.disconnectLinks(a,c):this.removeLinks(a,c)),this.get("cells").remove(a,{silent:!0}),a.graph===this&&(a.graph=null)},getCell:function(a){return this.get("cells").get(a)},getCells:function(){return this.get("cells").toArray()},getElements:function(){return Object.keys(this._nodes).map(this.getCell,this)},getLinks:function(){return Object.keys(this._edges).map(this.getCell,this)},getFirstCell:function(){return this.get("cells").first()},getLastCell:function(){return this.get("cells").last()},getConnectedLinks:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=[],f={};if(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),b.deep){var g=a.getEmbeddedCells({deep:!0}),h={};g.forEach(function(a){a.isLink()&&(h[a.id]=!0)}),g.forEach(function(a){a.isLink()||(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)))},this)}return e},getNeighbors:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=this.getConnectedLinks(a,b).reduce(function(e,f){var g=f.get("source"),h=f.get("target"),i=f.hasLoop(b);if(c&&joint.util.has(g,"id")&&!e[g.id]){var j=this.getCell(g.id);!i&&(!j||j===a||b.deep&&j.isEmbeddedIn(a))||(e[g.id]=j)}if(d&&joint.util.has(h,"id")&&!e[h.id]){var k=this.getCell(h.id);!i&&(!k||k===a||b.deep&&k.isEmbeddedIn(a))||(e[h.id]=k)}return e}.bind(this),{});return joint.util.toArray(e)},getCommonAncestor:function(){var a=Array.from(arguments).map(function(a){for(var b=[],c=a.get("parent");c;)b.push(c),c=this.getCell(c).get("parent");return b},this);a=a.sort(function(a,b){return a.length-b.length});var b=joint.util.toArray(a.shift()).find(function(b){return a.every(function(a){return a.includes(b)})});return this.getCell(b)},getSuccessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{outbound:!0})),c},cloneCells:function(a){a=joint.util.uniq(a);var b=joint.util.toArray(a).reduce(function(a,b){return a[b.id]=b.clone(),a},{});return joint.util.toArray(a).forEach(function(a){var c=b[a.id];if(c.isLink()){var d=c.get("source"),e=c.get("target");d.id&&b[d.id]&&c.prop("source/id",b[d.id].id),e.id&&b[e.id]&&c.prop("target/id",b[e.id].id)}var f=a.get("parent");f&&b[f]&&c.set("parent",b[f].id);var g=joint.util.toArray(a.get("embeds")).reduce(function(a,c){return b[c]&&a.push(b[c].id),a},[]);joint.util.isEmpty(g)||c.set("embeds",g)}),b},cloneSubgraph:function(a,b){var c=this.getSubgraph(a,b);return this.cloneCells(c)},getSubgraph:function(a,b){b=b||{};var c=[],d={},e=[],f=[];return joint.util.toArray(a).forEach(function(a){if(d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a)),b.deep){var g=a.getEmbeddedCells({deep:!0});g.forEach(function(a){d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a))})}}),f.forEach(function(a){var b=a.get("source"),f=a.get("target");if(b.id&&!d[b.id]){var g=this.getCell(b.id);c.push(g),d[g.id]=g,e.push(g)}if(f.id&&!d[f.id]){var h=this.getCell(f.id);c.push(this.getCell(f.id)),d[h.id]=h,e.push(h)}},this),e.forEach(function(a){var e=this.getConnectedLinks(a,b);e.forEach(function(a){var b=a.get("source"),e=a.get("target");!d[a.id]&&b.id&&d[b.id]&&e.id&&d[e.id]&&(c.push(a),d[a.id]=a)})},this),c},getPredecessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{inbound:!0})),c},search:function(a,b,c){c=c||{},c.breadthFirst?this.bfs(a,b,c):this.dfs(a,b,c)},bfs:function(a,b,c){c=c||{};var d={},e={},f=[];for(f.push(a),e[a.id]=0;f.length>0;){var g=f.shift();if(!d[g.id]){if(d[g.id]=!0,b(g,e[g.id])===!1)return;this.getNeighbors(g,c).forEach(function(a){e[a.id]=e[g.id]+1,f.push(a)})}}},dfs:function(a,b,c,d,e){c=c||{};var f=d||{},g=e||0;b(a,g)!==!1&&(f[a.id]=!0,this.getNeighbors(a,c).forEach(function(a){f[a.id]||this.dfs(a,b,c,f,g+1)},this))},getSources:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._in[c]&&!joint.util.isEmpty(this._in[c])||a.push(this.getCell(c))}.bind(this)),a},getSinks:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._out[c]&&!joint.util.isEmpty(this._out[c])||a.push(this.getCell(c))}.bind(this)),a},isSource:function(a){return!this._in[a.id]||joint.util.isEmpty(this._in[a.id])},isSink:function(a){return!this._out[a.id]||joint.util.isEmpty(this._out[a.id])},isSuccessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{outbound:!0}),c},isPredecessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{inbound:!0}),c},isNeighbor:function(a,b,c){c=c||{};var d=c.inbound,e=c.outbound;void 0===d&&void 0===e&&(d=e=!0);var f=!1;return this.getConnectedLinks(a,c).forEach(function(a){var c=a.get("source"),g=a.get("target");return d&&joint.util.has(c,"id")&&c.id===b.id?(f=!0,!1):e&&joint.util.has(g,"id")&&g.id===b.id?(f=!0,!1):void 0}),f},disconnectLinks:function(a,b){this.getConnectedLinks(a).forEach(function(c){c.set(c.get("source").id===a.id?"source":"target",{x:0,y:0},b)})},removeLinks:function(a,b){joint.util.invoke(this.getConnectedLinks(a),"remove",b)},findModelsFromPoint:function(a){return this.getElements().filter(function(b){return b.getBBox().containsPoint(a)})},findModelsInArea:function(a,b){a=g.rect(a),b=joint.util.defaults(b||{},{strict:!1});var c=b.strict?"containsRect":"intersect";return this.getElements().filter(function(b){return a[c](b.getBBox())})},findModelsUnderElement:function(a,b){b=joint.util.defaults(b||{},{searchBy:"bbox"});var c=a.getBBox(),d="bbox"===b.searchBy?this.findModelsInArea(c):this.findModelsFromPoint(c[b.searchBy]());return d.filter(function(b){return a.id!==b.id&&!b.isEmbeddedIn(a)})},getBBox:function(a,b){return this.getCellsBBox(a||this.getElements(),b)},getCellsBBox:function(a,b){return joint.util.toArray(a).reduce(function(a,c){return c.isLink()?a:a?a.union(c.getBBox(b)):c.getBBox(b)},null)},translate:function(a,b,c){var d=this.getCells().filter(function(a){return!a.isEmbedded()});return joint.util.invoke(d,"translate",a,b,c),this},resize:function(a,b,c){return this.resizeCells(a,b,this.getCells(),c)},resizeCells:function(a,b,c,d){var e=this.getCellsBBox(c);if(e){var f=Math.max(a/e.width,0),g=Math.max(b/e.height,0);joint.util.invoke(c,"scale",f,g,e.origin(),d)}return this; -},startBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)+1,this.trigger("batch:start",joint.util.assign({},b,{batchName:a}))},stopBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)-1,this.trigger("batch:stop",joint.util.assign({},b,{batchName:a}))},hasActiveBatch:function(a){return a?!!this._batches[a]:joint.util.toArray(this._batches).some(function(a){return a>0})}}),joint.util.wrapWith(joint.dia.Graph.prototype,["resetCells","addCells","removeCells"],"cells"),function(a,b,c,d,e){function f(a){return e.isString(a)&&"%"===a.slice(-1)}function g(a,b){return function(c,d){var e=f(c);c=parseFloat(c),e&&(c/=100);var g={};if(isFinite(c)){var h=e||c>=0&&c<=1?c*d[b]:Math.max(c+d[b],0);g[a]=h}return g}}function h(a,b,d){return function(e,g){var h=f(e);e=parseFloat(e),h&&(e/=100);var i;if(isFinite(e)){var j=g[d]();i=h||e>0&&e<1?j[a]+g[b]*e:j[a]+e}var k=c.Point();return k[a]=i||0,k}}function i(a,b,d){return function(e,g){var h;h="middle"===e?g[b]/2:e===d?g[b]:isFinite(e)?e>-1&&e<1?-g[b]*e:-e:f(e)?g[b]*parseFloat(e)/100:0;var i=c.Point();return i[a]=-(g[a]+h),i}}var j=a.dia.attributes={xlinkHref:{set:"xlink:href"},xlinkShow:{set:"xlink:show"},xlinkRole:{set:"xlink:role"},xlinkType:{set:"xlink:type"},xlinkArcrole:{set:"xlink:arcrole"},xlinkTitle:{set:"xlink:title"},xlinkActuate:{set:"xlink:actuate"},xmlSpace:{set:"xml:space"},xmlBase:{set:"xml:base"},xmlLang:{set:"xml:lang"},preserveAspectRatio:{set:"preserveAspectRatio"},requiredExtension:{set:"requiredExtension"},requiredFeatures:{set:"requiredFeatures"},systemLanguage:{set:"systemLanguage"},externalResourcesRequired:{set:"externalResourceRequired"},filter:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineFilter(a)+")"}},fill:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},stroke:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},sourceMarker:{qualify:e.isPlainObject,set:function(a){return{"marker-start":"url(#"+this.paper.defineMarker(a)+")"}}},targetMarker:{qualify:e.isPlainObject,set:function(a){return a=e.assign({transform:"rotate(180)"},a),{"marker-end":"url(#"+this.paper.defineMarker(a)+")"}}},vertexMarker:{qualify:e.isPlainObject,set:function(a){return{"marker-mid":"url(#"+this.paper.defineMarker(a)+")"}}},text:{set:function(b,c,e,f){var g=d(e),h="joint-text",i=g.data(h),j=a.util.pick(f,"lineHeight","annotations","textPath","x","eol"),k=j.fontSize=f["font-size"]||f.fontSize,l=JSON.stringify([b,j]);void 0!==i&&i===l||(k&&e.setAttribute("font-size",k),V(e).text(""+b,j),g.data(h,l))}},textWrap:{qualify:e.isPlainObject,set:function(b,c,d,e){var g=b.width||0;f(g)?c.width*=parseFloat(g)/100:g<=0?c.width+=g:c.width=g;var h=b.height||0;f(h)?c.height*=parseFloat(h)/100:h<=0?c.height+=h:c.height=h;var i=a.util.breakText(""+b.text,c,{"font-weight":e["font-weight"]||e.fontWeight,"font-size":e["font-size"]||e.fontSize,"font-family":e["font-family"]||e.fontFamily},{svgDocument:this.paper.svg});V(d).text(i)}},lineHeight:{qualify:function(a,b,c){return void 0!==c.text}},textPath:{qualify:function(a,b,c){return void 0!==c.text}},annotations:{qualify:function(a,b,c){return void 0!==c.text}},port:{set:function(a){return null===a||void 0===a.id?a:a.id}},style:{qualify:e.isPlainObject,set:function(a,b,c){d(c).css(a)}},html:{set:function(a,b,c){d(c).html(a+"")}},ref:{},refX:{position:h("x","width","origin")},refY:{position:h("y","height","origin")},refDx:{position:h("x","width","corner")},refDy:{position:h("y","height","corner")},refWidth:{set:g("width","width")},refHeight:{set:g("height","height")},refRx:{set:g("rx","width")},refRy:{set:g("ry","height")},refCx:{set:g("cx","width")},refCy:{set:g("cy","height")},xAlignment:{offset:i("x","width","right")},yAlignment:{offset:i("y","height","bottom")},resetOffset:{offset:function(a,b){return a?{x:-b.x,y:-b.y}:{x:0,y:0}}}};j.refX2=j.refX,j.refY2=j.refY,j["ref-x"]=j.refX,j["ref-y"]=j.refY,j["ref-dy"]=j.refDy,j["ref-dx"]=j.refDx,j["ref-width"]=j.refWidth,j["ref-height"]=j.refHeight,j["x-alignment"]=j.xAlignment,j["y-alignment"]=j.yAlignment}(joint,_,g,$,joint.util),joint.dia.Cell=Backbone.Model.extend({constructor:function(a,b){var c,d=a||{};this.cid=joint.util.uniqueId("c"),this.attributes={},b&&b.collection&&(this.collection=b.collection),b&&b.parse&&(d=this.parse(d,b)||{}),(c=joint.util.result(this,"defaults"))&&(d=joint.util.merge({},c,d)),this.set(d,b),this.changed={},this.initialize.apply(this,arguments)},translate:function(a,b,c){throw new Error("Must define a translate() method.")},toJSON:function(){var a=this.constructor.prototype.defaults.attrs||{},b=this.attributes.attrs,c={};joint.util.forIn(b,function(b,d){var e=a[d];joint.util.forIn(b,function(a,b){joint.util.isObject(a)&&!Array.isArray(a)?joint.util.forIn(a,function(a,f){e&&e[b]&&joint.util.isEqual(e[b][f],a)||(c[d]=c[d]||{},(c[d][b]||(c[d][b]={}))[f]=a)}):e&&joint.util.isEqual(e[b],a)||(c[d]=c[d]||{},c[d][b]=a)})});var d=joint.util.cloneDeep(joint.util.omit(this.attributes,"attrs"));return d.attrs=c,d},initialize:function(a){a&&a.id||this.set("id",joint.util.uuid(),{silent:!0}),this._transitionIds={},this.processPorts(),this.on("change:attrs",this.processPorts,this)},processPorts:function(){var a=this.ports,b={};joint.util.forIn(this.get("attrs"),function(a,c){a&&a.port&&(void 0!==a.port.id?b[a.port.id]=a.port:b[a.port]={id:a.port})});var c={};if(joint.util.forIn(a,function(a,d){b[d]||(c[d]=!0)}),this.graph&&!joint.util.isEmpty(c)){var d=this.graph.getConnectedLinks(this,{inbound:!0});d.forEach(function(a){c[a.get("target").port]&&a.remove()});var e=this.graph.getConnectedLinks(this,{outbound:!0});e.forEach(function(a){c[a.get("source").port]&&a.remove()})}this.ports=b},remove:function(a){a=a||{};var b=this.graph;b&&b.startBatch("remove");var c=this.get("parent");if(c){var d=b&&b.getCell(c);d.unembed(this)}return joint.util.invoke(this.getEmbeddedCells(),"remove",a),this.trigger("remove",this,this.collection,a),b&&b.stopBatch("remove"),this},toFront:function(a){if(this.graph){a=a||{};var b=(this.graph.getLastCell().get("z")||0)+1;if(this.startBatch("to-front").set("z",b,a),a.deep){var c=this.getEmbeddedCells({deep:!0,breadthFirst:!0});c.forEach(function(c){c.set("z",++b,a)})}this.stopBatch("to-front")}return this},toBack:function(a){if(this.graph){a=a||{};var b=(this.graph.getFirstCell().get("z")||0)-1;if(this.startBatch("to-back"),a.deep){var c=this.getEmbeddedCells({deep:!0,breadthFirst:!0});c.reverse().forEach(function(c){c.set("z",b--,a)})}this.set("z",b,a).stopBatch("to-back")}return this},embed:function(a,b){if(this===a||this.isEmbeddedIn(a))throw new Error("Recursive embedding not allowed.");this.startBatch("embed");var c=joint.util.assign([],this.get("embeds"));return c[a.isLink()?"unshift":"push"](a.id),a.set("parent",this.id,b),this.set("embeds",joint.util.uniq(c),b),this.stopBatch("embed"),this},unembed:function(a,b){return this.startBatch("unembed"),a.unset("parent",b),this.set("embeds",joint.util.without(this.get("embeds"),a.id),b),this.stopBatch("unembed"),this},getAncestors:function(){var a=[],b=this.get("parent");if(!this.graph)return a;for(;void 0!==b;){var c=this.graph.getCell(b);if(void 0===c)break;a.push(c),b=c.get("parent")}return a},getEmbeddedCells:function(a){if(a=a||{},this.graph){var b;if(a.deep)if(a.breadthFirst){b=[];for(var c=this.getEmbeddedCells();c.length>0;){var d=c.shift();b.push(d),c.push.apply(c,d.getEmbeddedCells())}}else b=this.getEmbeddedCells(),b.forEach(function(c){b.push.apply(b,c.getEmbeddedCells(a))});else b=joint.util.toArray(this.get("embeds")).map(this.graph.getCell,this.graph);return b}return[]},isEmbeddedIn:function(a,b){var c=joint.util.isString(a)?a:a.id,d=this.get("parent");if(b=joint.util.defaults({deep:!0},b),this.graph&&b.deep){for(;d;){if(d===c)return!0;d=this.graph.getCell(d).get("parent")}return!1}return d===c},isEmbedded:function(){return!!this.get("parent")},clone:function(a){if(a=a||{},a.deep)return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null,[this].concat(this.getEmbeddedCells({deep:!0}))));var b=Backbone.Model.prototype.clone.apply(this,arguments);return b.set("id",joint.util.uuid()),b.unset("embeds"),b.unset("parent"),b},prop:function(a,b,c){var d="/",e=joint.util.isString(a);if(e||Array.isArray(a)){if(arguments.length>1){var f,g;e?(f=a,g=f.split("/")):(f=a.join(d),g=a.slice());var h=g[0],i=g.length;if(c=c||{},c.propertyPath=f,c.propertyValue=b,c.propertyPathArray=g,1===i)return this.set(h,b,c);for(var j={},k=j,l=h,m=1;m0)},getSelector:function(a,b){if(a===this.el)return b;var c;if(a){var d=V(a).index()+1;c=a.tagName+":nth-child("+d+")",b&&(c+=" > "+b),c=this.getSelector(a.parentNode,c)}return c},getAttributeDefinition:function(a){return this.model.constructor.getAttributeDefinition(a)},setNodeAttributes:function(a,b){joint.util.isEmpty(b)||(a instanceof SVGElement?V(a).attr(b):$(a).attr(b))},processNodeAttributes:function(a,b){var c,d,e,f,g,h,i,j,k,l=[];for(c in b)b.hasOwnProperty(c)&&(d=b[c],e=this.getAttributeDefinition(c),!e||joint.util.isFunction(e.qualify)&&!e.qualify.call(this,d,a,b)?(h||(h={}),h[joint.util.toKebabCase(c)]=d):(joint.util.isString(e.set)&&(h||(h={}),h[e.set]=d),null!==d&&l.push(c,e)));for(f=0,g=l.length;f0&&x.height>0){var y=V.transformRect(a.getBBox(),p).scale(1/r,1/s);for(e in m)f=m[e],h=this.getAttributeDefinition(e),t=h.offset.call(this,f,y,a,i),t&&(q.offset(g.Point(t).scale(r,s)),w||(w=!0))}}(void 0!==o||v||w)&&(q.round(1),p.e=q.x,p.f=q.y,a.setAttribute("transform",V.matrixToTransformString(p)))},getNodeScale:function(a,b){var c,d;if(b&&b.contains(a)){var e=b.scale();c=1/e.sx,d=1/e.sy}else c=1,d=1;return{sx:c,sy:d}},findNodesAttributes:function(a,b,c){var d={};for(var e in a)if(a.hasOwnProperty(e))for(var f=c[e]=this.findBySelector(e,b),g=0,h=f.length;g-1?l.splice(t,0,d):l.push(d)}else this.setNodeAttributes(e,i.normal);for(var u=0,v=l.length;u0){this.startBatch("fit-embeds",a),a.deep&&joint.util.invoke(b,"fitEmbeds",a);var c=this.graph.getCellsBBox(b),d=joint.util.normalizeSides(a.padding);c.moveAndExpand({x:-d.left,y:-d.top,width:d.right+d.left,height:d.bottom+d.top}),this.set({position:{x:c.x,y:c.y},size:{width:c.width,height:c.height}},a),this.stopBatch("fit-embeds")}return this},rotate:function(a,b,c,d){if(c){var e=this.getBBox().center(),f=this.get("size"),g=this.get("position");e.rotate(c,this.get("angle")-a);var h=e.x-f.width/2-g.x,i=e.y-f.height/2-g.y;this.startBatch("rotate",{angle:a,absolute:b,origin:c}),this.position(g.x+h,g.y+i,d),this.rotate(a,b,null,d),this.stopBatch("rotate")}else this.set("angle",b?a:(this.get("angle")+a)%360,d);return this},getBBox:function(a){if(a=a||{},a.deep&&this.graph){var b=this.getEmbeddedCells({deep:!0,breadthFirst:!0});return b.push(this),this.graph.getCellsBBox(b)}var c=this.get("position"),d=this.get("size");return g.rect(c.x,c.y,d.width,d.height)}}),joint.dia.ElementView=joint.dia.CellView.extend({_removePorts:function(){},_renderPorts:function(){},className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("element"),a.join(" ")},initialize:function(){joint.dia.CellView.prototype.initialize.apply(this,arguments);var a=this.model;this.listenTo(a,"change:position",this.translate),this.listenTo(a,"change:size",this.resize),this.listenTo(a,"change:angle",this.rotate),this.listenTo(a,"change:markup",this.render),this._initializePorts()},_initializePorts:function(){},update:function(a,b){this._removePorts();var c=this.model,d=c.attr();this.updateDOMSubtreeAttributes(this.el,d,{rootBBox:g.Rect(c.size()),scalableNode:this.scalableNode,rotatableNode:this.rotatableNode,roAttributes:b===d?null:b}),this._renderPorts()},renderMarkup:function(){var a=this.model.get("markup")||this.model.markup;if(!a)throw new Error("properties.markup is missing while the default render() implementation is used.");var b=joint.util.template(a)(),c=V(b);this.vel.append(c)},render:function(){this.$el.empty(),this.renderMarkup(),this.rotatableNode=this.vel.findOne(".rotatable");var a=this.scalableNode=this.vel.findOne(".scalable");return a&&this.update(),this.resize(),this.rotate(),this.translate(),this},resize:function(a,b,c){var d=this.model,e=d.get("size")||{width:1,height:1},f=d.get("angle")||0,g=this.scalableNode;if(!g)return 0!==f&&this.rotate(),void this.update();var h=!1;g.node.getElementsByTagName("path").length>0&&(h=!0);var i=g.getBBox({recursive:h}),j=e.width/(i.width||1),k=e.height/(i.height||1);g.attr("transform","scale("+j+","+k+")");var l=this.rotatableNode,m=l&&l.attr("transform");if(m&&"null"!==m){l.attr("transform",m+" rotate("+-f+","+e.width/2+","+e.height/2+")");var n=g.getBBox({target:this.paper.viewport});d.set("position",{x:n.x,y:n.y},c),this.rotate()}this.update()},translate:function(a,b,c){var d=this.model.get("position")||{x:0,y:0};this.vel.attr("transform","translate("+d.x+","+d.y+")")},rotate:function(){var a=this.rotatableNode;if(a){var b=this.model.get("angle")||0,c=this.model.get("size")||{width:1,height:1},d=c.width/2,e=c.height/2;0!==b?a.attr("transform","rotate("+b+","+d+","+e+")"):a.removeAttr("transform")}},getBBox:function(a){if(a&&a.useModelGeometry){var b=this.model.getBBox().bbox(this.model.get("angle"));return this.paper.localToPaperRect(b)}return joint.dia.CellView.prototype.getBBox.apply(this,arguments)},prepareEmbedding:function(a){a=a||{};var b=a.model||this.model,c=a.paper||this.paper,d=c.model;b.startBatch("to-front",a),b.toFront({deep:!0,ui:!0});var e=d.get("cells").max("z").get("z"),f=d.getConnectedLinks(b,{deep:!0});joint.util.invoke(f,"set","z",e+1,{ui:!0}),b.stopBatch("to-front");var g=b.get("parent");g&&d.getCell(g).unembed(b,{ui:!0})},processEmbedding:function(a){a=a||{};var b=a.model||this.model,c=a.paper||this.paper,d=c.options,e=c.model.findModelsUnderElement(b,{searchBy:d.findParentBy});d.frontParentOnly&&(e=e.slice(-1));for(var f=null,g=this._candidateEmbedView,h=e.length-1;h>=0;h--){var i=e[h];if(g&&g.model.id==i.id){f=g;break}var j=i.findView(c);if(d.validateEmbedding.call(c,this,j)){f=j;break}}f&&f!=g&&(this.clearEmbedding(),this._candidateEmbedView=f.highlight(null,{embedding:!0})),!f&&g&&this.clearEmbedding()},clearEmbedding:function(){var a=this._candidateEmbedView;a&&(a.unhighlight(null,{embedding:!0}),this._candidateEmbedView=null)},finalizeEmbedding:function(a){a=a||{};var b=this._candidateEmbedView,c=a.model||this.model,d=a.paper||this.paper;b&&(b.model.embed(c,{ui:!0}),b.unhighlight(null,{embedding:!0}),delete this._candidateEmbedView),joint.util.invoke(d.model.getConnectedLinks(c,{deep:!0}),"reparent",{ui:!0})},pointerdown:function(a,b,c){var d=this.paper;if(a.target.getAttribute("magnet")&&this.can("addLinkFromMagnet")&&d.options.validateMagnet.call(d,this,a.target)){this.model.startBatch("add-link");var e=d.getDefaultLink(this,a.target);e.set({source:{id:this.model.id,selector:this.getSelector(a.target),port:a.target.getAttribute("port")},target:{x:b,y:c}}),d.model.addCell(e);var f=this._linkView=d.findViewByModel(e);f.pointerdown(a,b,c),f.startArrowheadMove("target",{whenNotAllowed:"remove"})}else this._dx=b,this._dy=c,this.restrictedArea=d.getRestrictedArea(this),joint.dia.CellView.prototype.pointerdown.apply(this,arguments),this.notify("element:pointerdown",a,b,c)},pointermove:function(a,b,c){if(this._linkView)this._linkView.pointermove(a,b,c);else{var d=this.paper.options.gridSize;if(this.can("elementMove")){var e=this.model.get("position"),f=g.snapToGrid(e.x,d)-e.x+g.snapToGrid(b-this._dx,d),h=g.snapToGrid(e.y,d)-e.y+g.snapToGrid(c-this._dy,d);this.model.translate(f,h,{restrictedArea:this.restrictedArea,ui:!0}),this.paper.options.embeddingMode&&(this._inProcessOfEmbedding||(this.prepareEmbedding(),this._inProcessOfEmbedding=!0),this.processEmbedding())}this._dx=g.snapToGrid(b,d),this._dy=g.snapToGrid(c,d),joint.dia.CellView.prototype.pointermove.apply(this,arguments),this.notify("element:pointermove",a,b,c)}},pointerup:function(a,b,c){this._linkView?(this._linkView.pointerup(a,b,c),this._linkView=null,this.model.stopBatch("add-link")):(this._inProcessOfEmbedding&&(this.finalizeEmbedding(),this._inProcessOfEmbedding=!1),this.notify("element:pointerup",a,b,c),joint.dia.CellView.prototype.pointerup.apply(this,arguments))},mouseenter:function(a){joint.dia.CellView.prototype.mouseenter.apply(this,arguments),this.notify("element:mouseenter",a)},mouseleave:function(a){joint.dia.CellView.prototype.mouseleave.apply(this,arguments),this.notify("element:mouseleave",a)}}),joint.dia.Link=joint.dia.Cell.extend({markup:['','','','','','','',''].join(""),labelMarkup:['',"","",""].join(""),toolMarkup:['','','','',"Remove link.","",'','','',"Link options.","",""].join(""),vertexMarkup:['','','','',"Remove vertex.","",""].join(""),arrowheadMarkup:['','',""].join(""),defaults:{type:"link",source:{},target:{}},isLink:function(){return!0},disconnect:function(){return this.set({source:g.point(0,0),target:g.point(0,0)})},label:function(a,b,c){return a=a||0,arguments.length<=1?this.prop(["labels",a]):this.prop(["labels",a],b,c)},translate:function(a,b,c){return c=c||{},c.translateBy=c.translateBy||this.id,c.tx=a,c.ty=b,this.applyToPoints(function(c){return{x:(c.x||0)+a,y:(c.y||0)+b}},c)},scale:function(a,b,c,d){return this.applyToPoints(function(d){return g.point(d).scale(a,b,c).toJSON()},d)},applyToPoints:function(a,b){if(!joint.util.isFunction(a))throw new TypeError("dia.Link: applyToPoints expects its first parameter to be a function.");var c={},d=this.get("source");d.id||(c.source=a(d));var e=this.get("target");e.id||(c.target=a(e));var f=this.get("vertices");return f&&f.length>0&&(c.vertices=f.map(a)),this.set(c,b)},reparent:function(a){var b;if(this.graph){var c=this.graph.getCell(this.get("source").id),d=this.graph.getCell(this.get("target").id),e=this.graph.getCell(this.get("parent"));c&&d&&(b=this.graph.getCommonAncestor(c,d)),!e||b&&b.id===e.id||e.unembed(this,a),b&&b.embed(this,a)}return b},hasLoop:function(a){a=a||{};var b=this.get("source").id,c=this.get("target").id;if(!b||!c)return!1;var d=b===c;if(!d&&a.deep&&this.graph){var e=this.graph.getCell(b),f=this.graph.getCell(c);d=e.isEmbeddedIn(f)||f.isEmbeddedIn(e)}return d},getSourceElement:function(){var a=this.get("source");return a&&a.id&&this.graph&&this.graph.getCell(a.id)||null},getTargetElement:function(){var a=this.get("target"); -return a&&a.id&&this.graph&&this.graph.getCell(a.id)||null},getRelationshipAncestor:function(){var a;if(this.graph){var b=[this,this.getSourceElement(),this.getTargetElement()].filter(function(a){return!!a});a=this.graph.getCommonAncestor.apply(this.graph,b)}return a||null},isRelationshipEmbeddedIn:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a.id,c=this.getRelationshipAncestor();return!!c&&(c.id===b||c.isEmbeddedIn(b))}},{endsEqual:function(a,b){var c=a.port===b.port||!a.port&&!b.port;return a.id===b.id&&c}}),joint.dia.LinkView=joint.dia.CellView.extend({className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("link"),a.join(" ")},options:{shortLinkLength:100,doubleLinkTools:!1,longLinkLength:160,linkToolsOffset:40,doubleLinkToolsOffset:60,sampleInterval:50},_z:null,initialize:function(a){joint.dia.CellView.prototype.initialize.apply(this,arguments),"function"!=typeof this.constructor.prototype.watchSource&&(this.constructor.prototype.watchSource=this.createWatcher("source"),this.constructor.prototype.watchTarget=this.createWatcher("target")),this._labelCache={},this._markerCache={},this.startListening()},startListening:function(){var a=this.model;this.listenTo(a,"change:markup",this.render),this.listenTo(a,"change:smooth change:manhattan change:router change:connector",this.update),this.listenTo(a,"change:toolMarkup",this.onToolsChange),this.listenTo(a,"change:labels change:labelMarkup",this.onLabelsChange),this.listenTo(a,"change:vertices change:vertexMarkup",this.onVerticesChange),this.listenTo(a,"change:source",this.onSourceChange),this.listenTo(a,"change:target",this.onTargetChange)},onSourceChange:function(a,b,c){this.watchSource(a,b),c.translateBy&&this.model.get("target").id||(c.updateConnectionOnly=!0,this.update(this.model,null,c))},onTargetChange:function(a,b,c){this.watchTarget(a,b),c.translateBy||(c.updateConnectionOnly=!0,this.update(this.model,null,c))},onVerticesChange:function(a,b,c){this.renderVertexMarkers(),c.translateBy&&c.translateBy!==this.model.id||(c.updateConnectionOnly=!0,this.update(a,null,c))},onToolsChange:function(){this.renderTools().updateToolsPosition()},onLabelsChange:function(a,b,c){var d=!0,e=this.model.previous("labels");if(e&&"propertyPathArray"in c&&"propertyValue"in c){var f=c.propertyPathArray||[],g=f.length;if(g>1){var h=!!e[f[1]];h&&(2===g?d="markup"in Object(c.propertyValue):"markup"!==f[2]&&(d=!1))}}d?this.renderLabels():this.updateLabels(),this.updateLabelPositions()},render:function(){this.$el.empty();var a=this.model,b=a.get("markup")||a.markup,c=V(b);if(Array.isArray(c)||(c=[c]),this._V={},c.forEach(function(a){var b=a.attr("class");b&&(b=joint.util.removeClassNamePrefix(b),this._V[$.camelCase(b)]=a)},this),!this._V.connection)throw new Error("link: no connection path in the markup");return this.renderTools(),this.renderVertexMarkers(),this.renderArrowheadMarkers(),this.vel.append(c),this.renderLabels(),this.watchSource(a,a.get("source")).watchTarget(a,a.get("target")).update(),this},renderLabels:function(){var a=this._V.labels;if(!a)return this;a.empty();var b=this.model,c=b.get("labels")||[],d=this._labelCache={},e=c.length;if(0===e)return this;for(var f=joint.util.template(b.get("labelMarkup")||b.labelMarkup),g=V(f()),h=0;hd?d:l,l=l<0?d+l:l,l=l>1?l:d*l):l=d/2,h=c.getPointAtLength(l),joint.util.isObject(m))h=g.point(h).offset(m);else if(Number.isFinite(m)){b||(b=this._samples||this._V.connection.sample(this.options.sampleInterval));for(var n,o,p,q=1/0,r=0,s=b.length;r=this.options.longLinkLength){var e=this.options.doubleLinkToolsOffset||b;d=this.getPointAtLength(c-e),this._tool2Cache.attr("transform","translate("+d.x+", "+d.y+") "+a),this._tool2Cache.attr("visibility","visible")}else this.options.doubleLinkTools&&this._tool2Cache.attr("visibility","hidden")}return this},updateArrowheadMarkers:function(){if(!this._V.markerArrowheads)return this;if("none"===$.css(this._V.markerArrowheads.node,"display"))return this;var a=this.getConnectionLength()g);)c=d.slice();return h===-1&&(h=0,c.splice(h,0,a)),this.model.set("vertices",c,{ui:!0}),h},sendToken:function(a,b,c){function d(a,b){return function(){a.remove(),"function"==typeof b&&b()}}var e,f;joint.util.isObject(b)?(e=b.duration,f="reverse"===b.direction):(e=b,f=!1),e=e||1e3;var g={dur:e+"ms",repeatCount:1,calcMode:"linear",fill:"freeze"};f&&(g.keyPoints="1;0",g.keyTimes="0;1");var h=V(a),i=this._V.connection;h.appendTo(this.paper.viewport).animateAlongPath(g,i),setTimeout(d(h,c),e)},findRoute:function(a){var b=joint.routers,c=this.model.get("router"),d=this.paper.options.defaultRouter;if(!c)if(this.model.get("manhattan"))c={name:"orthogonal"};else{if(!d)return a;c=d}var e=c.args||{},f=joint.util.isFunction(c)?c:b[c.name];if(!joint.util.isFunction(f))throw new Error('unknown router: "'+c.name+'"');var g=f.call(this,a||[],e,this);return g},getPathData:function(a){var b=joint.connectors,c=this.model.get("connector"),d=this.paper.options.defaultConnector;c||(c=this.model.get("smooth")?{name:"smooth"}:d||{});var e=joint.util.isFunction(c)?c:b[c.name],f=c.args||{};if(!joint.util.isFunction(e))throw new Error('unknown connector: "'+c.name+'"');var g=e.call(this,this._markerCache.sourcePoint,this._markerCache.targetPoint,a||this.model.get("vertices")||{},f,this);return g},getConnectionPoint:function(a,b,c){var d;if(joint.util.isEmpty(b)&&(b={x:0,y:0}),joint.util.isEmpty(c)&&(c={x:0,y:0}),b.id){var e,f=g.Rect("source"===a?this.sourceBBox:this.targetBBox);if(c.id){var h=g.Rect("source"===a?this.targetBBox:this.sourceBBox);e=h.intersectionWithLineFromCenterToPoint(f.center()),e=e||h.center()}else e=g.Point(c);var i=this.paper.options;if(i.perpendicularLinks||this.options.perpendicular){var j,k=f.origin(),l=f.corner();if(k.y<=e.y&&e.y<=l.y)switch(j=f.sideNearestToPoint(e)){case"left":d=g.Point(k.x,e.y);break;case"right":d=g.Point(l.x,e.y);break;default:d=f.center()}else if(k.x<=e.x&&e.x<=l.x)switch(j=f.sideNearestToPoint(e)){case"top":d=g.Point(e.x,k.y);break;case"bottom":d=g.Point(e.x,l.y);break;default:d=f.center()}else d=f.intersectionWithLineFromCenterToPoint(e),d=d||f.center()}else if(i.linkConnectionPoint){var m="target"===a?this.targetView:this.sourceView,n="target"===a?this.targetMagnet:this.sourceMagnet;d=i.linkConnectionPoint(this,m,n,e,a)}else d=f.intersectionWithLineFromCenterToPoint(e),d=d||f.center()}else d=g.Point(b);return d},getConnectionLength:function(){return this._V.connection.node.getTotalLength()},getPointAtLength:function(a){return this._V.connection.node.getPointAtLength(a)},_beforeArrowheadMove:function(){this._z=this.model.get("z"),this.model.toFront(),this.el.style.pointerEvents="none",this.paper.options.markAvailable&&this._markAvailableMagnets()},_afterArrowheadMove:function(){null!==this._z&&(this.model.set("z",this._z,{ui:!0}),this._z=null),this.el.style.pointerEvents="visiblePainted",this.paper.options.markAvailable&&this._unmarkAvailableMagnets()},_createValidateConnectionArgs:function(a){function b(a,b){return c[f]=a,c[f+1]=a.el===b?void 0:b,c}var c=[];c[4]=a,c[5]=this;var d,e=0,f=0;"source"===a?(e=2,d="target"):(f=2,d="source");var g=this.model.get(d);return g.id&&(c[e]=this.paper.findViewByModel(g.id),c[e+1]=g.selector&&c[e].el.querySelector(g.selector)),b},_markAvailableMagnets:function(){function a(a,b){var c=a.paper,d=c.options.validateConnection;return d.apply(c,this._validateConnectionArgs(a,b))}var b=this.paper,c=b.model.getElements();this._marked={};for(var d=0,e=c.length;d0){for(var i=0,j=h.length;i").addClass(joint.util.addClassNamePrefix("paper-background")),this.options.background&&this.drawBackground(this.options.background),this.$grid=$("
").addClass(joint.util.addClassNamePrefix("paper-grid")),this.options.drawGrid&&this.drawGrid(),this.$el.append(this.$background,this.$grid,this.svg),this},update:function(){return this.options.drawGrid&&this.drawGrid(),this._background&&this.updateBackgroundImage(this._background),this},_viewportMatrix:null,_viewportTransformString:null,matrix:function(a){var b=this.viewport;if(void 0===a){var c=b.getAttribute("transform");return(this._viewportTransformString||null)===c?a=this._viewportMatrix:(a=b.getCTM(),this._viewportMatrix=a,this._viewportTransformString=c),V.createSVGMatrix(a)}return a=V.createSVGMatrix(a),V(b).transform(a,{absolute:!0}),this._viewportMatrix=a,this._viewportTransformString=b.getAttribute("transform"),this},clientMatrix:function(){return V.createSVGMatrix(this.viewport.getScreenCTM())},_onSort:function(){this.model.hasActiveBatch("add")||this.sortViews()},_onBatchStop:function(a){var b=a&&a.batchName;"add"!==b||this.model.hasActiveBatch("add")||this.sortViews()},onRemove:function(){this.removeViews(),this.unbindDocumentEvents()},setDimensions:function(a,b){a=this.options.width=a||this.options.width,b=this.options.height=b||this.options.height,this.$el.css({width:Math.round(a),height:Math.round(b)}),this.trigger("resize",a,b)},setOrigin:function(a,b){return this.translate(a||0,b||0,{absolute:!0})},fitToContent:function(a,b,c,d){joint.util.isObject(a)?(d=a,a=d.gridWidth||1,b=d.gridHeight||1,c=d.padding||0):(d=d||{},a=a||1,b=b||1,c=c||0),c=joint.util.normalizeSides(c);var e=V(this.viewport).getBBox(),f=this.scale(),g=this.translate();e.x*=f.sx,e.y*=f.sy,e.width*=f.sx,e.height*=f.sy;var h=Math.max(Math.ceil((e.width+e.x)/a),1)*a,i=Math.max(Math.ceil((e.height+e.y)/b),1)*b,j=0,k=0;("negative"==d.allowNewOrigin&&e.x<0||"positive"==d.allowNewOrigin&&e.x>=0||"any"==d.allowNewOrigin)&&(j=Math.ceil(-e.x/a)*a,j+=c.left,h+=j),("negative"==d.allowNewOrigin&&e.y<0||"positive"==d.allowNewOrigin&&e.y>=0||"any"==d.allowNewOrigin)&&(k=Math.ceil(-e.y/b)*b,k+=c.top,i+=k),h+=c.right,i+=c.bottom,h=Math.max(h,d.minWidth||0),i=Math.max(i,d.minHeight||0),h=Math.min(h,d.maxWidth||Number.MAX_VALUE),i=Math.min(i,d.maxHeight||Number.MAX_VALUE);var l=h!=this.options.width||i!=this.options.height,m=j!=g.tx||k!=g.ty;m&&this.translate(j,k),l&&this.setDimensions(h,i)},scaleContentToFit:function(a){var b=this.getContentBBox();if(b.width&&b.height){a=a||{},joint.util.defaults(a,{padding:0,preserveAspectRatio:!0,scaleGrid:null,minScale:0,maxScale:Number.MAX_VALUE});var c,d=a.padding,e=a.minScaleX||a.minScale,f=a.maxScaleX||a.maxScale,h=a.minScaleY||a.minScale,i=a.maxScaleY||a.maxScale;if(a.fittingBBox)c=a.fittingBBox;else{var j=this.translate();c={x:j.tx,y:j.ty,width:this.options.width,height:this.options.height}}c=g.rect(c).moveAndExpand({x:d,y:d,width:-2*d,height:-2*d});var k=this.scale(),l=c.width/b.width*k.sx,m=c.height/b.height*k.sy;if(a.preserveAspectRatio&&(l=m=Math.min(l,m)),a.scaleGrid){var n=a.scaleGrid;l=n*Math.floor(l/n),m=n*Math.floor(m/n)}l=Math.min(f,Math.max(e,l)),m=Math.min(i,Math.max(h,m)),this.scale(l,m);var o=this.getContentBBox(),p=c.x-o.x,q=c.y-o.y;this.translate(p,q)}},getContentBBox:function(){var a=this.viewport.getBoundingClientRect(),b=this.clientMatrix(),c=this.translate();return g.rect({x:a.left-b.e+c.tx,y:a.top-b.f+c.ty,width:a.width,height:a.height})},getArea:function(){return this.paperToLocalRect({x:0,y:0,width:this.options.width,height:this.options.height})},getRestrictedArea:function(){var a;return a=joint.util.isFunction(this.options.restrictTranslate)?this.options.restrictTranslate.apply(this,arguments):this.options.restrictTranslate===!0?this.getArea():this.options.restrictTranslate||null},createViewForModel:function(a){var b,c,d=this.options.cellViewNamespace,e=a.get("type")+"View",f=joint.util.getByPath(d,e,".");a.isLink()?(b=this.options.linkView,c=joint.dia.LinkView):(b=this.options.elementView,c=joint.dia.ElementView);var g=b.prototype instanceof Backbone.View?f||b:b.call(this,a)||f||c;return new g({model:a,interactive:this.options.interactive})},onCellAdded:function(a,b,c){if(this.options.async&&c.async!==!1&&joint.util.isNumber(c.position)){if(this._asyncCells=this._asyncCells||[],this._asyncCells.push(a),0==c.position){if(this._frameId)throw new Error("another asynchronous rendering in progress");this.asyncRenderViews(this._asyncCells,c),delete this._asyncCells}}else this.renderView(a)},removeView:function(a){var b=this._views[a.id];return b&&(b.remove(),delete this._views[a.id]),b},renderView:function(a){var b=this._views[a.id]=this.createViewForModel(a);return V(this.viewport).append(b.el),b.paper=this,b.render(),$(b.el).find("image").on("dragstart",function(){return!1}),b},beforeRenderViews:function(a){return a.sort(function(a){return a.isLink()?1:-1}),a},afterRenderViews:function(){this.sortViews()},resetViews:function(a,b){this.removeViews();var c=a.models.slice();if(c=this.beforeRenderViews(c,b)||c,this.cancelRenderViews(),this.options.async)this.asyncRenderViews(c,b);else{for(var d=0,e=c.length;d(e.get("z")||0)?1:-1})},scale:function(a,b,c,d){if(void 0===a)return V.matrixToScale(this.matrix());void 0===b&&(b=a),void 0===c&&(c=0,d=0);var e=this.translate();if(c||d||e.tx||e.ty){var f=e.tx-c*(a-1),g=e.ty-d*(b-1);this.translate(f,g)}var h=this.matrix();return h.a=a||0,h.d=b||0,this.matrix(h),this.trigger("scale",a,b,c,d),this},rotate:function(a,b,c){if(void 0===a)return V.matrixToRotate(this.matrix());if(void 0===b){var d=this.viewport.getBBox();b=d.width/2,c=d.height/2}var e=this.matrix().translate(b,c).rotate(a).translate(-b,-c);return this.matrix(e),this},translate:function(a,b){if(void 0===a)return V.matrixToTranslate(this.matrix());var c=this.matrix();c.e=a||0,c.f=b||0,this.matrix(c);var d=this.translate(),e=this.options.origin;return e.x=d.tx,e.y=d.ty,this.trigger("translate",d.tx,d.ty),this.options.drawGrid&&this.drawGrid(),this},findView:function(a){for(var b=joint.util.isString(a)?this.viewport.querySelector(a):a instanceof $?a[0]:a;b&&b!==this.el&&b!==document;){var c=b.getAttribute("model-id");if(c)return this._views[c];b=b.parentNode}},findViewByModel:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a&&a.id;return this._views[b]},findViewsFromPoint:function(a){a=g.point(a);var b=this.model.getElements().map(this.findViewByModel,this);return b.filter(function(b){return b&&b.vel.getBBox({target:this.viewport}).containsPoint(a)},this)},findViewsInArea:function(a,b){b=joint.util.defaults(b||{},{strict:!1}),a=g.rect(a);var c=this.model.getElements().map(this.findViewByModel,this),d=b.strict?"containsRect":"intersect";return c.filter(function(b){return b&&a[d](b.vel.getBBox({target:this.viewport}))},this)},getModelById:function(a){return this.model.getCell(a)},snapToGrid:function(a,b){return this.clientToLocalPoint(a,b).snapToGrid(this.options.gridSize)},localToPaperPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix());return g.Point(d)},localToPaperRect:function(a,b,c,d){var e=g.Rect(a,b),f=V.transformRect(e,this.matrix()); -return g.Rect(f)},paperToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix().inverse());return g.Point(d)},paperToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.matrix().inverse());return g.Rect(f)},localToClientPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix());return g.Point(d)},localToClientRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix());return g.Rect(f)},clientToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix().inverse());return g.Point(d)},clientToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix().inverse());return g.Rect(f)},localToPagePoint:function(a,b){return this.localToPaperPoint(a,b).offset(this.pageOffset())},localToPageRect:function(a,b,c,d){return this.localToPaperRect(a,b,c,d).moveAndExpand(this.pageOffset())},pageToLocalPoint:function(a,b){var c=g.Point(a,b),d=c.difference(this.pageOffset());return this.paperToLocalPoint(d)},pageToLocalRect:function(a,b,c,d){var e=this.pageOffset(),f=g.Rect(a,b,c,d);return f.x-=e.x,f.y-=e.y,this.paperToLocalRect(f)},clientOffset:function(){var a=this.svg.getBoundingClientRect();return g.Point(a.left,a.top)},pageOffset:function(){return this.clientOffset().offset(window.scrollX,window.scrollY)},linkAllowed:function(a){var b;if(a instanceof joint.dia.Link)b=a;else{if(!(a instanceof joint.dia.LinkView))throw new Error("Must provide link model or view.");b=a.model}if(!this.options.multiLinks){var c=b.get("source"),d=b.get("target");if(c.id&&d.id){var e=b.getSourceElement();if(e){var f=this.model.getConnectedLinks(e,{outbound:!0,inbound:!1}),g=f.filter(function(a){var b=a.get("source"),e=a.get("target");return b&&b.id===c.id&&(!b.port||b.port===c.port)&&e&&e.id===d.id&&(!e.port||e.port===d.port)}).length;if(g>1)return!1}}}return!!(this.options.linkPinning||joint.util.has(b.get("source"),"id")&&joint.util.has(b.get("target"),"id"))},getDefaultLink:function(a,b){return joint.util.isFunction(this.options.defaultLink)?this.options.defaultLink.call(this,a,b):this.options.defaultLink.clone()},resolveHighlighter:function(a){a=a||{};var b=a.highlighter,c=this.options;if(void 0===b){var d=["embedding","connecting","magnetAvailability","elementAvailability"].find(function(b){return!!a[b]});b=d&&c.highlighting[d]||c.highlighting.default}if(!b)return!1;joint.util.isString(b)&&(b={name:b});var e=b.name,f=c.highlighterNamespace[e];if(!f)throw new Error('Unknown highlighter ("'+e+'")');if("function"!=typeof f.highlight)throw new Error('Highlighter ("'+e+'") is missing required highlight() method');if("function"!=typeof f.unhighlight)throw new Error('Highlighter ("'+e+'") is missing required unhighlight() method');return{highlighter:f,options:b.options||{},name:e}},onCellHighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){b.id||(b.id=V.uniqueId());var d=c.name+b.id+JSON.stringify(c.options);if(!this._highlights[d]){var e=c.highlighter;e.highlight(a,b,joint.util.assign({},c.options)),this._highlights[d]={cellView:a,magnetEl:b,opt:c.options,highlighter:e}}}},onCellUnhighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){var d=c.name+b.id+JSON.stringify(c.options),e=this._highlights[d];e&&(e.highlighter.unhighlight(e.cellView,e.magnetEl,e.opt),this._highlights[d]=null)}},mousedblclick:function(a){a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerdblclick(a,c.x,c.y):this.trigger("blank:pointerdblclick",a,c.x,c.y)}},mouseclick:function(a){if(this._mousemoved<=this.options.clickThreshold){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(this.guard(a,b))return;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerclick(a,c.x,c.y):this.trigger("blank:pointerclick",a,c.x,c.y)}},guard:function(a,b){return!(!this.options.guard||!this.options.guard(a,b))||(a.data&&void 0!==a.data.guarded?a.data.guarded:!(b&&b.model&&b.model instanceof joint.dia.Cell)&&(this.svg!==a.target&&this.el!==a.target&&!$.contains(this.svg,a.target)))},contextmenu:function(a){a=joint.util.normalizeEvent(a),this.options.preventContextMenu&&a.preventDefault();var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.contextmenu(a,c.x,c.y):this.trigger("blank:contextmenu",a,c.x,c.y)}},pointerdown:function(a){this.bindDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){this._mousemoved=0;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?(a.preventDefault(),this.sourceView=b,b.pointerdown(a,c.x,c.y)):(this.options.preventDefaultBlankAction&&a.preventDefault(),this.trigger("blank:pointerdown",a,c.x,c.y))}},pointermove:function(a){var b=this.sourceView;if(b){a.preventDefault();var c=++this._mousemoved;if(c>this.options.moveThreshold){a=joint.util.normalizeEvent(a);var d=this.snapToGrid({x:a.clientX,y:a.clientY});b.pointermove(a,d.x,d.y)}}},pointerup:function(a){this.unbindDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.snapToGrid({x:a.clientX,y:a.clientY});this.sourceView?(this.sourceView.pointerup(a,b.x,b.y),this.sourceView=null):this.trigger("blank:pointerup",a,b.x,b.y)},mousewheel:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=a.originalEvent,d=this.snapToGrid({x:c.clientX,y:c.clientY}),e=Math.max(-1,Math.min(1,c.wheelDelta||-c.detail));b?b.mousewheel(a,d.x,d.y,e):this.trigger("blank:mousewheel",a,d.x,d.y,e)}},cellMouseover:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(b){if(this.guard(a,b))return;b.mouseover(a)}},cellMouseout:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(b){if(this.guard(a,b))return;b.mouseout(a)}},cellMouseenter:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);b&&!this.guard(a,b)&&b.mouseenter(a)},cellMouseleave:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);b&&!this.guard(a,b)&&b.mouseleave(a)},cellEvent:function(a){a=joint.util.normalizeEvent(a);var b=a.currentTarget,c=b.getAttribute("event");if(c){var d=this.findView(b);if(d&&!this.guard(a,d)){var e=this.snapToGrid({x:a.clientX,y:a.clientY});d.event(a,c,e.x,e.y)}}},setGridSize:function(a){return this.options.gridSize=a,this.options.drawGrid&&this.drawGrid(),this},clearGrid:function(){return this.$grid&&this.$grid.css("backgroundImage","none"),this},_getGriRefs:function(){return this._gridCache||(this._gridCache={root:V("svg",{width:"100%",height:"100%"},V("defs")),patterns:{},add:function(a,b){V(this.root.node.childNodes[0]).append(b),this.patterns[a]=b,this.root.append(V("rect",{width:"100%",height:"100%",fill:"url(#"+a+")"}))},get:function(a){return this.patterns[a]},exist:function(a){return void 0!==this.patterns[a]}}),this._gridCache},setGrid:function(a){this.clearGrid(),this._gridCache=null,this._gridSettings=[];var b=Array.isArray(a)?a:[a||{}];return b.forEach(function(a){this._gridSettings.push.apply(this._gridSettings,this._resolveDrawGridOption(a))},this),this},_resolveDrawGridOption:function(a){var b=this.constructor.gridPatterns;if(joint.util.isString(a)&&Array.isArray(b[a]))return b[a].map(function(a){return joint.util.assign({},a)});var c=a||{args:[{}]},d=Array.isArray(c),e=c.name;if(d||e||c.markup||(e="dot"),e&&Array.isArray(b[e])){var f=b[e].map(function(a){return joint.util.assign({},a)}),g=Array.isArray(c.args)?c.args:[c.args||{}];joint.util.defaults(g[0],joint.util.omit(a,"args"));for(var h=0;h'),f=joint.util.toArray(d).map(function(a){return e({offset:a.offset,color:a.color,opacity:Number.isFinite(a.opacity)?a.opacity:1})}),g=["<"+c+">",f.join(""),""].join(""),h=joint.util.assign({id:b},a.attrs);V(g,h).appendTo(this.defs)}return b},defineMarker:function(a){if(!joint.util.isObject(a))throw new TypeError("dia.Paper: defineMarker() requires 1. argument to be an object.");var b=a.id;if(b||(b=this.svg.id+joint.util.hashCode(JSON.stringify(a))),!this.isDefined(b)){var c=joint.util.omit(a,"type","userSpaceOnUse"),d=V("marker",{id:b,orient:"auto",overflow:"visible",markerUnits:a.markerUnits||"userSpaceOnUse"},[V(a.type||"path",c)]);d.appendTo(this.defs)}return b}},{backgroundPatterns:{flipXy:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,-1,b.width,b.height),e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,1,b.width,0),e.drawImage(a,0,0,c,d),e.setTransform(1,0,0,-1,0,b.height),e.drawImage(a,0,0,c,d),b},flipX:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(2*c,0),e.scale(-1,1),e.drawImage(a,0,0,c,d),b},flipY:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(0,2*d),e.scale(1,-1),e.drawImage(a,0,0,c,d),b},watermark:function(a,b){b=b||{};var c=a.width,d=a.height,e=document.createElement("canvas");e.width=3*c,e.height=3*d;for(var f=e.getContext("2d"),h=joint.util.isNumber(b.watermarkAngle)?-b.watermarkAngle:-20,i=g.toRad(h),j=e.width/4,k=e.height/4,l=0;l<4;l++)for(var m=0;m<4;m++)(l+m)%2>0&&(f.setTransform(1,0,0,1,(2*l-1)*j,(2*m-1)*k),f.rotate(i),f.drawImage(a,-c/2,-d/2,c,d));return e}},gridPatterns:{dot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){V(a).attr({width:b.thickness*b.sx,height:b.thickness*b.sy,fill:b.color})}}],fixedDot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){var c=b.sx<=1?b.thickness*b.sx:b.thickness;V(a).attr({width:c,height:c,fill:b.color})}}],mesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}],doubleMesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}},{color:"#000000",thickness:3,scaleFactor:4,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}]}}),function(a,b,c){var d=function(b){var d=c.cloneDeep(b)||{};this.ports=[],this.groups={},this.portLayoutNamespace=a.layout.Port,this.portLabelLayoutNamespace=a.layout.PortLabel,this._init(d)};d.prototype={getPorts:function(){return this.ports},getGroup:function(a){return this.groups[a]||{}},getPortsByGroup:function(a){return this.ports.filter(function(b){return b.group===a})},getGroupPortsMetrics:function(a,b){var d=this.getGroup(a),e=this.getPortsByGroup(a),f=d.position||{},h=f.name,i=this.portLayoutNamespace;i[h]||(h="left");var j=f.args||{},k=e.map(function(a){return a&&a.position&&a.position.args}),l=i[h](k,b,j),m={ports:e,result:[]};return c.toArray(l).reduce(function(a,c,d){var e=a.ports[d];return a.result.push({portId:e.id,portTransformation:c,labelTransformation:this._getPortLabelLayout(e,g.Point(c),b),portAttrs:e.attrs,portSize:e.size,labelSize:e.label.size}),a}.bind(this),m),m.result},_getPortLabelLayout:function(a,b,c){var d=this.portLabelLayoutNamespace,e=a.label.position.name||"left";return d[e]?d[e](b,c,a.label.position.args):null},_init:function(a){if(c.isObject(a.groups))for(var b=Object.keys(a.groups),d=0,e=b.length;d0},hasPort:function(a){return this.getPortIndex(a)!==-1},getPorts:function(){return c.cloneDeep(this.prop("ports/items"))||[]},getPort:function(a){return c.cloneDeep(c.toArray(this.prop("ports/items")).find(function(b){return b.id&&b.id===a}))},getPortsPositions:function(a){var b=this._portSettingsData.getGroupPortsMetrics(a,g.Rect(this.size()));return b.reduce(function(a,b){var c=b.portTransformation;return a[b.portId]={x:c.x,y:c.y,angle:c.angle},a},{})},getPortIndex:function(a){var b=c.isObject(a)?a.id:a;return this._isValidPortId(b)?c.toArray(this.prop("ports/items")).findIndex(function(a){return a.id===b}):-1},addPort:function(a,b){if(!c.isObject(a)||Array.isArray(a))throw new Error("Element: addPort requires an object.");var d=c.assign([],this.prop("ports/items"));return d.push(a),this.prop("ports/items",d,b),this},portProp:function(a,b,d,e){var f=this.getPortIndex(a);if(f===-1)throw new Error("Element: unable to find port with id "+a);var g=Array.prototype.slice.call(arguments,1);return Array.isArray(b)?g[0]=["ports","items",f].concat(b):c.isString(b)?g[0]=["ports/items/",f,"/",b].join(""):(g=["ports/items/"+f],c.isPlainObject(b)&&(g.push(b),g.push(d))),this.prop.apply(this,g)},_validatePorts:function(){var b=this.get("ports")||{},d=[];b=b||{};var e=c.toArray(b.items);return e.forEach(function(a){"object"!=typeof a&&d.push("Element: invalid port ",a),this._isValidPortId(a.id)||(a.id=c.uuid())},this),a.util.uniq(e,"id").length!==e.length&&d.push("Element: found id duplicities in ports."),d},_isValidPortId:function(a){return null!==a&&void 0!==a&&!c.isObject(a)},addPorts:function(a,b){return a.length&&this.prop("ports/items",c.assign([],this.prop("ports/items")).concat(a),b),this},removePort:function(a,b){var d=b||{},e=c.assign([],this.prop("ports/items")),f=this.getPortIndex(a);return f!==-1&&(e.splice(f,1),d.rewrite=!0,this.prop("ports/items",e,d)),this},_createPortData:function(){var a=this._validatePorts();if(a.length>0)throw this.set("ports",this.previous("ports")),new Error(a.join(" "));var b;this._portSettingsData&&(b=this._portSettingsData.getPorts()),this._portSettingsData=new d(this.get("ports"));var c=this._portSettingsData.getPorts();if(b){var e=c.filter(function(a){if(!b.find(function(b){return b.id===a.id}))return a}),f=b.filter(function(a){if(!c.find(function(b){return b.id===a.id}))return a});f.length>0&&this.trigger("ports:remove",this,f),e.length>0&&this.trigger("ports:add",this,e)}}}),c.assign(a.dia.ElementView.prototype,{portContainerMarkup:'',portMarkup:'',portLabelMarkup:'',_portElementsCache:null,_initializePorts:function(){this._portElementsCache={},this.listenTo(this.model,"change:ports",function(){this._refreshPorts()})},_refreshPorts:function(){this._removePorts(),this._portElementsCache={},this._renderPorts()},_renderPorts:function(){for(var a=[],b=this._getContainerElement(),d=0,e=b.node.childNodes.length;d1)throw new Error("ElementView: Invalid port markup - multiple roots.");b.attr({port:a.id,"port-group":a.group});var d=V(this.portContainerMarkup).append(b).append(c);return this._portElementsCache[a.id]={portElement:d,portLabelElement:c},d},_updatePortGroup:function(a){for(var b=g.Rect(this.model.size()),c=this.model._portSettingsData.getGroupPortsMetrics(a,b),d=0,e=c.length;d'}),joint.shapes.basic.TextView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"change:attrs",this.resize)}}),joint.shapes.basic.Generic.define("basic.Text",{attrs:{text:{"font-size":18,fill:"#000000"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Circle",{size:{width:60,height:60},attrs:{circle:{fill:"#ffffff",stroke:"#000000",r:30,cx:30,cy:30},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Ellipse",{size:{width:60,height:40},attrs:{ellipse:{fill:"#ffffff",stroke:"#000000",rx:30,ry:20,cx:30,cy:20},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polygon",{size:{width:60,height:40},attrs:{polygon:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polyline",{size:{width:60,height:40},attrs:{polyline:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Image",{attrs:{text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Path",{size:{width:60,height:60},attrs:{path:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle",ref:"path","ref-x":.5,"ref-dy":10,fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Path.define("basic.Rhombus",{attrs:{path:{d:"M 30 0 L 60 30 30 60 0 30 z"},text:{"ref-y":.5,"ref-dy":null,"y-alignment":"middle"}}}),joint.shapes.basic.PortsModelInterface={initialize:function(){this.updatePortsAttrs(),this.on("change:inPorts change:outPorts",this.updatePortsAttrs,this),this.constructor.__super__.constructor.__super__.initialize.apply(this,arguments)},updatePortsAttrs:function(a){if(this._portSelectors){var b=joint.util.omit(this.get("attrs"),this._portSelectors);this.set("attrs",b,{silent:!0})}this._portSelectors=[];var c={};joint.util.toArray(this.get("inPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".inPorts","in");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),joint.util.toArray("outPorts").forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".outPorts","out");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),this.attr(c,{silent:!0}),this.processPorts(),this.trigger("process:ports")},getPortSelector:function(a){var b=".inPorts",c=this.get("inPorts").indexOf(a);if(c<0&&(b=".outPorts",c=this.get("outPorts").indexOf(a),c<0))throw new Error("getPortSelector(): Port doesn't exist.");return b+">g:nth-child("+(c+1)+")>.port-body"}},joint.shapes.basic.PortsViewInterface={initialize:function(){this.listenTo(this.model,"process:ports",this.update),joint.dia.ElementView.prototype.initialize.apply(this,arguments)},update:function(){this.renderPorts(),joint.dia.ElementView.prototype.update.apply(this,arguments)},renderPorts:function(){var a=this.$(".inPorts").empty(),b=this.$(".outPorts").empty(),c=joint.util.template(this.model.portMarkup),d=this.model.ports||[];d.filter(function(a){return"in"===a.type}).forEach(function(b,d){a.append(V(c({id:d,port:b})).node)}),d.filter(function(a){return"out"===a.type}).forEach(function(a,d){b.append(V(c({id:d,port:a})).node)})}},joint.shapes.basic.Generic.define("basic.TextBlock",{attrs:{rect:{fill:"#ffffff",stroke:"#000000",width:80,height:100},text:{fill:"#000000","font-size":14,"font-family":"Arial, helvetica, sans-serif"},".content":{text:"","ref-x":.5,"ref-y":.5,"y-alignment":"middle","x-alignment":"middle"}},content:""},{markup:['','',joint.env.test("svgforeignobject")?'
':'',""].join(""),initialize:function(){this.listenTo(this,"change:size",this.updateSize),this.listenTo(this,"change:content",this.updateContent),this.updateSize(this,this.get("size")),this.updateContent(this,this.get("content")),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateSize:function(a,b){this.attr({".fobj":joint.util.assign({},b),div:{style:joint.util.assign({},b)}})},updateContent:function(a,b){joint.env.test("svgforeignobject")?this.attr({".content":{html:b}}):this.attr({".content":{text:b}})},setForeignObjectSize:function(){this.updateSize.apply(this,arguments)},setDivContent:function(){this.updateContent.apply(this,arguments)}}),joint.shapes.basic.TextBlockView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.noSVGForeignObjectElement=!joint.env.test("svgforeignobject"),joint.env.test("svgforeignobject")||this.listenTo(this.model,"change:content change:size",function(a){this.updateContent(a)})},update:function(a,b){var c=this.model;if(joint.env.test("svgforeignobject"))joint.dia.ElementView.prototype.update.call(this,c,b);else{var d=joint.util.omit(b||c.get("attrs"),".content");joint.dia.ElementView.prototype.update.call(this,c,d),b&&!joint.util.has(b,".content")||this.updateContent(c,b)}},updateContent:function(a,b){var c=joint.util.merge({},(b||a.get("attrs"))[".content"]);c=joint.util.omit(c,"text");var d=joint.util.breakText(a.get("content"),a.get("size"),c,{svgDocument:this.paper.svg}),e=joint.util.setByPath({},".content",c,"/");e[".content"].text=d,joint.dia.ElementView.prototype.update.call(this,a,e)}}),joint.routers.manhattan=function(a,b,c,d){"use strict";function e(a){this.map={},this.options=a,this.mapGridSize=100}function f(){this.items=[],this.hash={},this.values={},this.OPEN=1,this.CLOSE=2}function g(b){return a.point(0===b.x?0:Math.abs(b.x)/b.x,0===b.y?0:Math.abs(b.y)/b.y)}function h(b,c,d,e){for(var f,h=[],i=g(e.difference(c)),j=c;f=b[j];){var k=g(j.difference(f));k.equals(i)||(h.unshift(j),i=k),j=f}var l=g(a.point(j).difference(d));return l.equals(i)||h.unshift(j),h}function i(a,b,c){var e=c.step,f=a.center(),g=d.isObject(c.directionMap)?Object.keys(c.directionMap):[],h=d.toArray(b);return g.reduce(function(b,d){if(h.includes(d)){var g=c.directionMap[d],i=g.x*a.width/2,j=g.y*a.height/2,k=f.clone().offset(i,j);a.containsPoint(k)&&k.offset(g.x*e,g.y*e),b.push(k.snapToGrid(e))}return b},[])}function j(b,c,d){var e=360/d;return Math.floor(a.normalizeAngle(b.theta(c)+e/2)/e)*e}function k(a,b){var c=Math.abs(a-b);return c>180?360-c:c}function l(a,b){for(var c=1/0,d=0,e=b.length;d0&&n.length>0){for(var r=new f,s={},t={},u=0,v=m.length;u0;){var E=r.pop(),F=a.point(E),G=t[E],H=I,I=s[E]?j(s[E],F,B):null!=g.previousDirAngle?g.previousDirAngle:j(o,F,B);if(D.indexOf(E)>=0&&(z=k(I,j(F,p,B)),F.equals(p)||z<180))return g.previousDirAngle=I,h(s,F,o,p);for(u=0;ug.maxAllowedDirectionChange)){var J=F.clone().offset(y.offsetX,y.offsetY),K=J.toString();if(!r.isClose(K)&&e.isPointAccessible(J)){var L=G+y.cost+g.penalties[z];(!r.isOpen(K)||L90){var h=e;e=f,f=h}var i=d%90<45?e:f,j=g.line(a,i),k=90*Math.ceil(d/90),l=g.point.fromPolar(j.squaredLength(),g.toRad(k+135),i),m=g.line(b,l),n=j.intersection(m);return n?[n.round(),b]:[b]}};return function(c,d,e){return joint.routers.manhattan(c,a.assign({},b,d),e)}}(joint.util),joint.routers.normal=function(a,b,c){return a},joint.routers.oneSide=function(a,b,c){var d,e,f,g=b.side||"bottom",h=b.padding||40,i=c.sourceBBox,j=c.targetBBox,k=i.center(),l=j.center();switch(g){case"bottom":f=1,d="y",e="height";break;case"top":f=-1,d="y",e="height";break;case"left":f=-1,d="x",e="width";break;case"right":f=1,d="x",e="width";break;default:throw new Error("Router: invalid side")}return k[d]+=f*(i[e]/2+h),l[d]+=f*(j[e]/2+h),f*(k[d]-l[d])>0?l[d]=k[d]:k[d]=l[d],[k].concat(a,l)},joint.routers.orthogonal=function(a){function b(a,b){return a.x==b.x?a.y>b.y?"N":"S":a.y==b.y?a.x>b.x?"W":"E":null}function c(a,b){return a["W"==b||"E"==b?"width":"height"]}function d(a,b){return g.rect(a).moveAndExpand({x:-b,y:-b,width:2*b,height:2*b})}function e(a){return g.rect(a.x,a.y,0,0)}function f(a,b){var c=Math.min(a.x,b.x),d=Math.min(a.y,b.y),e=Math.max(a.x+a.width,b.x+b.width),f=Math.max(a.y+a.height,b.y+b.height);return g.rect(c,d,e-c,f-d)}function h(a,b,c){var d=g.point(a.x,b.y);return c.containsPoint(d)&&(d=g.point(b.x,a.y)),d}function i(a,c,d){var e=g.point(a.x,c.y),f=g.point(c.x,a.y),h=b(a,e),i=b(a,f),j=o[d],k=h==d||h!=j&&(i==j||i!=d)?e:f;return{points:[k],direction:b(k,c)}}function j(a,c,d){var e=h(a,c,d);return{points:[e],direction:b(e,c)}}function k(d,e,f,i){var j,k={},l=[g.point(d.x,e.y),g.point(e.x,d.y)],m=l.filter(function(a){return!f.containsPoint(a)}),n=m.filter(function(a){return b(a,d)!=i});if(n.length>0)j=n.filter(function(a){return b(d,a)==i}).pop(),j=j||n[0],k.points=[j],k.direction=b(j,e);else{j=a.difference(l,m)[0];var o=g.point(e).move(j,-c(f,i)/2),p=h(o,d,f);k.points=[p,o],k.direction=b(o,e)}return k}function l(a,d,e,f){var h=j(d,a,f),k=h.points[0];if(e.containsPoint(k)){h=j(a,d,e);var l=h.points[0];if(f.containsPoint(l)){var m=g.point(a).move(l,-c(e,b(a,l))/2),n=g.point(d).move(k,-c(f,b(d,k))/2),o=g.line(m,n).midpoint(),p=j(a,o,e),q=i(o,d,p.direction);h.points=[p.points[0],q.points[0]],h.direction=q.direction}}return h}function m(a,c,e,i,j){var k,l,m,n={},o=d(f(e,i),1),q=o.center().distance(c)>o.center().distance(a),r=q?c:a,s=q?a:c;return j?(k=g.point.fromPolar(o.width+o.height,p[j],r),k=o.pointNearestToPoint(k).move(k,-1)):k=o.pointNearestToPoint(r).move(r,1),l=h(k,s,o),k.round().equals(l.round())?(l=g.point.fromPolar(o.width+o.height,g.toRad(k.theta(r))+Math.PI/2,s),l=o.pointNearestToPoint(l).move(s,1).round(),m=h(k,l,o),n.points=q?[l,m,k]:[k,m,l]):n.points=q?[l,k]:[k,l],n.direction=q?b(k,c):b(l,c),n}function n(c,f,h){var n=f.elementPadding||20,o=[],p=d(h.sourceBBox,n),q=d(h.targetBBox,n);c=a.toArray(c).map(g.point),c.unshift(p.center()),c.push(q.center());for(var r,s=0,t=c.length-1;sv)||"jumpover"!==d.name)}),y=x.map(function(a){return r.findViewByModel(a)}),z=d(a,b,f),A=y.map(function(a){return null==a?[]:a===this?z:d(a.sourcePoint,a.targetPoint,a.route)},this),B=z.reduce(function(a,b){var c=x.reduce(function(a,c,d){if(c!==u){var e=g(b,A[d]);a.push.apply(a,e)}return a},[]).sort(function(a,c){return h(b.start,a)-h(b.start,c)});return c.length>0?a.push.apply(a,i(b,c,o)):a.push(b),a},[]);return j(B,o,p)}}(_,g,joint.util),function(a,b,c,d){function e(a,b,d){var e=a.toJSON();return e.angle=b||0,c.util.defaults({},d,e)}function f(a,c,d){return a.map(function(a,b,c){var d=this.pointAt((b+.5)/c.length);return(a.dx||a.dy)&&d.offset(a.dx||0,a.dy||0),e(d.round(),0,a)},b.line(c,d))}function g(a,c,d,f){var g=c.center(),h=c.width/c.height,i=c.topMiddle(),j=b.Ellipse.fromRect(c);return a.map(function(a,b,c){var k=d+f(b,c.length),l=i.clone().rotate(g,-k).scale(h,1,g),m=a.compensateRotation?-j.tangentTheta(l):0;return(a.dx||a.dy)&&l.offset(a.dx||0,a.dy||0),a.dr&&l.move(g,a.dr),e(l.round(),m,a)})}function h(a,c){var e=c.x;d.isString(e)&&(e=parseFloat(e)/100*a.width);var f=c.y;return d.isString(f)&&(f=parseFloat(f)/100*a.height),b.point(e||0,f||0)}c.layout.Port={absolute:function(a,b,c){return a.map(h.bind(null,b))},fn:function(a,b,c){return c.fn(a,b,c)},line:function(a,b,c){var d=h(b,c.start||b.origin()),e=h(b,c.end||b.corner());return f(a,d,e)},left:function(a,b,c){return f(a,b.origin(),b.bottomLeft())},right:function(a,b,c){return f(a,b.topRight(),b.corner())},top:function(a,b,c){return f(a,b.origin(),b.topRight())},bottom:function(a,b,c){return f(a,b.bottomLeft(),b.corner())},ellipseSpread:function(a,b,c){var d=c.startAngle||0,e=c.step||360/a.length;return g(a,b,d,function(a){return a*e})},ellipse:function(a,b,c){var d=c.startAngle||0,e=c.step||20;return g(a,b,d,function(a,b){return(a+.5-b/2)*e})}}}(_,g,joint,joint.util),function(a,b,c,d){function e(a,b){return d.defaultsDeep({},a,b,{x:0,y:0,angle:0,attrs:{".":{y:"0","text-anchor":"start"}}})}function f(a,b,c,f){f=d.defaults({},f,{offset:15});var h,i,j,k,l=b.center().theta(a),m=g(b),n=f.offset,o=0;lm[2]?(j=".3em",h=n,i=0,k="start"):lo[2]?(k=".3em",i=-m,j=0,l="end"):h-270&&i<-90?(g="start",j=i-180):g="end";var m=Math.round;return e({x:m(k.x),y:m(k.y),angle:c?j:0,attrs:{".":{y:l,"text-anchor":g}}})}c.layout.PortLabel={manual:function(a,b,c){return e(c,a)},left:function(a,b,c){return e(c,{x:-15,attrs:{".":{y:".3em","text-anchor":"end"}}})},right:function(a,b,c){return e(c,{x:15,attrs:{".":{y:".3em","text-anchor":"start"}}})},top:function(a,b,c){return e(c,{y:-15,attrs:{".":{"text-anchor":"middle"}}})},bottom:function(a,b,c){return e(c,{y:15,attrs:{".":{y:".6em","text-anchor":"middle"}}})},outsideOriented:function(a,b,c){return f(a,b,!0,c)},outside:function(a,b,c){return f(a,b,!1,c)},insideOriented:function(a,b,c){return h(a,b,!0,c)},inside:function(a,b,c){return h(a,b,!1,c)},radial:function(a,b,c){return i(a.difference(b.center()),!1,c)},radialOriented:function(a,b,c){return i(a.difference(b.center()),!0,c)}}}(_,g,joint,joint.util),joint.highlighters.addClass={className:joint.util.addClassNamePrefix("highlighted"),highlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).addClass(e)},unhighlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).removeClass(e)}},joint.highlighters.opacity={highlight:function(a,b){V(b).addClass(joint.util.addClassNamePrefix("highlight-opacity"))},unhighlight:function(a,b){V(b).removeClass(joint.util.addClassNamePrefix("highlight-opacity"))}},joint.highlighters.stroke={defaultOptions:{padding:3,rx:0,ry:0,attrs:{"stroke-width":3,stroke:"#FEB663"}},_views:{},getHighlighterId:function(a,b){return a.id+JSON.stringify(b)},removeHighlighter:function(a){this._views[a]&&(this._views[a].remove(),this._views[a]=null)},highlight:function(a,b,c){var d=this.getHighlighterId(b,c);if(!this._views[d]){var e,f=joint.util.defaults(c||{},this.defaultOptions),g=V(b);try{var h=g.convertToPathData()}catch(a){e=g.bbox(!0),h=V.rectToPath(joint.util.assign({},f,e))}var i=V("path").attr({d:h,"pointer-events":"none","vector-effect":"non-scaling-stroke",fill:"none"}).attr(f.attrs),j=g.getTransformToElement(a.el),k=f.padding;if(k){e||(e=g.bbox(!0));var l=e.x+e.width/2,m=e.y+e.height/2;e=V.transformRect(e,j);var n=Math.max(e.width,1),o=Math.max(e.height,1),p=(n+k)/n,q=(o+k)/o,r=V.createSVGMatrix({a:p,b:0,c:0,d:q,e:l-p*l,f:m-q*m});j=j.multiply(r)}i.transform(j);var s=this._views[d]=new joint.mvc.View({svgElement:!0,className:"highlight-stroke",el:i.node}),t=this.removeHighlighter.bind(this,d),u=a.model;s.listenTo(u,"remove",t),s.listenTo(u.graph,"reset",t),a.vel.append(i)}},unhighlight:function(a,b,c){this.removeHighlighter(this.getHighlighterId(b,c))}}; +var g=function(){function a(a,b){return b.unshift(null),new(Function.prototype.bind.apply(a,b))}function b(a){var b,c,d=[];for(c=arguments.length,b=1;b=1)return[new q(b,c,d,e),new q(e,e,e,e)];var f=this.getSkeletonPoints(a),g=f.startControlPoint1,h=f.startControlPoint2,i=f.divider,j=f.dividerControlPoint1,k=f.dividerControlPoint2;return[new q(b,g,h,i),new q(i,j,k,e)]},endpointDistance:function(){return this.start.distance(this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.controlPoint1.x===a.controlPoint1.x&&this.controlPoint1.y===a.controlPoint1.y&&this.controlPoint2.x===a.controlPoint2.x&&this.controlPoint2.y===a.controlPoint2.y&&this.end.x===a.end.x&&this.end.y===a.end.y},getSkeletonPoints:function(a){var b=this.start,c=this.controlPoint1,d=this.controlPoint2,e=this.end;if(a<=0)return{startControlPoint1:b.clone(),startControlPoint2:b.clone(),divider:b.clone(),dividerControlPoint1:c.clone(),dividerControlPoint2:d.clone()};if(a>=1)return{startControlPoint1:c.clone(),startControlPoint2:d.clone(),divider:e.clone(),dividerControlPoint1:e.clone(),dividerControlPoint2:e.clone()};var f=new s(b,c).pointAt(a),g=new s(c,d).pointAt(a),h=new s(d,e).pointAt(a),i=new s(f,g).pointAt(a),j=new s(g,h).pointAt(a),k=new s(i,j).pointAt(a),l={startControlPoint1:f,startControlPoint2:i,divider:k,dividerControlPoint1:j,dividerControlPoint2:h};return l},getSubdivisions:function(a){a=a||{};var b=void 0===a.precision?this.PRECISION:a.precision,c=[new q(this.start,this.controlPoint1,this.controlPoint2,this.end)];if(0===b)return c;for(var d=this.endpointDistance(),e=p(10,-b),f=0;;){f+=1;for(var g=[],h=c.length,i=0;i1&&r=1)return this.end.clone();var c=this.tAt(a,b);return this.pointAtT(c)},pointAtLength:function(a,b){var c=this.tAtLength(a,b);return this.pointAtT(c)},pointAtT:function(a){return a<=0?this.start.clone():a>=1?this.end.clone():this.getSkeletonPoints(a).divider},PRECISION:3,scale:function(a,b,c){return this.start.scale(a,b,c),this.controlPoint1.scale(a,b,c),this.controlPoint2.scale(a,b,c),this.end.scale(a,b,c),this},tangentAt:function(a,b){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var c=this.tAt(a,b);return this.tangentAtT(c)},tangentAtLength:function(a,b){if(!this.isDifferentiable())return null;var c=this.tAtLength(a,b);return this.tangentAtT(c)},tangentAtT:function(a){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var b=this.getSkeletonPoints(a),c=b.startControlPoint2,d=b.dividerControlPoint1,e=b.divider,f=new s(c,d);return f.translate(e.x-c.x,e.y-c.y),f},tAt:function(a,b){if(a<=0)return 0;if(a>=1)return 1;b=b||{};var c=void 0===b.precision?this.PRECISION:b.precision,d=void 0===b.subdivisions?this.getSubdivisions({precision:c}):b.subdivisions,e={precision:c,subdivisions:d},f=this.length(e),g=f*a;return this.tAtLength(g,e)},tAtLength:function(a,b){var c=!0;a<0&&(c=!1,a=-a),b=b||{};for(var d,e,f,g,h,i=void 0===b.precision?this.PRECISION:b.precision,j=void 0===b.subdivisions?this.getSubdivisions({precision:i}):b.subdivisions,k={precision:i,subdivisions:j},l=0,m=j.length,n=1/m,o=c?0:m-1;c?o=0;c?o++:o--){var q=j[o],r=q.endpointDistance();if(a<=l+r){d=q,e=o*n,f=(o+1)*n,g=c?a-l:r+l-a,h=c?r+l-a:a-l;break}l+=r}if(!d)return c?1:0;for(var s=this.length(k),t=p(10,-i);;){var u;if(u=0!==s?g/s:0,ui.x+g/2,m=ei.x?f-d:f+d,c=g*g/(e-j)-g*g*(f-k)*(b-k)/(h*h*(e-j))+j):(c=f>i.y?e+d:e-d,b=h*h/(f-k)-h*h*(e-j)*(c-j)/(g*g*(f-k))+k),new u(c,b).theta(a)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLine:function(a){var b=[],c=a.start,d=a.end,e=this.a,f=this.b,g=a.vector(),i=c.difference(new u(this)),j=new u(g.x/(e*e),g.y/(f*f)),k=new u(i.x/(e*e),i.y/(f*f)),l=g.dot(j),m=g.dot(k),n=i.dot(k)-1,o=m*m-l*n;if(o<0)return null;if(o>0){var p=h(o),q=(-m-p)/l,r=(-m+p)/l;if((q<0||10){if(f>d||g>d)return null}else if(f=1?c.clone():b.lerp(c,a)},pointAtLength:function(a){var b=this.start,c=this.end;fromStart=!0,a<0&&(fromStart=!1,a=-a);var d=this.length();return a>=d?fromStart?c.clone():b.clone():this.pointAt((fromStart?a:d-a)/d)},pointOffset:function(a){a=new c.Point(a);var b=this.start,d=this.end,e=(d.x-b.x)*(a.y-b.y)-(d.y-b.y)*(a.x-b.x);return e/this.length()},rotate:function(a,b){return this.start.rotate(a,b),this.end.rotate(a,b),this},round:function(a){var b=p(10,a||0);return this.start.x=l(this.start.x*b)/b,this.start.y=l(this.start.y*b)/b,this.end.x=l(this.end.x*b)/b,this.end.y=l(this.end.y*b)/b,this},scale:function(a,b,c){return this.start.scale(a,b,c),this.end.scale(a,b,c),this},setLength:function(a){var b=this.length();if(!b)return this;var c=a/b;return this.scale(c,c,this.start)},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},tangentAt:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAt(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},tangentAtLength:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAtLength(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},translate:function(a,b){return this.start.translate(a,b),this.end.translate(a,b),this},vector:function(){return new u(this.end.x-this.start.x,this.end.y-this.start.y)},toString:function(){return this.start.toString()+" "+this.end.toString()}},s.prototype.intersection=s.prototype.intersect;var t=c.Path=function(a){if(!(this instanceof t))return new t(a);if("string"==typeof a)return new t.parse(a);this.segments=[];var b,c;if(a){if(Array.isArray(a)&&0!==a.length)if(c=a.length,a[0].isSegment)for(b=0;b=c||a<0)throw new Error("Index out of range.");return b[a]},getSegmentSubdivisions:function(a){var b=this.segments,c=b.length;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=[],f=0;fd||a<0)throw new Error("Index out of range.");var e,f=null,g=null;if(0!==d&&(a>=1?(f=c[a-1],g=f.nextSegment):g=c[0]),Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");for(var h=b.length,i=0;i=d?(e=d-1,f=1):f<0?f=0:f>1&&(f=1),b=b||{};for(var g,h=void 0===b.precision?this.PRECISION:b.precision,i=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:h}):b.segmentSubdivisions,j=0,k=0;k=1)return this.end.clone();b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.pointAtLength(i,g)},pointAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;if(0===a)return this.start.clone();var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isVisible){if(a<=i+m)return k.pointAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f)return e?f.end:f.start;var n=c[d-1];return n.end.clone()},pointAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].pointAtT(0);if(d>=c)return b[c-1].pointAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].pointAtT(e)},prepareSegment:function(a,b,c){a.previousSegment=b,a.nextSegment=c,b&&(b.nextSegment=a),c&&(c.previousSegment=a);var d=a;return a.isSubpathStart&&(a.subpathStartSegment=a,d=c),d&&this.updateSubpathStartSegment(d),a},PRECISION:3,removeSegment:function(a){var b=this.segments,c=b.length;if(0===c)throw new Error("Path has no segments.");if(a<0&&(a=c+a),a>=c||a<0)throw new Error("Index out of range.");var d=b.splice(a,1)[0],e=d.previousSegment,f=d.nextSegment;e&&(e.nextSegment=f),f&&(f.previousSegment=e),d.isSubpathStart&&f&&this.updateSubpathStartSegment(f)},replaceSegment:function(a,b){var c=this.segments,d=c.length;if(0===d)throw new Error("Path has no segments.");if(a<0&&(a=d+a),a>=d||a<0)throw new Error("Index out of range.");var e,f=c[a],g=f.previousSegment,h=f.nextSegment,i=f.isSubpathStart;if(Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");c.splice(a,1);for(var j=b.length,k=0;k1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.segmentIndexAtLength(i,g)},toPoints:function(a){var b=this.segments,c=b.length;if(0===c)return null;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=void 0===a.segmentSubdivisions?this.getSegmentSubdivisions({precision:d}):a.segmentSubdivisions,f=[],g=[],h=0;h0){var k=j.map(function(a){return a.start});Array.prototype.push.apply(g,k)}else g.push(i.start)}else g.length>0&&(g.push(b[h-1].end),f.push(g),g=[])}return g.length>0&&(g.push(this.end),f.push(g)),f},toPolylines:function(a){var b=[],c=this.toPoints(a);if(!c)return null;for(var d=0,e=c.length;d=0;e?j++:j--){var k=c[j],l=g[j],m=k.length({precision:f,subdivisions:l});if(k.isVisible){if(a<=i+m)return j;h=j}i+=m}return h},tangentAt:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;a<0&&(a=0),a>1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.tangentAtLength(i,g)},tangentAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isDifferentiable()){if(a<=i+m)return k.tangentAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f){var n=e?1:0;return f.tangentAtT(n)}return null},tangentAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].tangentAtT(0);if(d>=c)return b[c-1].tangentAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].tangentAtT(e)},translate:function(a,b){for(var c=this.segments,d=c.length,e=0;e=0;c--){var d=a[c];if(d.isVisible)return d.end}return a[b-1].end}});var u=c.Point=function(a,b){if(!(this instanceof u))return new u(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseFloat(c[0]),b=parseFloat(c[1])}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};u.fromPolar=function(a,b,c){c=c&&new u(c)||new u(0,0);var d=e(a*f(b)),h=e(a*g(b)),i=x(z(b));return i<90?h=-h:i<180?(d=-d,h=-h):i<270&&(d=-d),new u(c.x+d,c.y+h)},u.random=function(a,b,c,d){return new u(m(o()*(b-a+1)+a),m(o()*(d-c+1)+c))},u.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=i(j(this.x,a.x),a.x+a.width),this.y=i(j(this.y,a.y),a.y+a.height),this)},bearing:function(a){return new s(this,a).bearing()},changeInAngle:function(a,b,c){return this.clone().offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return new u(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),new u(this.x-(a||0),this.y-(b||0))},distance:function(a){return new s(this,a).length()},squaredDistance:function(a){return new s(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return h(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return e(a.x-this.x)+e(a.y-this.y)},move:function(a,b){var c=A(new u(a).theta(this)),d=this.offset(f(c)*b,-g(c)*b);return d},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return new u(a).move(this,this.distance(a))},rotate:function(a,b){a=a||new c.Point(0,0),b=A(x(-b));var d=f(b),e=g(b),h=d*(this.x-a.x)-e*(this.y-a.y)+a.x,i=e*(this.x-a.x)+d*(this.y-a.y)+a.y;return this.x=h,this.y=i,this},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this},scale:function(a,b,c){return c=c&&new u(c)||new u(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=y(this.x,a),this.y=y(this.y,b||a),this},theta:function(a){a=new u(a);var b=-(a.y-this.y),c=a.x-this.x,d=k(b,c);return d<0&&(d=2*n+d),180*d/n},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=new u(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&new u(a)||new u(0,0);var b=this.x,c=this.y;return this.x=h((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=A(a.theta(new u(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN},lerp:function(a,b){var c=this.x,d=this.y;return new u((1-b)*c+b*a.x,(1-b)*d+b*a.y)}},u.prototype.translate=u.prototype.offset;var v=c.Rect=function(a,b,c,d){return this instanceof v?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new v(a,b,c,d)};v.fromEllipse=function(a){return a=new r(a),new v(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},v.prototype={bbox:function(a){if(!a)return this.clone();var b=A(a||0),c=e(g(b)),d=e(f(b)),h=this.width*d+this.height*c,i=this.width*c+this.height*d;return new v(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return new u(this.x,this.y+this.height)},bottomLine:function(){return new s(this.bottomLeft(),this.bottomRight())},bottomMiddle:function(){ +return new u(this.x+this.width/2,this.y+this.height)},center:function(){return new u(this.x+this.width/2,this.y+this.height/2)},clone:function(){return new v(this)},containsPoint:function(a){return a=new u(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=new v(this).normalize(),c=new v(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return new u(this.x+this.width,this.y+this.height)},equals:function(a){var b=new v(this).normalize(),c=new v(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=j(b.x,d.x),g=j(b.y,d.y);return new v(f,g,i(c.x,e.x)-f,i(c.y,e.y)-g)},intersectionWithLine:function(a){var b,c,d=this,e=[d.topLine(),d.rightLine(),d.bottomLine(),d.leftLine()],f=[],g=[],h=e.length;for(c=0;c0?f:null},intersectionWithLineFromCenterToPoint:function(a,b){a=new u(a);var c,d=new u(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[this.topLine(),this.rightLine(),this.bottomLine(),this.leftLine()],f=new s(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return new s(this.topLeft(),this.bottomLeft())},leftMiddle:function(){return new u(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return u.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return new u(this.x,this.y)},pointNearestToPoint:function(a){if(a=new u(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return new u(this.x+this.width,a.y);case"left":return new u(this.x,a.y);case"bottom":return new u(a.x,this.y+this.height);case"top":return new u(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return new s(this.topRight(),this.bottomRight())},rightMiddle:function(){return new u(this.x+this.width,this.y+this.height/2)},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this.width=l(this.width*b)/b,this.height=l(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(a,b){a=new v(a),b||(b=a.center());var c,d,e,f,g,h,j,k,l=b.x,m=b.y;c=d=e=f=g=h=j=k=1/0;var n=a.topLeft();n.xl&&(d=(this.x+this.width-l)/(o.x-l)),o.y>m&&(h=(this.y+this.height-m)/(o.y-m));var p=a.topRight();p.x>l&&(e=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:i(c,d,e,f),sy:i(g,h,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return i(c.sx,c.sy)},sideNearestToPoint:function(a){a=new u(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cb&&(b=i),jd&&(d=j)}return new v(a,c,b-a,d-c)},clone:function(){var a=this.points,b=a.length;if(0===b)return new w;for(var c=[],d=0;df.x&&(f=c[a]);var g=[];for(a=0;a2){var j=g[g.length-1];g.unshift(j)}for(var k,l,m,n,o,p,q={},r=[];0!==g.length;)if(k=g.pop(),l=k[0],!q.hasOwnProperty(k[0]+"@@"+k[1]))for(var s=!1;!s;)if(r.length<2)r.push(k),s=!0;else{m=r.pop(),n=m[0],o=r.pop(),p=o[0];var t=p.cross(n,l);if(t<0)r.push(o),r.push(m),r.push(k),s=!0;else if(0===t){var u=1e-10,v=n.angleBetween(p,l);e(v-180)2&&r.pop();var x,y=-1;for(b=r.length,a=0;a0){var B=r.slice(y),C=r.slice(0,y);A=B.concat(C)}else A=r;var D=[];for(b=A.length,a=0;a=1)return b[c-1].clone();var d=this.length(),e=d*a;return this.pointAtLength(e)},pointAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return b[0].clone();var d=!0;a<0&&(d=!1,a=-a);for(var e=0,f=c-1,g=d?0:f-1;d?g=0;d?g++:g--){var h=b[g],i=b[g+1],j=new s(h,i),k=h.distance(i);if(a<=e+k)return j.pointAtLength((d?1:-1)*(a-e));e+=k}var l=d?b[c-1]:b[0];return l.clone()},scale:function(a,b,c){var d=this.points,e=d.length;if(0===e)return this;for(var f=0;f1&&(a=1);var d=this.length(),e=d*a;return this.tangentAtLength(e)},tangentAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return null;var d=!0;a<0&&(d=!1,a=-a);for(var e,f=0,g=c-1,h=d?0:g-1;d?h=0;d?h++:h--){var i=b[h],j=b[h+1],k=new s(i,j),l=i.distance(j);if(k.isDifferentiable()){if(a<=f+l)return k.tangentAtLength((d?1:-1)*(a-f));e=k}f+=l}if(e){var m=d?1:0;return e.tangentAt(m)}return null},intersectionWithLine:function(a){for(var b=new s(a),c=[],d=this.points,e=0,f=d.length-1;e0?c:null},translate:function(a,b){var c=this.points,d=c.length;if(0===d)return this;for(var e=0;e=1?b:b*a},pointAtT:function(a){if(this.pointAt)return this.pointAt(a);throw new Error("Neither pointAtT() nor pointAt() function is implemented.")},previousSegment:null,subpathStartSegment:null,tangentAtT:function(a){if(this.tangentAt)return this.tangentAt(a);throw new Error("Neither tangentAtT() nor tangentAt() function is implemented.")},bbox:function(){throw new Error("Declaration missing for virtual function.")},clone:function(){throw new Error("Declaration missing for virtual function.")},closestPoint:function(){throw new Error("Declaration missing for virtual function.")},closestPointLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointNormalizedLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointTangent:function(){throw new Error("Declaration missing for virtual function.")},equals:function(){throw new Error("Declaration missing for virtual function.")},getSubdivisions:function(){throw new Error("Declaration missing for virtual function.")},isDifferentiable:function(){throw new Error("Declaration missing for virtual function.")},length:function(){throw new Error("Declaration missing for virtual function.")},pointAt:function(){throw new Error("Declaration missing for virtual function.")},pointAtLength:function(){throw new Error("Declaration missing for virtual function.")},scale:function(){throw new Error("Declaration missing for virtual function.")},tangentAt:function(){throw new Error("Declaration missing for virtual function.")},tangentAtLength:function(){throw new Error("Declaration missing for virtual function.")},translate:function(){throw new Error("Declaration missing for virtual function.")},serialize:function(){throw new Error("Declaration missing for virtual function.")},toString:function(){throw new Error("Declaration missing for virtual function.")}},C=function(){for(var b=[],c=arguments.length,d=0;d0)throw new Error("Closepath constructor expects no arguments.");return this},J={clone:function(){return new I},getSubdivisions:function(){return[]},isDifferentiable:function(){return!(!this.previousSegment||!this.subpathStartSegment)&&!this.start.equals(this.end)},scale:function(){return this},translate:function(){return this},type:"Z",serialize:function(){return this.type},toString:function(){return this.type+" "+this.start+" "+this.end}};Object.defineProperty(J,"start",{configurable:!0,enumerable:!0,get:function(){if(!this.previousSegment)throw new Error("Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)");return this.previousSegment.end}}),Object.defineProperty(J,"end",{configurable:!0,enumerable:!0,get:function(){if(!this.subpathStartSegment)throw new Error("Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)");return this.subpathStartSegment.end}}),I.prototype=b(B,s.prototype,J);var K=t.segmentTypes={L:C,C:E,M:G,Z:I,z:I};return t.regexSupportedData=new RegExp("^[\\s\\d"+Object.keys(K).join("")+",.]*$"),t.isDataSupported=function(a){return"string"==typeof a&&this.regexSupportedData.test(a)},c}(); +var V,Vectorizer;V=Vectorizer=function(){"use strict";function a(a,b){a||(a={});var c=q("textPath"),d=a.d;if(d&&void 0===a["xlink:href"]){var e=q("path").attr("d",d).appendTo(b.defs());c.attr("xlink:href","#"+e.id)}return q.isObject(a)&&c.attr(a),c.node}function b(a,b,c){c||(c={});for(var d=c.includeAnnotationIndices,e=c.eol,f=c.lineHeight,g=c.baseSize,h=0,i={},j=b.length-1,k=0;k<=j;k++){var l=b[k],m=null;if(q.isObject(l)){var n=l.attrs,o=q("tspan",n),p=o.node,r=l.t;e&&k===j&&(r+=e),p.textContent=r;var s=n.class;s&&o.addClass(s),d&&o.attr("annotations",l.annotations),m=parseFloat(n["font-size"]),void 0===m&&(m=g),m&&m>h&&(h=m)}else e&&k===j&&(l+=e),p=document.createTextNode(l||" "),g&&g>h&&(h=g);a.appendChild(p)}return h&&(i.maxFontSize=h),f?i.lineHeight=f:h&&(i.lineHeight=1.2*h),i}function c(a,b){var c=parseFloat(a);return s.test(a)?c*b:c}function d(a,b,d,e){if(!Array.isArray(b))return 0;var f=b.length;if(!f)return 0;for(var g=b[0],h=c(g.maxFontSize,d)||d,i=0,j=c(e,d),k=1;k1){var e,g,h=[];for(e=0,g=d.childNodes.length;e0&&D.setAttribute("dy",B),(y>0||g)&&D.setAttribute("x",j),D.className.baseVal=C,r.appendChild(D),v+=E.length+1}if(i)if(l)B=d(h,x,p,o);else if("top"===h)B="0.8em";else{var I;switch(z>0?(I=parseFloat(o)||1,I*=z,s.test(o)||(I/=p)):I=0,h){case"middle":B=.3-I/2+"em";break;case"bottom":B=-I-.3+"em"}}else 0===h?B="0em":h?B=h:(B=0,null===this.attr("y")&&this.attr("y",u||"0.8em"));return r.firstChild.setAttribute("dy",B),this.append(r),this},r.removeAttr=function(a){var b=q.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},r.attr=function(a,b){if(q.isUndefined(a)){for(var c=this.node.attributes,d={},e=0;e1&&k.push(k[0]),new g.Polyline(k);case"PATH":return l=this.attr("d"),g.Path.isDataSupported(l)||(l=q.normalizePathData(l)),new g.Path(l);case"LINE":return x1=parseFloat(this.attr("x1"))||0,y1=parseFloat(this.attr("y1"))||0,x2=parseFloat(this.attr("x2"))||0,y2=parseFloat(this.attr("y2"))||0,new g.Line({x:x1,y:y1},{x:x2,y:y2})}return this.getBBox()},r.findIntersection=function(a,b){var c=this.svg().node;b=b||c;var d=this.getBBox({target:b}),e=d.center();if(d.intersectionWithLineFromCenterToPoint(a)){var f,h=this.tagName();if("RECT"===h){var i=new g.Rect(parseFloat(this.attr("x")||0),parseFloat(this.attr("y")||0),parseFloat(this.attr("width")),parseFloat(this.attr("height"))),j=this.getTransformToElement(b),k=q.decomposeMatrix(j),l=c.createSVGTransform();l.setRotate(-k.rotation,e.x,e.y);var m=q.transformRect(i,l.matrix.multiply(j));f=new g.Rect(m).intersectionWithLineFromCenterToPoint(a,k.rotation)}else if("PATH"===h||"POLYGON"===h||"POLYLINE"===h||"CIRCLE"===h||"ELLIPSE"===h){var n,o,p,r,s,t,u="PATH"===h?this:this.convertToPath(),v=u.sample(),w=1/0,x=[];for(n=0;n'+(a||"")+"",c=q.parseXML(b,{async:!1});return c.documentElement},q.idCounter=0,q.uniqueId=function(){return"v-"+ ++q.idCounter},q.toNode=function(a){return q.isV(a)?a.node:a.nodeName&&a||a[0]},q.ensureId=function(a){return a=q.toNode(a),a.id||(a.id=q.uniqueId())},q.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},q.isUndefined=function(a){return"undefined"==typeof a},q.isString=function(a){return"string"==typeof a},q.isObject=function(a){return a&&"object"==typeof a},q.isArray=Array.isArray,q.parseXML=function(a,b){b=b||{};var c;try{var d=new DOMParser;q.isUndefined(b.async)||(d.async=b.async),c=d.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},q.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var b=a.split(":");return{ns:f[b[0]],local:b[1]}}return{ns:null,local:a}},q.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,q.transformSeparatorRegex=/[ ,]+/,q.transformationListRegex=/^(\w+)\((.*)\)/,q.transformStringToMatrix=function(a){var b=q.createSVGMatrix(),c=a&&a.match(q.transformRegex);if(!c)return b;for(var d=0,e=c.length;d=0){var f=q.transformStringToMatrix(a),g=q.decomposeMatrix(f);b=[g.translateX,g.translateY],d=[g.scaleX,g.scaleY],c=[g.rotation];var h=[];0===b[0]&&0===b[0]||h.push("translate("+b+")"),1===d[0]&&1===d[1]||h.push("scale("+d+")"),0!==c[0]&&h.push("rotate("+c+")"),a=h.join(" ")}else{var i=a.match(/translate\((.*?)\)/);i&&(b=i[1].split(e));var j=a.match(/rotate\((.*?)\)/);j&&(c=j[1].split(e));var k=a.match(/scale\((.*?)\)/);k&&(d=k[1].split(e))}}var l=d&&d[0]?parseFloat(d[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:l,sy:d&&d[1]?parseFloat(d[1]):l}}},q.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},q.decomposeMatrix=function(a){var b=q.deltaTransformPoint(a,{x:0,y:1}),c=q.deltaTransformPoint(a,{x:1,y:0}),d=180/j*k(b.y,b.x)-90,e=180/j*k(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:l(a.a*a.a+a.b*a.b),scaleY:l(a.c*a.c+a.d*a.d),skewX:d,skewY:e,rotation:d}},q.matrixToScale=function(a){var b,c,d,e;return a?(b=q.isUndefined(a.a)?1:a.a,e=q.isUndefined(a.d)?1:a.d,c=a.b,d=a.c):b=e=1,{sx:c?l(b*b+c*c):b,sy:d?l(d*d+e*e):e}},q.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=q.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(k(b.y,b.x))-90)}},q.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},q.isV=function(a){return a instanceof q},q.isVElement=q.isV;var t=q("svg").node;return q.createSVGMatrix=function(a){var b=t.createSVGMatrix();for(var c in a)b[c]=a[c];return b},q.createSVGTransform=function(a){return q.isUndefined(a)?t.createSVGTransform():(a instanceof SVGMatrix||(a=q.createSVGMatrix(a)),t.createSVGTransformFromMatrix(a))},q.createSVGPoint=function(a,b){var c=t.createSVGPoint();return c.x=a,c.y=b,c},q.transformRect=function(a,b){var c=t.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var e=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var f=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var h=c.matrixTransform(b),i=m(d.x,e.x,f.x,h.x),j=n(d.x,e.x,f.x,h.x),k=m(d.y,e.y,f.y,h.y),l=n(d.y,e.y,f.y,h.y);return new g.Rect(i,k,j-i,l-k)},q.transformPoint=function(a,b){return new g.Point(q.createSVGPoint(a.x,a.y).matrixTransform(b))},q.transformLine=function(a,b){return new g.Line(q.transformPoint(a.start,b),q.transformPoint(a.end,b))},q.transformPolyline=function(a,b){var c=a instanceof g.Polyline?a.points:a;q.isArray(c)||(c=[]);for(var d=[],e=0,f=c.length;e=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L"+f*q+","+f*r+"A"+f+","+f+" 0 "+l+",0 "+f*m+","+f*n+"Z":"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L0,0Z"},q.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?q.isObject(a[c])&&q.isObject(b[c])?a[c]=q.mergeAttrs(a[c],b[c]):q.isObject(a[c])?a[c]=q.mergeAttrs(a[c],q.styleToObject(b[c])):q.isObject(b[c])?a[c]=q.mergeAttrs(q.styleToObject(a[c]),b[c]):a[c]=q.mergeAttrs(q.styleToObject(a[c]),q.styleToObject(b[c])):a[c]=b[c];return a},q.annotateString=function(a,b,c){b=b||[],c=c||{};for(var d,e,f,g=c.offset||0,h=[],i=[],j=0;j=m&&j=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},q.convertLineToPathData=function(a){a=q(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},q.convertPolygonToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)+" Z"},q.convertPolylineToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)},q.svgPointsToPath=function(a){for(var b=0,c=a.length;b1&&(z=o(z),d*=z,e*=z);var A=d*d,B=e*e,C=(g==h?-1:1)*o(p((A*B-A*y*y-B*x*x)/(A*y*y+B*x*x))),D=C*d*y/e+(a+i)/2,E=C*-e*x/d+(c+q)/2,F=n(((c-E)/e).toFixed(9)),G=n(((q-E)/e).toFixed(9));F=aG&&(F-=2*j),(!h&&G)>F&&(G-=2*j)}var H=G-F;if(p(H)>t){var I=G,J=i,K=q;G=F+t*((h&&G)>F?1:-1),i=D+d*l(G),q=E+e*k(G),v=b(i,q,d,e,f,0,h,J,K,[G,I,D,E])}H=G-F;var L=l(F),M=k(F),N=l(G),O=k(G),P=m(H/4),Q=4/3*(d*P),R=4/3*(e*P),S=[a,c],T=[a+Q*M,c-R*L],U=[i+Q*O,q-R*N],V=[i,q];if(T[0]=2*S[0]-T[0],T[1]=2*S[1]-T[1],r)return[T,U,V].concat(v);v=[T,U,V].concat(v).join().split(",");for(var W=[],X=v.length,Y=0;Y2&&(c.push([d].concat(f.splice(0,2))),g="l",d="m"===d?"l":"L");f.length>=b[g]&&(c.push([d].concat(f.splice(0,b[g]))),b[g]););}),c}function d(a){if(Array.isArray(a)&&Array.isArray(a&&a[0])||(a=c(a)),!a||!a.length)return[["M",0,0]];for(var b,d=[],e=0,f=0,g=0,h=0,i=0,j=a.length,k=i;k7){a[b].shift();for(var c=a[b];c.length;)i[b]="A",a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),l=g.length}}for(var g=d(c),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i=[],j="",k="",l=g.length,m=0;m0&&(k=i[m-1])),g[m]=e(g[m],h,k),"A"!==i[m]&&"C"===j&&(i[m]="C"),f(g,m);var n=g[m],o=n.length;h.x=n[o-2],h.y=n[o-1],h.bx=parseFloat(n[o-4])||h.x,h.by=parseFloat(n[o-3])||h.y}return g[0][0]&&"M"===g[0][0]||g.unshift(["M",0,0]),g}var f="\t\n\v\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029",g=new RegExp("([a-z])["+f+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+f+"]*,?["+f+"]*)+)","ig"),h=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+f+"]*,?["+f+"]*","ig"),i=Math,j=i.PI,k=i.sin,l=i.cos,m=i.tan,n=i.asin,o=i.sqrt,p=i.abs;return function(a){return e(a).join(",").split(",").join(" ")}}(),q.namespace=f,q}(); +var joint={version:"2.1.0",config:{classNamePrefix:"joint-",defaultTheme:"default"},dia:{},ui:{},layout:{},shapes:{},format:{},connectors:{},highlighters:{},routers:{},anchors:{},connectionPoints:{},connectionStrategies:{},linkTools:{},mvc:{views:{}},setTheme:function(a,b){b=b||{},joint.util.invoke(joint.mvc.views,"setTheme",a,b),joint.mvc.View.prototype.defaultTheme=a},env:{_results:{},_tests:{svgforeignobject:function(){return!!document.createElementNS&&/SVGForeignObject/.test({}.toString.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))}},addTest:function(a,b){return joint.env._tests[a]=b},test:function(a){var b=joint.env._tests[a];if(!b)throw new Error('Test not defined ("'+a+'"). Use `joint.env.addTest(name, fn) to add a new test.`');var c=joint.env._results[a];if("undefined"!=typeof c)return c;try{c=b()}catch(a){c=!1}return joint.env._results[a]=c,c}},util:{hashCode:function(a){var b=0;if(0==a.length)return b;for(var c=0;c0){var f=joint.util.getByPath(a,d,c);f&&delete f[e]}else delete a[e];return a},flattenObject:function(a,b,c){b=b||"/";var d={};for(var e in a)if(a.hasOwnProperty(e)){var f="object"==typeof a[e];if(f&&c&&c(a[e])&&(f=!1),f){var g=this.flattenObject(a[e],b,c);for(var h in g)g.hasOwnProperty(h)&&(d[e+b+h]=g[h])}else d[e]=a[e]}return d},uuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"==a?b:3&b|8;return c.toString(16)})},guid:function(a){return this.guid.id=this.guid.id||1,a.id=void 0===a.id?"j_"+this.guid.id++:a.id,a.id},toKebabCase:function(a){return a.replace(/[A-Z]/g,"-$&").toLowerCase()},mixin:_.assign,supplement:_.defaults,deepMixin:_.mixin,deepSupplement:_.defaultsDeep,normalizeEvent:function(a){var b=a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0];if(b){for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return a},nextFrame:function(){var a;if("undefined"!=typeof window&&(a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame),!a){var b=0;a=function(a){var c=(new Date).getTime(),d=Math.max(0,16-(c-b)),e=setTimeout(function(){a(c+d)},d);return b=c+d,e}}return function(b,c){return a(c?b.bind(c):b)}}(),cancelFrame:function(){var a,b="undefined"!=typeof window;return b&&(a=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame),a=a||clearTimeout,b?a.bind(window):a}(),shapePerimeterConnectionPoint:function(a,b,c,d){var e,f;if(!c){var g=b.$(".scalable")[0],h=b.$(".rotatable")[0];g&&g.firstChild?c=g.firstChild:h&&h.firstChild&&(c=h.firstChild)}return c?(f=V(c).findIntersection(d,a.paper.viewport),f||(e=V(c).getBBox({target:a.paper.viewport}))):(e=b.model.getBBox(),f=e.intersectionWithLineFromCenterToPoint(d)),f||e.center()},isPercentage:function(a){return joint.util.isString(a)&&"%"===a.slice(-1)},parseCssNumeric:function(a,b){b=b||[];var c={value:parseFloat(a)};if(Number.isNaN(c.value))return null;var d=b.join("|");if(joint.util.isString(a)){var e=new RegExp("(\\d+)("+d+")$").exec(a);if(!e)return null;e[2]&&(c.unit=e[2])}return c},breakText:function(a,b,c,d){d=d||{},c=c||{};var e=b.width,f=b.height,g=d.svgDocument||V("svg").node,h=V("tspan").node,i=V("text").attr(c).append(h).node,j=document.createTextNode("");i.style.opacity=0,i.style.display="block",h.style.display="block",h.appendChild(j),g.appendChild(i),d.svgDocument||document.body.appendChild(g);for(var k,l,m=d.separator||" ",n=d.eol||"\n",o=a.split(m),p=[],q=[],r=0,s=0,t=o.length;r=0)if(u.length>1){for(var v=u.split(n),w=0,x=v.length-1;wf){q.splice(Math.floor(f/l));break}}}}return d.svgDocument?g.removeChild(i):document.body.removeChild(g),q.join(n)},sanitizeHTML:function(a){var b=$($.parseHTML("
"+a+"
",null,!1));return b.find("*").each(function(){var a=this;$.each(a.attributes,function(){var b=this,c=b.name,d=b.value;0!==c.indexOf("on")&&0!==d.indexOf("javascript:")||$(a).removeAttr(c)})}),b.html()},downloadBlob:function(a,b){if(window.navigator.msSaveBlob)window.navigator.msSaveBlob(a,b);else{var c=window.URL.createObjectURL(a),d=document.createElement("a");d.href=c,d.download=b,document.body.appendChild(d),d.click(),document.body.removeChild(d),window.URL.revokeObjectURL(c)}},downloadDataUri:function(a,b){var c=joint.util.dataUriToBlob(a);joint.util.downloadBlob(c,b)},dataUriToBlob:function(a){a=a.replace(/\s/g,""),a=decodeURIComponent(a);var b,c=a.indexOf(","),d=a.slice(0,c),e=d.split(":")[1].split(";")[0],f=a.slice(c+1);b=d.indexOf("base64")>=0?atob(f):unescape(encodeURIComponent(f));for(var g=new window.Uint8Array(b.length),h=0;h=1)return 1;var b=a*a,c=b*a;return 4*(a<.5?c:3*(a-b)+c-.75)},exponential:function(a){return Math.pow(2,10*(a-1))},bounce:function(a){for(var b=0,c=1;1;b+=c,c/=2)if(a>=(7-4*b)/11){var d=(11-6*b-11*a)/4;return-d*d+c*c}},reverse:function(a){return function(b){return 1-a(1-b)}},reflect:function(a){return function(b){return.5*(b<.5?a(2*b):2-a(2-2*b))}},clamp:function(a,b,c){return b=b||0,c=c||1,function(d){var e=a(d);return ec?c:e}},back:function(a){return a||(a=1.70158),function(b){return b*b*((a+1)*b-a)}},elastic:function(a){return a||(a=1.5),function(b){return Math.pow(2,10*(b-1))*Math.cos(20*Math.PI*a/3*b)}}},interpolate:{number:function(a,b){var c=b-a;return function(b){return a+c*b}},object:function(a,b){var c=Object.keys(a);return function(d){var e,f,g={};for(e=c.length-1;e!=-1;e--)f=c[e],g[f]=a[f]+(b[f]-a[f])*d;return g}},hexColor:function(a,b){var c=parseInt(a.slice(1),16),d=parseInt(b.slice(1),16),e=255&c,f=(255&d)-e,g=65280&c,h=(65280&d)-g,i=16711680&c,j=(16711680&d)-i;return function(a){var b=e+f*a&255,c=g+h*a&65280,d=i+j*a&16711680;return"#"+(1<<24|b|c|d).toString(16).slice(1)}},unit:function(a,b){var c=/(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/,d=c.exec(a),e=c.exec(b),f=e[1].indexOf("."),g=f>0?e[1].length-f-1:0;a=+d[1];var h=+e[1]-a,i=d[2];return function(b){return(a+h*b).toFixed(g)+i}}},filter:{outline:function(a){var b='',c=Number.isFinite(a.margin)?a.margin:2,d=Number.isFinite(a.width)?a.width:1;return joint.util.template(b)({color:a.color||"blue",opacity:Number.isFinite(a.opacity)?a.opacity:1,outerRadius:c+d,innerRadius:c})},highlight:function(a){var b='';return joint.util.template(b)({color:a.color||"red",width:Number.isFinite(a.width)?a.width:1,blur:Number.isFinite(a.blur)?a.blur:0,opacity:Number.isFinite(a.opacity)?a.opacity:1})},blur:function(a){var b=Number.isFinite(a.x)?a.x:2;return joint.util.template('')({stdDeviation:Number.isFinite(a.y)?[b,a.y]:b})},dropShadow:function(a){var b="SVGFEDropShadowElement"in window?'':'';return joint.util.template(b)({dx:a.dx||0,dy:a.dy||0,opacity:Number.isFinite(a.opacity)?a.opacity:1,color:a.color||"black",blur:Number.isFinite(a.blur)?a.blur:4})},grayscale:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},sepia:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},saturate:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:1-b})},hueRotate:function(a){return joint.util.template('')({angle:a.angle||0})},invert:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:1-b})},brightness:function(a){return joint.util.template('')({amount:Number.isFinite(a.amount)?a.amount:1})},contrast:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:.5-b/2})}},format:{number:function(a,b,c){function d(a){for(var b=a.length,d=[],e=0,f=c.grouping[0];b>0&&f>0;)d.push(a.substring(b-=f,b+f)),f=c.grouping[e=(e+1)%c.grouping.length];return d.reverse().join(c.thousands)}c=c||{currency:["$",""],decimal:".",thousands:",",grouping:[3]};var e=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,f=e.exec(a),g=f[1]||" ",h=f[2]||">",i=f[3]||"",j=f[4]||"",k=f[5],l=+f[6],m=f[7],n=f[8],o=f[9],p=1,q="",r="",s=!1;switch(n&&(n=+n.substring(1)),(k||"0"===g&&"="===h)&&(k=g="0",h="=",m&&(l-=Math.floor((l-1)/4))),o){case"n":m=!0,o="g";break;case"%":p=100,r="%",o="f";break;case"p":p=100,r="%",o="r";break;case"b":case"o":case"x":case"X":"#"===j&&(q="0"+o.toLowerCase());break;case"c":case"d":s=!0,n=0;break;case"s":p=-1,o="r"}"$"===j&&(q=c.currency[0],r=c.currency[1]),"r"!=o||n||(o="g"),null!=n&&("g"==o?n=Math.max(1,Math.min(21,n)):"e"!=o&&"f"!=o||(n=Math.max(0,Math.min(20,n))));var t=k&&m;if(s&&b%1)return"";var u=b<0||0===b&&1/b<0?(b=-b,"-"):i,v=r;if(p<0){var w=this.prefix(b,n);b=w.scale(b),v=w.symbol+r}else b*=p;b=this.convert(o,b,n);var x=b.lastIndexOf("."),y=x<0?b:b.substring(0,x),z=x<0?"":c.decimal+b.substring(x+1);!k&&m&&c.grouping&&(y=d(y));var A=q.length+y.length+z.length+(t?0:u.length),B=A"===h?B+u+b:"^"===h?B.substring(0,A>>=1)+u+b+B.substring(A):u+(t?b:B+b))+v},string:function(a,b){for(var c,d="{",e=!1,f=[];(c=a.indexOf(d))!==-1;){var g,h,i;if(g=a.slice(0,c),e){h=g.split(":"),i=h.shift().split("."),g=b;for(var j=0;j8?function(a){return a/c}:function(a){return a*c},symbol:a}}),d=0;return a&&(a<0&&(a*=-1),b&&(a=this.round(a,this.precision(a,b))),d=1+Math.floor(1e-12+Math.log(a)/Math.LN10),d=Math.max(-24,Math.min(24,3*Math.floor((d<=0?d+1:d-1)/3)))),c[8+d/3]}},template:function(a){var b=/<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g;return function(c){return c=c||{},a.replace(b,function(a){for(var b=Array.from(arguments),d=b.slice(1,4).find(function(a){return!!a}),e=d.split("."),f=c[e.shift()];void 0!==f&&e.length;)f=f[e.shift()];return void 0!==f?f:""})}},toggleFullScreen:function(a){function b(a,b){for(var c=["webkit","moz","ms","o",""],d=0;d0&&b[0]||[],e=c>1&&b[c-1]||{};return Array.isArray(d)||(e instanceof joint.dia.Cell?d=b:d instanceof joint.dia.Cell&&(b.length>1&&b.pop(),d=b)),e instanceof joint.dia.Cell&&(e={}),a.call(this,d,e)}}},parseDOMJSON:function(a,b){for(var c={},d=V.namespace.xmlns,e=b||d,f=document.createDocumentFragment(),g=[a,f,e];g.length>0;){e=g.pop();for(var h=g.pop(),i=g.pop(),j=0,k=i.length;j0);return this.stopBatch("clear"),this},_prepareCell:function(a,b){var c;if(a instanceof Backbone.Model?(c=a.attributes,a.graph||b&&b.dry||(a.graph=this)):c=a,!joint.util.isString(c.type))throw new TypeError("dia.Graph: cell type must be a string.");return a},minZIndex:function(){var a=this.get("cells").first();return a?a.get("z")||0:0},maxZIndex:function(){var a=this.get("cells").last();return a?a.get("z")||0:0},addCell:function(a,b){return Array.isArray(a)?this.addCells(a,b):(a instanceof Backbone.Model?a.has("z")||a.set("z",this.maxZIndex()+1):void 0===a.z&&(a.z=this.maxZIndex()+1),this.get("cells").add(this._prepareCell(a,b),b||{}),this)},addCells:function(a,b){return a.length&&(a=joint.util.flattenDeep(a),b.position=a.length,this.startBatch("add"),a.forEach(function(a){b.position--,this.addCell(a,b)},this),this.stopBatch("add")),this},resetCells:function(a,b){var c=joint.util.toArray(a).map(function(a){return this._prepareCell(a,b)},this);return this.get("cells").reset(c,b),this},removeCells:function(a,b){return a.length&&(this.startBatch("remove"),joint.util.invoke(a,"remove",b),this.stopBatch("remove")),this},_removeCell:function(a,b,c){c=c||{},c.clear||(c.disconnectLinks?this.disconnectLinks(a,c):this.removeLinks(a,c)),this.get("cells").remove(a,{silent:!0}),a.graph===this&&(a.graph=null)},getCell:function(a){return this.get("cells").get(a)},getCells:function(){return this.get("cells").toArray()},getElements:function(){return Object.keys(this._nodes).map(this.getCell,this)},getLinks:function(){return Object.keys(this._edges).map(this.getCell,this)},getFirstCell:function(){return this.get("cells").first()},getLastCell:function(){return this.get("cells").last()},getConnectedLinks:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=[],f={};if(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),b.deep){var g=a.getEmbeddedCells({deep:!0}),h={};g.forEach(function(a){a.isLink()&&(h[a.id]=!0)}),g.forEach(function(a){a.isLink()||(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)))},this)}return e},getNeighbors:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=this.getConnectedLinks(a,b).reduce(function(e,f){var g=f.get("source"),h=f.get("target"),i=f.hasLoop(b);if(c&&joint.util.has(g,"id")&&!e[g.id]){var j=this.getCell(g.id);!i&&(!j||j===a||b.deep&&j.isEmbeddedIn(a))||(e[g.id]=j)}if(d&&joint.util.has(h,"id")&&!e[h.id]){var k=this.getCell(h.id);!i&&(!k||k===a||b.deep&&k.isEmbeddedIn(a))||(e[h.id]=k)}return e}.bind(this),{});return joint.util.toArray(e)},getCommonAncestor:function(){var a=Array.from(arguments).map(function(a){for(var b=[],c=a.get("parent");c;)b.push(c),c=this.getCell(c).get("parent");return b},this);a=a.sort(function(a,b){return a.length-b.length});var b=joint.util.toArray(a.shift()).find(function(b){return a.every(function(a){return a.includes(b)})});return this.getCell(b)},getSuccessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{outbound:!0})),c},cloneCells:function(a){a=joint.util.uniq(a);var b=joint.util.toArray(a).reduce(function(a,b){return a[b.id]=b.clone(),a},{});return joint.util.toArray(a).forEach(function(a){var c=b[a.id];if(c.isLink()){var d=c.get("source"),e=c.get("target");d.id&&b[d.id]&&c.prop("source/id",b[d.id].id),e.id&&b[e.id]&&c.prop("target/id",b[e.id].id)}var f=a.get("parent");f&&b[f]&&c.set("parent",b[f].id);var g=joint.util.toArray(a.get("embeds")).reduce(function(a,c){return b[c]&&a.push(b[c].id),a},[]);joint.util.isEmpty(g)||c.set("embeds",g)}),b},cloneSubgraph:function(a,b){var c=this.getSubgraph(a,b);return this.cloneCells(c)},getSubgraph:function(a,b){b=b||{};var c=[],d={},e=[],f=[];return joint.util.toArray(a).forEach(function(a){if(d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a)),b.deep){var g=a.getEmbeddedCells({deep:!0});g.forEach(function(a){d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a))})}}),f.forEach(function(a){var b=a.get("source"),f=a.get("target");if(b.id&&!d[b.id]){var g=this.getCell(b.id);c.push(g),d[g.id]=g,e.push(g)}if(f.id&&!d[f.id]){var h=this.getCell(f.id);c.push(this.getCell(f.id)), +d[h.id]=h,e.push(h)}},this),e.forEach(function(a){var e=this.getConnectedLinks(a,b);e.forEach(function(a){var b=a.get("source"),e=a.get("target");!d[a.id]&&b.id&&d[b.id]&&e.id&&d[e.id]&&(c.push(a),d[a.id]=a)})},this),c},getPredecessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{inbound:!0})),c},search:function(a,b,c){c=c||{},c.breadthFirst?this.bfs(a,b,c):this.dfs(a,b,c)},bfs:function(a,b,c){c=c||{};var d={},e={},f=[];for(f.push(a),e[a.id]=0;f.length>0;){var g=f.shift();if(!d[g.id]){if(d[g.id]=!0,b(g,e[g.id])===!1)return;this.getNeighbors(g,c).forEach(function(a){e[a.id]=e[g.id]+1,f.push(a)})}}},dfs:function(a,b,c,d,e){c=c||{};var f=d||{},g=e||0;b(a,g)!==!1&&(f[a.id]=!0,this.getNeighbors(a,c).forEach(function(a){f[a.id]||this.dfs(a,b,c,f,g+1)},this))},getSources:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._in[c]&&!joint.util.isEmpty(this._in[c])||a.push(this.getCell(c))}.bind(this)),a},getSinks:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._out[c]&&!joint.util.isEmpty(this._out[c])||a.push(this.getCell(c))}.bind(this)),a},isSource:function(a){return!this._in[a.id]||joint.util.isEmpty(this._in[a.id])},isSink:function(a){return!this._out[a.id]||joint.util.isEmpty(this._out[a.id])},isSuccessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{outbound:!0}),c},isPredecessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{inbound:!0}),c},isNeighbor:function(a,b,c){c=c||{};var d=c.inbound,e=c.outbound;void 0===d&&void 0===e&&(d=e=!0);var f=!1;return this.getConnectedLinks(a,c).forEach(function(a){var c=a.get("source"),g=a.get("target");return d&&joint.util.has(c,"id")&&c.id===b.id?(f=!0,!1):e&&joint.util.has(g,"id")&&g.id===b.id?(f=!0,!1):void 0}),f},disconnectLinks:function(a,b){this.getConnectedLinks(a).forEach(function(c){c.set(c.get("source").id===a.id?"source":"target",{x:0,y:0},b)})},removeLinks:function(a,b){joint.util.invoke(this.getConnectedLinks(a),"remove",b)},findModelsFromPoint:function(a){return this.getElements().filter(function(b){return b.getBBox().containsPoint(a)})},findModelsInArea:function(a,b){a=g.rect(a),b=joint.util.defaults(b||{},{strict:!1});var c=b.strict?"containsRect":"intersect";return this.getElements().filter(function(b){return a[c](b.getBBox())})},findModelsUnderElement:function(a,b){b=joint.util.defaults(b||{},{searchBy:"bbox"});var c=a.getBBox(),d="bbox"===b.searchBy?this.findModelsInArea(c):this.findModelsFromPoint(c[b.searchBy]());return d.filter(function(b){return a.id!==b.id&&!b.isEmbeddedIn(a)})},getBBox:function(a,b){return this.getCellsBBox(a||this.getElements(),b)},getCellsBBox:function(a,b){return joint.util.toArray(a).reduce(function(a,c){return c.isLink()?a:a?a.union(c.getBBox(b)):c.getBBox(b)},null)},translate:function(a,b,c){var d=this.getCells().filter(function(a){return!a.isEmbedded()});return joint.util.invoke(d,"translate",a,b,c),this},resize:function(a,b,c){return this.resizeCells(a,b,this.getCells(),c)},resizeCells:function(a,b,c,d){var e=this.getCellsBBox(c);if(e){var f=Math.max(a/e.width,0),g=Math.max(b/e.height,0);joint.util.invoke(c,"scale",f,g,e.origin(),d)}return this},startBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)+1,this.trigger("batch:start",joint.util.assign({},b,{batchName:a}))},stopBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)-1,this.trigger("batch:stop",joint.util.assign({},b,{batchName:a}))},hasActiveBatch:function(a){return 0===arguments.length?joint.util.toArray(this._batches).some(function(a){return a>0}):Array.isArray(a)?a.some(function(a){return!!this._batches[a]},this):!!this._batches[a]}},{validations:{multiLinks:function(a,b){var c=b.get("source"),d=b.get("target");if(c.id&&d.id){var e=b.getSourceElement();if(e){var f=a.getConnectedLinks(e,{outbound:!0}),g=f.filter(function(a){var b=a.get("source"),e=a.get("target");return b&&b.id===c.id&&(!b.port||b.port===c.port)&&e&&e.id===d.id&&(!e.port||e.port===d.port)});if(g.length>1)return!1}}return!0},linkPinning:function(a,b){return b.source().id&&b.target().id}}}),joint.util.wrapWith(joint.dia.Graph.prototype,["resetCells","addCells","removeCells"],"cells"),function(a,b,c,d,e){function f(a,b){return function(c,d){var f=e.isPercentage(c);c=parseFloat(c),f&&(c/=100);var g={};if(isFinite(c)){var h=f||c>=0&&c<=1?c*d[b]:Math.max(c+d[b],0);g[a]=h}return g}}function g(a,b,d){return function(f,g){var h=e.isPercentage(f);f=parseFloat(f),h&&(f/=100);var i;if(isFinite(f)){var j=g[d]();i=h||f>0&&f<1?j[a]+g[b]*f:j[a]+f}var k=c.Point();return k[a]=i||0,k}}function h(a,b,d){return function(f,g){var h;h="middle"===f?g[b]/2:f===d?g[b]:isFinite(f)?f>-1&&f<1?-g[b]*f:-f:e.isPercentage(f)?g[b]*parseFloat(f)/100:0;var i=c.Point();return i[a]=-(g[a]+h),i}}function i(a,b){var c="joint-shape",e=b&&b.resetOffset;return function(b,f,g){var h=d(g),i=h.data(c);if(!i||i.value!==b){var j=a(b);i={value:b,shape:j,shapeBBox:j.bbox()},h.data(c,i)}var k=i.shape.clone(),l=i.shapeBBox.clone(),m=l.origin(),n=f.origin();l.x=n.x,l.y=n.y;var o=f.maxRectScaleToFit(l,n),p=0===l.width||0===f.width?1:o.sx,q=0===l.height||0===f.height?1:o.sy;return k.scale(p,q,m),e&&k.translate(-m.x,-m.y),k}}function j(a){function d(a){return new c.Path(b.normalizePathData(a))}var e=i(d,a);return function(a,b,c){var d=e(a,b,c);return{d:d.serialize()}}}function k(a){var b=i(c.Polyline,a);return function(a,c,d){var e=b(a,c,d);return{points:e.serialize()}}}function l(a,b){var d=new c.Point(1,0);return function(c){var e,f,g=this[a](c);return g?(f=b.rotate?g.vector().vectorAngle(d):0,e=g.start):(e=this.path.start,f=0),0===f?{transform:"translate("+e.x+","+e.y+")"}:{transform:"translate("+e.x+","+e.y+") rotate("+f+")"}}}function m(a,b,c){return void 0!==c.text}function n(){return this instanceof a.dia.LinkView}function o(a){var b={},c=a.stroke;"string"==typeof c&&(b.stroke=c,b.fill=c);var d=a.strokeOpacity;return void 0===d&&(d=a["stroke-opacity"]),void 0===d&&(d=a.opacity),void 0!==d&&(b["stroke-opacity"]=d,b["fill-opacity"]=d),b}var p=a.dia.attributes={xlinkHref:{set:"xlink:href"},xlinkShow:{set:"xlink:show"},xlinkRole:{set:"xlink:role"},xlinkType:{set:"xlink:type"},xlinkArcrole:{set:"xlink:arcrole"},xlinkTitle:{set:"xlink:title"},xlinkActuate:{set:"xlink:actuate"},xmlSpace:{set:"xml:space"},xmlBase:{set:"xml:base"},xmlLang:{set:"xml:lang"},preserveAspectRatio:{set:"preserveAspectRatio"},requiredExtension:{set:"requiredExtension"},requiredFeatures:{set:"requiredFeatures"},systemLanguage:{set:"systemLanguage"},externalResourcesRequired:{set:"externalResourceRequired"},filter:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineFilter(a)+")"}},fill:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},stroke:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},sourceMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),a),{"marker-start":"url(#"+this.paper.defineMarker(a)+")"}}},targetMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),{transform:"rotate(180)"},a),{"marker-end":"url(#"+this.paper.defineMarker(a)+")"}}},vertexMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),a),{"marker-mid":"url(#"+this.paper.defineMarker(a)+")"}}},text:{qualify:function(a,b,c){return!c.textWrap||!e.isPlainObject(c.textWrap)},set:function(c,f,g,h){var i=d(g),j="joint-text",k=i.data(j),l=a.util.pick(h,"lineHeight","annotations","textPath","x","textVerticalAnchor","eol"),m=l.fontSize=h["font-size"]||h.fontSize,n=JSON.stringify([c,l]);if(void 0===k||k!==n){m&&g.setAttribute("font-size",m);var o=l.textPath;if(e.isObject(o)){var p=o.selector;if("string"==typeof p){var q=this.findBySelector(p)[0];q instanceof SVGPathElement&&(l.textPath=e.assign({"xlink:href":"#"+q.id},o))}}b(g).text(""+c,l),i.data(j,n)}}},textWrap:{qualify:e.isPlainObject,set:function(b,c,d,f){var g=b.width||0;e.isPercentage(g)?c.width*=parseFloat(g)/100:g<=0?c.width+=g:c.width=g;var h=b.height||0;e.isPercentage(h)?c.height*=parseFloat(h)/100:h<=0?c.height+=h:c.height=h;var i=b.text;if(void 0===i&&(i=attr.text),void 0!==i)var j=a.util.breakText(""+i,c,{"font-weight":f["font-weight"]||f.fontWeight,"font-size":f["font-size"]||f.fontSize,"font-family":f["font-family"]||f.fontFamily,lineHeight:f.lineHeight},{svgDocument:this.paper.svg});a.dia.attributes.text.set.call(this,j,c,d,f)}},title:{qualify:function(a,b){return b instanceof SVGElement},set:function(a,b,c){var e=d(c),f="joint-title",g=e.data(f);if(void 0===g||g!==a){e.data(f,a);var h=c.firstChild;if(h&&"TITLE"===h.tagName.toUpperCase())h.textContent=a;else{var i=document.createElementNS(c.namespaceURI,"title");i.textContent=a,c.insertBefore(i,h)}}}},lineHeight:{qualify:m},textVerticalAnchor:{qualify:m},textPath:{qualify:m},annotations:{qualify:m},port:{set:function(a){return null===a||void 0===a.id?a:a.id}},style:{qualify:e.isPlainObject,set:function(a,b,c){d(c).css(a)}},html:{set:function(a,b,c){d(c).html(a+"")}},ref:{},refX:{position:g("x","width","origin")},refY:{position:g("y","height","origin")},refDx:{position:g("x","width","corner")},refDy:{position:g("y","height","corner")},refWidth:{set:f("width","width")},refHeight:{set:f("height","height")},refRx:{set:f("rx","width")},refRy:{set:f("ry","height")},refRInscribed:{set:function(a){var b=f(a,"width"),c=f(a,"height");return function(a,d){var e=d.height>d.width?b:c;return e(a,d)}}("r")},refRCircumscribed:{set:function(a,b){var c=e.isPercentage(a);a=parseFloat(a),c&&(a/=100);var d,f=Math.sqrt(b.height*b.height+b.width*b.width);return isFinite(a)&&(d=c||a>=0&&a<=1?a*f:Math.max(a+f,0)),{r:d}}},refCx:{set:f("cx","width")},refCy:{set:f("cy","height")},xAlignment:{offset:h("x","width","right")},yAlignment:{offset:h("y","height","bottom")},resetOffset:{offset:function(a,b){return a?{x:-b.x,y:-b.y}:{x:0,y:0}}},refDResetOffset:{set:j({resetOffset:!0})},refDKeepOffset:{set:j({resetOffset:!1})},refPointsResetOffset:{set:k({resetOffset:!0})},refPointsKeepOffset:{set:k({resetOffset:!1})},connection:{qualify:n,set:function(){return{d:this.getSerializedConnection()}}},atConnectionLengthKeepGradient:{qualify:n,set:l("getTangentAtLength",{rotate:!0})},atConnectionLengthIgnoreGradient:{qualify:n,set:l("getTangentAtLength",{rotate:!1})},atConnectionRatioKeepGradient:{qualify:n,set:l("getTangentAtRatio",{rotate:!0})},atConnectionRatioIgnoreGradient:{qualify:n,set:l("getTangentAtRatio",{rotate:!1})}};p.refR=p.refRInscribed,p.refD=p.refDResetOffset,p.refPoints=p.refPointsResetOffset,p.atConnectionLength=p.atConnectionLengthKeepGradient,p.atConnectionRatio=p.atConnectionRatioKeepGradient,p.refX2=p.refX,p.refY2=p.refY,p["ref-x"]=p.refX,p["ref-y"]=p.refY,p["ref-dy"]=p.refDy,p["ref-dx"]=p.refDx,p["ref-width"]=p.refWidth,p["ref-height"]=p.refHeight,p["x-alignment"]=p.xAlignment,p["y-alignment"]=p.yAlignment}(joint,V,g,$,joint.util),function(a,b){var c=a.mvc.View.extend({name:null,tagName:"g",className:"tool",svgElement:!0,_visible:!0,init:function(){var a=this.name;a&&this.vel.attr("data-tool-name",a)},configure:function(a,b){return this.relatedView=a,this.paper=a.paper,this.parentView=b,this.simulateRelatedView(this.el),this},simulateRelatedView:function(a){a&&a.setAttribute("model-id",this.relatedView.model.id)},getName:function(){return this.name},show:function(){this.el.style.display="",this._visible=!0},hide:function(){this.el.style.display="none",this._visible=!1},isVisible:function(){return!!this._visible},focus:function(){var a=this.options.focusOpacity;isFinite(a)&&(this.el.style.opacity=a),this.parentView.focusTool(this)},blur:function(){this.el.style.opacity="",this.parentView.blurTool(this)},update:function(){}}),d=a.mvc.View.extend({tagName:"g",className:"tools",svgElement:!0,tools:null,options:{tools:null,relatedView:null,name:null,component:!1},configure:function(d){d=b.assign(this.options,d);var e=d.tools;if(!Array.isArray(e))return this;var f=d.relatedView;if(!(f instanceof a.dia.CellView))return this;for(var g=this.tools=[],h=0,i=e.length;h0;){var d=c.shift();b.push(d),c.push.apply(c,d.getEmbeddedCells())}}else b=this.getEmbeddedCells(),b.forEach(function(c){b.push.apply(b,c.getEmbeddedCells(a))});else b=joint.util.toArray(this.get("embeds")).map(this.graph.getCell,this.graph);return b}return[]},isEmbeddedIn:function(a,b){var c=joint.util.isString(a)?a:a.id,d=this.parent();if(b=joint.util.defaults({deep:!0},b),this.graph&&b.deep){for(;d;){if(d===c)return!0;d=this.graph.getCell(d).parent()}return!1}return d===c},isEmbedded:function(){return!!this.parent()},clone:function(a){if(a=a||{},a.deep)return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null,[this].concat(this.getEmbeddedCells({deep:!0}))));var b=Backbone.Model.prototype.clone.apply(this,arguments);return b.set("id",joint.util.uuid()),b.unset("embeds"),b.unset("parent"),b},prop:function(a,b,c){var d="/",e=joint.util.isString(a);if(e||Array.isArray(a)){if(arguments.length>1){var f,g;e?(f=a,g=f.split("/")):(f=a.join(d),g=a.slice());var h=g[0],i=g.length;if(c=c||{},c.propertyPath=f,c.propertyValue=b,c.propertyPathArray=g,1===i)return this.set(h,b,c);for(var j={},k=j,l=h,m=1;m0)},getSelector:function(a,b){if(a===this.el)return b;var c;if(a){var d=V(a).index()+1;c=a.tagName+":nth-child("+d+")",b&&(c+=" > "+b),c=this.getSelector(a.parentNode,c)}return c},getLinkEnd:function(a,b,c,d,e){var f=this.model,h=f.id,i=this.findAttribute("port",a),j=a.getAttribute("joint-selector"),k={id:h};null!=j&&(k.magnet=j),null!=i?(k.port=i,f.hasPort(i)||j||(k.selector=this.getSelector(a))):null==j&&this.el!==a&&(k.selector=this.getSelector(a));var l=this.paper,m=l.options.connectionStrategy;if("function"==typeof m){var n=m.call(l,k,this,a,new g.Point(b,c),d,e);n&&(k=n)}return k},getMagnetFromLinkEnd:function(a){var b,c=this.el,d=a.port,e=a.magnet;return b=null!=d&&this.model.hasPort(d)?this.findPortNode(d,e)||c:this.findBySelector(e||a.selector,c,this.selectors)[0]},findAttribute:function(a,b){if(!b)return null;var c=b.getAttribute(a);if(null===c){if(b===this.el)return null;for(var d=b.parentNode;d&&d!==this.el&&1===d.nodeType&&(c=d.getAttribute(a),null===c);)d=d.parentNode}return c},getAttributeDefinition:function(a){return this.model.constructor.getAttributeDefinition(a)},setNodeAttributes:function(a,b){joint.util.isEmpty(b)||(a instanceof SVGElement?V(a).attr(b):$(a).attr(b))},processNodeAttributes:function(a,b){var c,d,e,f,g,h,i,j,k,l=[];for(c in b)b.hasOwnProperty(c)&&(d=b[c],e=this.getAttributeDefinition(c),!e||joint.util.isFunction(e.qualify)&&!e.qualify.call(this,d,a,b)?(h||(h={}),h[joint.util.toKebabCase(c)]=d):(joint.util.isString(e.set)&&(h||(h={}),h[e.set]=d),null!==d&&l.push(c,e)));for(f=0,g=l.length;f0&&x.height>0){var y=V.transformRect(a.getBBox(),p).scale(1/r,1/s);for(e in m)f=m[e],h=this.getAttributeDefinition(e),t=h.offset.call(this,f,y,a,i),t&&(q.offset(g.Point(t).scale(r,s)),w||(w=!0))}}(void 0!==o||v||w)&&(q.round(1),p.e=q.x,p.f=q.y,a.setAttribute("transform",V.matrixToTransformString(p)))},getNodeScale:function(a,b){var c,d;if(b&&b.contains(a)){var e=b.scale();c=1/e.sx,d=1/e.sy}else c=1,d=1;return{sx:c,sy:d}},findNodesAttributes:function(a,b,c,d){var e={};for(var f in a)if(a.hasOwnProperty(f))for(var g=c[f]=this.findBySelector(f,b,d),h=0,i=g.length;h-1?l.splice(t,0,d):l.push(d)}else this.setNodeAttributes(e,i.normal);for(var u=0,v=l.length;u0){this.startBatch("fit-embeds",a),a.deep&&joint.util.invoke(b,"fitEmbeds",a);var c=this.graph.getCellsBBox(b),d=joint.util.normalizeSides(a.padding);c.moveAndExpand({x:-d.left,y:-d.top,width:d.right+d.left,height:d.bottom+d.top}),this.set({position:{x:c.x,y:c.y},size:{width:c.width,height:c.height}},a),this.stopBatch("fit-embeds")}return this},rotate:function(a,b,c,d){if(c){var e=this.getBBox().center(),f=this.get("size"),g=this.get("position");e.rotate(c,this.get("angle")-a);var h=e.x-f.width/2-g.x,i=e.y-f.height/2-g.y;this.startBatch("rotate",{angle:a,absolute:b,origin:c}),this.position(g.x+h,g.y+i,d),this.rotate(a,b,null,d),this.stopBatch("rotate")}else this.set("angle",b?a:(this.get("angle")+a)%360,d);return this},angle:function(){return g.normalizeAngle(this.get("angle")||0)},getBBox:function(a){if(a=a||{},a.deep&&this.graph){var b=this.getEmbeddedCells({deep:!0,breadthFirst:!0});return b.push(this),this.graph.getCellsBBox(b)}var c=this.get("position"),d=this.get("size");return new g.Rect(c.x,c.y,d.width,d.height)}}),joint.dia.ElementView=joint.dia.CellView.extend({_removePorts:function(){},_renderPorts:function(){},className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("element"),a.join(" ")},metrics:null,initialize:function(){joint.dia.CellView.prototype.initialize.apply(this,arguments);var a=this.model;this.listenTo(a,"change:position",this.translate),this.listenTo(a,"change:size",this.resize),this.listenTo(a,"change:angle",this.rotate),this.listenTo(a,"change:markup",this.render),this._initializePorts(),this.metrics={}},_initializePorts:function(){},update:function(a,b){this.metrics={},this._removePorts();var c=this.model,d=c.attr();this.updateDOMSubtreeAttributes(this.el,d,{rootBBox:new g.Rect(c.size()),selectors:this.selectors,scalableNode:this.scalableNode,rotatableNode:this.rotatableNode,roAttributes:b===d?null:b}),this._renderPorts()},rotatableSelector:"rotatable",scalableSelector:"scalable",scalableNode:null,rotatableNode:null,renderMarkup:function(){var a=this.model,b=a.get("markup")||a.markup;if(!b)throw new Error("dia.ElementView: markup required");if(Array.isArray(b))return this.renderJSONMarkup(b);if("string"==typeof b)return this.renderStringMarkup(b);throw new Error("dia.ElementView: invalid markup")},renderJSONMarkup:function(a){var b=joint.util.parseDOMJSON(a),c=this.selectors=b.selectors,d=this.selector;if(c[d])throw new Error("dia.ElementView: ambiguous root selector.");c[d]=this.el,this.rotatableNode=V(c[this.rotatableSelector])||null,this.scalableNode=V(c[this.scalableSelector])||null,this.vel.append(b.fragment)},renderStringMarkup:function(a){var b=this.vel;b.append(V(a)),this.rotatableNode=b.findOne(".rotatable"),this.scalableNode=b.findOne(".scalable");var c=this.selectors={};c[this.selector]=this.el},render:function(){return this.vel.empty(),this.renderMarkup(),this.scalableNode&&this.update(),this.resize(),this.rotatableNode?(this.rotate(),this.translate(),this):(this.updateTransformation(),this)},resize:function(){return this.scalableNode?this.sgResize.apply(this,arguments):(this.model.attributes.angle&&this.rotate(),void this.update())},translate:function(){return this.rotatableNode?this.rgTranslate():void this.updateTransformation()},rotate:function(){return this.rotatableNode?this.rgRotate():void this.updateTransformation()},updateTransformation:function(){var a=this.getTranslateString(),b=this.getRotateString();b&&(a+=" "+b),this.vel.attr("transform",a)},getTranslateString:function(){var a=this.model.attributes.position;return"translate("+a.x+","+a.y+")"},getRotateString:function(){var a=this.model.attributes,b=a.angle;if(!b)return null;var c=a.size;return"rotate("+b+","+c.width/2+","+c.height/2+")"},getBBox:function(a){var b;if(a&&a.useModelGeometry){var c=this.model;b=c.getBBox().bbox(c.angle())}else b=this.getNodeBBox(this.el);return this.paper.localToPaperRect(b)},nodeCache:function(a){var b=V.ensureId(a),c=this.metrics[b];return c||(c=this.metrics[b]={}),c},getNodeData:function(a){var b=this.nodeCache(a);return b.data||(b.data={}),b.data},getNodeBBox:function(a){var b=this.getNodeBoundingRect(a),c=this.getNodeMatrix(a),d=this.getRootTranslateMatrix(),e=this.getRootRotateMatrix();return V.transformRect(b,d.multiply(e).multiply(c))},getNodeBoundingRect:function(a){var b=this.nodeCache(a);return void 0===b.boundingRect&&(b.boundingRect=V(a).getBBox()),new g.Rect(b.boundingRect)},getNodeUnrotatedBBox:function(a){var b=this.getNodeBoundingRect(a),c=this.getNodeMatrix(a),d=this.getRootTranslateMatrix();return V.transformRect(b,d.multiply(c))},getNodeShape:function(a){var b=this.nodeCache(a);return void 0===b.geometryShape&&(b.geometryShape=V(a).toGeometryShape()),b.geometryShape.clone()},getNodeMatrix:function(a){var b=this.nodeCache(a);if(void 0===b.magnetMatrix){var c=this.rotatableNode||this.el;b.magnetMatrix=V(a).getTransformToElement(c)}return V.createSVGMatrix(b.magnetMatrix)},getRootTranslateMatrix:function(){var a=this.model,b=a.position(),c=V.createSVGMatrix().translate(b.x,b.y);return c},getRootRotateMatrix:function(){var a=V.createSVGMatrix(),b=this.model,c=b.angle();if(c){var d=b.getBBox(),e=d.width/2,f=d.height/2;a=a.translate(e,f).rotate(c).translate(-e,-f)}return a},rgRotate:function(){this.rotatableNode.attr("transform",this.getRotateString())},rgTranslate:function(){this.vel.attr("transform",this.getTranslateString())},sgResize:function(a,b,c){var d=this.model,e=d.get("angle")||0,f=d.get("size")||{width:1,height:1},g=this.scalableNode,h=!1;g.node.getElementsByTagName("path").length>0&&(h=!0);var i=g.getBBox({recursive:h}),j=f.width/(i.width||1),k=f.height/(i.height||1);g.attr("transform","scale("+j+","+k+")");var l=this.rotatableNode,m=l&&l.attr("transform");if(m&&null!==m){l.attr("transform",m+" rotate("+-e+","+f.width/2+","+f.height/2+")");var n=g.getBBox({target:this.paper.viewport});d.set("position",{x:n.x,y:n.y},c),this.rotate()}this.update()},prepareEmbedding:function(a){a||(a={});var b=a.model||this.model,c=a.paper||this.paper,d=c.model;b.startBatch("to-front"),b.toFront({deep:!0,ui:!0});var e=d.get("cells").max("z").get("z"),f=d.getConnectedLinks(b,{deep:!0});joint.util.invoke(f,"set","z",e+1,{ui:!0}),b.stopBatch("to-front");var g=b.parent();g&&d.getCell(g).unembed(b,{ui:!0})},processEmbedding:function(a){a||(a={});var b=a.model||this.model,c=a.paper||this.paper,d=c.options,e=[];if(joint.util.isFunction(d.findParentBy)){var f=joint.util.toArray(d.findParentBy.call(c.model,this));e=f.filter(function(a){return a instanceof joint.dia.Cell&&this.model.id!==a.id&&!a.isEmbeddedIn(this.model)}.bind(this))}else e=c.model.findModelsUnderElement(b,{searchBy:d.findParentBy});d.frontParentOnly&&(e=e.slice(-1));for(var g=null,h=a.candidateEmbedView,i=e.length-1;i>=0;i--){var j=e[i];if(h&&h.model.id==j.id){g=h;break}var k=j.findView(c);if(d.validateEmbedding.call(c,this,k)){g=k;break}}g&&g!=h&&(this.clearEmbedding(a),a.candidateEmbedView=g.highlight(null,{embedding:!0})),!g&&h&&this.clearEmbedding(a)},clearEmbedding:function(a){a||(a={});var b=a.candidateEmbedView;b&&(b.unhighlight(null,{embedding:!0}),a.candidateEmbedView=null)},finalizeEmbedding:function(a){a||(a={});var b=a.candidateEmbedView,c=a.model||this.model,d=a.paper||this.paper;b&&(b.model.embed(c,{ui:!0}),b.unhighlight(null,{embedding:!0}),a.candidateEmbedView=null),joint.util.invoke(d.model.getConnectedLinks(c,{deep:!0}),"reparent",{ui:!0})},pointerdblclick:function(a,b,c){joint.dia.CellView.prototype.pointerdblclick.apply(this,arguments),this.notify("element:pointerdblclick",a,b,c)},pointerclick:function(a,b,c){joint.dia.CellView.prototype.pointerclick.apply(this,arguments),this.notify("element:pointerclick",a,b,c)},contextmenu:function(a,b,c){joint.dia.CellView.prototype.contextmenu.apply(this,arguments),this.notify("element:contextmenu",a,b,c)},pointerdown:function(a,b,c){joint.dia.CellView.prototype.pointerdown.apply(this,arguments),this.notify("element:pointerdown",a,b,c),this.dragStart(a,b,c)},pointermove:function(a,b,c){var d=this.eventData(a);switch(d.action){case"move":this.drag(a,b,c);break;case"magnet":this.dragMagnet(a,b,c)}d.stopPropagation||(joint.dia.CellView.prototype.pointermove.apply(this,arguments),this.notify("element:pointermove",a,b,c)),this.eventData(a,d)},pointerup:function(a,b,c){var d=this.eventData(a);switch(d.action){case"move":this.dragEnd(a,b,c);break;case"magnet":return void this.dragMagnetEnd(a,b,c)}d.stopPropagation||(this.notify("element:pointerup",a,b,c),joint.dia.CellView.prototype.pointerup.apply(this,arguments))},mouseover:function(a){joint.dia.CellView.prototype.mouseover.apply(this,arguments),this.notify("element:mouseover",a)},mouseout:function(a){joint.dia.CellView.prototype.mouseout.apply(this,arguments),this.notify("element:mouseout",a)},mouseenter:function(a){joint.dia.CellView.prototype.mouseenter.apply(this,arguments),this.notify("element:mouseenter",a)},mouseleave:function(a){joint.dia.CellView.prototype.mouseleave.apply(this,arguments),this.notify("element:mouseleave",a)},mousewheel:function(a,b,c,d){joint.dia.CellView.prototype.mousewheel.apply(this,arguments),this.notify("element:mousewheel",a,b,c,d)},onmagnet:function(a,b,c){this.dragMagnetStart(a,b,c);var d=this.eventData(a).stopPropagation;d&&a.stopPropagation()},dragStart:function(a,b,c){this.can("elementMove")&&this.eventData(a,{action:"move",x:b,y:c,restrictedArea:this.paper.getRestrictedArea(this)})},dragMagnetStart:function(a,b,c){if(this.can("addLinkFromMagnet")){this.model.startBatch("add-link");var d=this.paper,e=d.model,f=a.target,g=d.getDefaultLink(this,f),h=this.getLinkEnd(f,b,c,g,"source"),i={x:b,y:c};g.set({source:h,target:i}),g.addTo(e,{async:!1,ui:!0});var j=g.findView(d);joint.dia.CellView.prototype.pointerdown.apply(j,arguments),j.notify("link:pointerdown",a,b,c);var k=j.startArrowheadMove("target",{whenNotAllowed:"remove"});j.eventData(a,k),this.eventData(a,{action:"magnet",linkView:j,stopPropagation:!0}),this.paper.delegateDragEvents(this,a.data)}},drag:function(a,b,c){var d=this.paper,e=d.options.gridSize,f=this.model,h=f.position(),i=this.eventData(a),j=g.snapToGrid(h.x,e)-h.x+g.snapToGrid(b-i.x,e),k=g.snapToGrid(h.y,e)-h.y+g.snapToGrid(c-i.y,e);f.translate(j,k,{restrictedArea:i.restrictedArea,ui:!0});var l=!!i.embedding;d.options.embeddingMode&&(l||(this.prepareEmbedding(i),l=!0),this.processEmbedding(i)),this.eventData(a,{x:g.snapToGrid(b,e),y:g.snapToGrid(c,e),embedding:l})},dragMagnet:function(a,b,c){var d=this.eventData(a),e=d.linkView;e&&e.pointermove(a,b,c)},dragEnd:function(a,b,c){var d=this.eventData(a);d.embedding&&this.finalizeEmbedding(d)},dragMagnetEnd:function(a,b,c){var d=this.eventData(a),e=d.linkView;e&&e.pointerup(a,b,c),this.model.stopBatch("add-link")}}),joint.dia.Link=joint.dia.Cell.extend({markup:['','','','','','','',''].join(""),toolMarkup:['','','','',"Remove link.","",'','','',"Link options.","",""].join(""),doubleToolMarkup:void 0,vertexMarkup:['','','','',"Remove vertex.","",""].join(""),arrowheadMarkup:['','',""].join(""),defaultLabel:void 0,labelMarkup:void 0,_builtins:{defaultLabel:{markup:[{tagName:"rect",selector:"rect"},{tagName:"text",selector:"text"}],attrs:{text:{fill:"#000000",fontSize:14,textAnchor:"middle",yAlignment:"middle",pointerEvents:"none"},rect:{ref:"text",fill:"#ffffff",rx:3,ry:3,refWidth:1,refHeight:1,refX:0,refY:0}},position:{distance:.5}}},defaults:{type:"link",source:{},target:{}},isLink:function(){return!0},disconnect:function(a){return this.set({source:{x:0,y:0},target:{x:0,y:0}},a)},source:function(a,b,c){if(void 0===a)return joint.util.clone(this.get("source"));var d,e,f=a instanceof joint.dia.Cell;if(f)return d=joint.util.clone(b)||{},d.id=a.id,e=c,this.set("source",d,e);var h=a instanceof g.Point;return h?(d=joint.util.clone(b)||{},d.x=a.x,d.y=a.y,e=c,this.set("source",d,e)):(d=a,e=b,this.set("source",d,e))},target:function(a,b,c){if(void 0===a)return joint.util.clone(this.get("target"));var d,e,f=a instanceof joint.dia.Cell;if(f)return d=joint.util.clone(b)||{},d.id=a.id,e=c,this.set("target",d,e);var h=a instanceof g.Point;return h?(d=joint.util.clone(b)||{},d.x=a.x,d.y=a.y,e=c,this.set("target",d,e)):(d=a,e=b,this.set("target",d,e))},router:function(a,b,c){if(void 0===a)return router=this.get("router"),router?"object"==typeof router?joint.util.clone(router):router:this.get("manhattan")?{name:"orthogonal"}:null;var d="object"==typeof a||"function"==typeof a,e=d?a:{name:a,args:b},f=d?b:c;return this.set("router",e,f)},connector:function(a,b,c){if(void 0===a)return connector=this.get("connector"),connector?"object"==typeof connector?joint.util.clone(connector):connector:this.get("smooth")?{name:"smooth"}:null;var d="object"==typeof a||"function"==typeof a,e=d?a:{name:a,args:b},f=d?b:c;return this.set("connector",e,f)},label:function(a,b,c){var d=this.labels();return a=isFinite(a)&&null!==a?0|a:0,a<0&&(a=d.length+a),arguments.length<=1?this.prop(["labels",a]):this.prop(["labels",a],b,c)},labels:function(a,b){return 0===arguments.length?(a=this.get("labels"),Array.isArray(a)?a.slice():[]):(Array.isArray(a)||(a=[]),this.set("labels",a,b))},insertLabel:function(a,b,c){if(!b)throw new Error("dia.Link: no label provided");var d=this.labels(),e=d.length;return a=isFinite(a)&&null!==a?0|a:e,a<0&&(a=e+a+1),d.splice(a,0,b),this.labels(d,c)},appendLabel:function(a,b){return this.insertLabel(-1,a,b)},removeLabel:function(a,b){var c=this.labels();return a=isFinite(a)&&null!==a?0|a:-1,c.splice(a,1),this.labels(c,b)},vertex:function(a,b,c){var d=this.vertices();return a=isFinite(a)&&null!==a?0|a:0,a<0&&(a=d.length+a),arguments.length<=1?this.prop(["vertices",a]):this.prop(["vertices",a],b,c)},vertices:function(a,b){return 0===arguments.length?(a=this.get("vertices"),Array.isArray(a)?a.slice():[]):(Array.isArray(a)||(a=[]),this.set("vertices",a,b))},insertVertex:function(a,b,c){if(!b)throw new Error("dia.Link: no vertex provided");var d=this.vertices(),e=d.length;return a=isFinite(a)&&null!==a?0|a:e,a<0&&(a=e+a+1),d.splice(a,0,b),this.vertices(d,c)},removeVertex:function(a,b){var c=this.vertices();return a=isFinite(a)&&null!==a?0|a:-1,c.splice(a,1),this.vertices(c,b)},translate:function(a,b,c){return c=c||{},c.translateBy=c.translateBy||this.id,c.tx=a,c.ty=b,this.applyToPoints(function(c){return{x:(c.x||0)+a,y:(c.y||0)+b}},c)},scale:function(a,b,c,d){return this.applyToPoints(function(d){return g.point(d).scale(a,b,c).toJSON()},d)},applyToPoints:function(a,b){if(!joint.util.isFunction(a))throw new TypeError("dia.Link: applyToPoints expects its first parameter to be a function.");var c={},d=this.source();d.id||(c.source=a(d));var e=this.target();e.id||(c.target=a(e));var f=this.vertices();return f.length>0&&(c.vertices=f.map(a)),this.set(c,b)},reparent:function(a){var b;if(this.graph){var c=this.getSourceElement(),d=this.getTargetElement(),e=this.getParentCell();c&&d&&(b=this.graph.getCommonAncestor(c,d)),!e||b&&b.id===e.id||e.unembed(this,a),b&&b.embed(this,a)}return b},hasLoop:function(a){a=a||{};var b=this.source().id,c=this.target().id;if(!b||!c)return!1;var d=b===c;if(!d&&a.deep&&this.graph){var e=this.getSourceElement(),f=this.getTargetElement();d=e.isEmbeddedIn(f)||f.isEmbeddedIn(e)}return d},getSourceElement:function(){var a=this.source(),b=this.graph;return a&&a.id&&b&&b.getCell(a.id)||null},getTargetElement:function(){var a=this.target(),b=this.graph;return a&&a.id&&b&&b.getCell(a.id)||null},getRelationshipAncestor:function(){var a;if(this.graph){var b=[this,this.getSourceElement(),this.getTargetElement()].filter(function(a){return!!a});a=this.graph.getCommonAncestor.apply(this.graph,b)}return a||null},isRelationshipEmbeddedIn:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a.id,c=this.getRelationshipAncestor();return!!c&&(c.id===b||c.isEmbeddedIn(b))},_getDefaultLabel:function(){var a=this.get("defaultLabel")||this.defaultLabel||{},b={};return b.markup=a.markup||this.get("labelMarkup")||this.labelMarkup,b.position=a.position,b.attrs=a.attrs,b.size=a.size,b}},{endsEqual:function(a,b){var c=a.port===b.port||!a.port&&!b.port;return a.id===b.id&&c}}),joint.dia.LinkView=joint.dia.CellView.extend({className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("link"),a.join(" ")},options:{shortLinkLength:105,doubleLinkTools:!1,longLinkLength:155,linkToolsOffset:40,doubleLinkToolsOffset:65,sampleInterval:50},_labelCache:null,_labelSelectors:null,_markerCache:null,_V:null,_dragData:null,metrics:null,decimalsRounding:2,initialize:function(a){joint.dia.CellView.prototype.initialize.apply(this,arguments),"function"!=typeof this.constructor.prototype.watchSource&&(this.constructor.prototype.watchSource=this.createWatcher("source"),this.constructor.prototype.watchTarget=this.createWatcher("target")),this._labelCache={},this._labelSelectors={},this._markerCache={},this._V={},this.metrics={},this.startListening()},startListening:function(){var a=this.model;this.listenTo(a,"change:markup",this.render),this.listenTo(a,"change:smooth change:manhattan change:router change:connector",this.update),this.listenTo(a,"change:toolMarkup",this.onToolsChange),this.listenTo(a,"change:labels change:labelMarkup",this.onLabelsChange),this.listenTo(a,"change:vertices change:vertexMarkup",this.onVerticesChange),this.listenTo(a,"change:source",this.onSourceChange),this.listenTo(a,"change:target",this.onTargetChange)},onSourceChange:function(a,b,c){this.watchSource(a,b);var d=this.model;c.translateBy&&d.get("target").id&&b.id||this.update(d,null,c)},onTargetChange:function(a,b,c){this.watchTarget(a,b);var d=this.model;(!c.translateBy||d.get("source").id&&!b.id&&joint.util.isEmpty(d.get("vertices")))&&this.update(d,null,c)},onVerticesChange:function(a,b,c){this.renderVertexMarkers(),c.translateBy&&c.translateBy!==this.model.id||this.update(a,null,c)},onToolsChange:function(){this.renderTools().updateToolsPosition()},onLabelsChange:function(a,b,c){var d=!0,e=this.model.previous("labels");if(e&&"propertyPathArray"in c&&"propertyValue"in c){var f=c.propertyPathArray||[],g=f.length;if(g>1){var h=!!e[f[1]];h&&(2===g?d="markup"in Object(c.propertyValue):"markup"!==f[2]&&(d=!1))}}d?this.renderLabels():this.updateLabels(),this.updateLabelPositions()},render:function(){this.vel.empty(),this._V={},this.renderMarkup(),this.renderLabels();var a=this.model;return this.watchSource(a,a.source()).watchTarget(a,a.target()).update(),this},renderMarkup:function(){var a=this.model,b=a.get("markup")||a.markup;if(!b)throw new Error("dia.LinkView: markup required");if(Array.isArray(b))return this.renderJSONMarkup(b);if("string"==typeof b)return this.renderStringMarkup(b);throw new Error("dia.LinkView: invalid markup")},renderJSONMarkup:function(a){var b=joint.util.parseDOMJSON(a),c=this.selectors=b.selectors,d=this.selector;if(c[d])throw new Error("dia.LinkView: ambiguous root selector.");c[d]=this.el,this.vel.append(b.fragment)},renderStringMarkup:function(a){var b=V(a);Array.isArray(b)||(b=[b]);for(var c=this._V,d=0,e=b.length;d1||"G"!==d[0].nodeName.toUpperCase()?(c=V("g"),c.append(b),c.addClass("label")):(c=V(d[0]),c.addClass("label")),{node:c.node,selectors:a.selectors}}},renderLabels:function(){var a=this._V,b=a.labels,c=this._labelCache={},d=this._labelSelectors={};b&&b.empty();var e=this.model,f=e.get("labels")||[],g=f.length;if(0===g)return this;b||(b=a.labels=V("g").addClass("labels").appendTo(this.el));for(var h=0;h=this.options.longLinkLength){var e=this.options.doubleLinkToolsOffset||b;d=this.getPointAtLength(c-e),this._tool2Cache.attr("transform","translate("+d.x+", "+d.y+") "+a),this._tool2Cache.attr("visibility","visible")}else this.options.doubleLinkTools&&this._tool2Cache.attr("visibility","hidden")}return this},updateArrowheadMarkers:function(){if(!this._V.markerArrowheads)return this;if("none"===$.css(this._V.markerArrowheads.node,"display"))return this;var a=this.getConnectionLength()0&&b<=1,d=0,e={x:0,y:0};if(a.offset){var f=a.offset;"number"==typeof f&&(d=f),f.x&&(e.x=f.x),f.y&&(e.y=f.y)}var g,h=0!==e.x||0!==e.y||0===d,i=this.path,j={segmentSubdivisions:this.getConnectionSubdivisions()},k=c?b*this.getConnectionLength():b;if(h)g=i.pointAtLength(k,j),g.offset(e);else{var l=i.tangentAtLength(k,j);l?(l.rotate(l.start,-90),l.setLength(d),g=l.end):g=i.start}return g},getVertexIndex:function(a,b){for(var c=this.model,d=c.vertices(),e=this.getClosestPointLength(new g.Point(a,b)),f=0,h=d.length;f0){for(var j=0,k=i.length;j").addClass(joint.util.addClassNamePrefix("paper-background")),this.options.background&&this.drawBackground(this.options.background),this.$grid=$("
").addClass(joint.util.addClassNamePrefix("paper-grid")),this.options.drawGrid&&this.drawGrid(),this.$el.append(this.$background,this.$grid,this.svg),this},update:function(){return this.options.drawGrid&&this.drawGrid(),this._background&&this.updateBackgroundImage(this._background),this},_viewportMatrix:null,_viewportTransformString:null,matrix:function(a){var b=this.viewport;if(void 0===a){var c=b.getAttribute("transform");return(this._viewportTransformString||null)===c?a=this._viewportMatrix:(a=b.getCTM(),this._viewportMatrix=a,this._viewportTransformString=c),V.createSVGMatrix(a)}return a=V.createSVGMatrix(a),ctmString=V.matrixToTransformString(a),b.setAttribute("transform",ctmString),this.tools.setAttribute("transform",ctmString),this._viewportMatrix=a,this._viewportTransformString=b.getAttribute("transform"),this},clientMatrix:function(){return V.createSVGMatrix(this.viewport.getScreenCTM())},_sortDelayingBatches:["add","to-front","to-back"],_onSort:function(){this.model.hasActiveBatch(this._sortDelayingBatches)||this.sortViews()},_onBatchStop:function(a){var b=a&&a.batchName;this._sortDelayingBatches.includes(b)&&!this.model.hasActiveBatch(this._sortDelayingBatches)&&this.sortViews()},onRemove:function(){this.removeViews()},setDimensions:function(a,b){a=this.options.width=a||this.options.width,b=this.options.height=b||this.options.height,this.$el.css({width:Math.round(a),height:Math.round(b)}),this.trigger("resize",a,b)},setOrigin:function(a,b){return this.translate(a||0,b||0,{absolute:!0})},fitToContent:function(a,b,c,d){joint.util.isObject(a)?(d=a,a=d.gridWidth||1,b=d.gridHeight||1,c=d.padding||0):(d=d||{},a=a||1,b=b||1,c=c||0),c=joint.util.normalizeSides(c);var e=V(this.viewport).getBBox(),f=this.scale(),g=this.translate();e.x*=f.sx,e.y*=f.sy,e.width*=f.sx,e.height*=f.sy;var h=Math.max(Math.ceil((e.width+e.x)/a),1)*a,i=Math.max(Math.ceil((e.height+e.y)/b),1)*b,j=0,k=0;("negative"==d.allowNewOrigin&&e.x<0||"positive"==d.allowNewOrigin&&e.x>=0||"any"==d.allowNewOrigin)&&(j=Math.ceil(-e.x/a)*a,j+=c.left,h+=j),("negative"==d.allowNewOrigin&&e.y<0||"positive"==d.allowNewOrigin&&e.y>=0||"any"==d.allowNewOrigin)&&(k=Math.ceil(-e.y/b)*b,k+=c.top,i+=k),h+=c.right,i+=c.bottom,h=Math.max(h,d.minWidth||0),i=Math.max(i,d.minHeight||0),h=Math.min(h,d.maxWidth||Number.MAX_VALUE),i=Math.min(i,d.maxHeight||Number.MAX_VALUE);var l=h!=this.options.width||i!=this.options.height,m=j!=g.tx||k!=g.ty;m&&this.translate(j,k),l&&this.setDimensions(h,i)},scaleContentToFit:function(a){var b=this.getContentBBox();if(b.width&&b.height){a=a||{},joint.util.defaults(a,{padding:0,preserveAspectRatio:!0,scaleGrid:null,minScale:0,maxScale:Number.MAX_VALUE});var c,d=a.padding,e=a.minScaleX||a.minScale,f=a.maxScaleX||a.maxScale,h=a.minScaleY||a.minScale,i=a.maxScaleY||a.maxScale;if(a.fittingBBox)c=a.fittingBBox;else{var j=this.translate();c={x:j.tx,y:j.ty,width:this.options.width,height:this.options.height}}c=g.rect(c).moveAndExpand({x:d,y:d,width:-2*d,height:-2*d});var k=this.scale(),l=c.width/b.width*k.sx,m=c.height/b.height*k.sy;if(a.preserveAspectRatio&&(l=m=Math.min(l,m)),a.scaleGrid){var n=a.scaleGrid;l=n*Math.floor(l/n),m=n*Math.floor(m/n)}l=Math.min(f,Math.max(e,l)),m=Math.min(i,Math.max(h,m)),this.scale(l,m);var o=this.getContentBBox(),p=c.x-o.x,q=c.y-o.y;this.translate(p,q)}},getContentArea:function(){return V(this.viewport).getBBox()},getContentBBox:function(){var a=this.viewport.getBoundingClientRect(),b=this.clientMatrix(),c=this.translate();return g.rect({x:a.left-b.e+c.tx,y:a.top-b.f+c.ty,width:a.width,height:a.height})},getArea:function(){return this.paperToLocalRect({x:0,y:0,width:this.options.width,height:this.options.height})},getRestrictedArea:function(){var a;return a=joint.util.isFunction(this.options.restrictTranslate)?this.options.restrictTranslate.apply(this,arguments):this.options.restrictTranslate===!0?this.getArea():this.options.restrictTranslate||null},createViewForModel:function(a){var b,c,d=this.options.cellViewNamespace,e=a.get("type")+"View",f=joint.util.getByPath(d,e,".");a.isLink()?(b=this.options.linkView,c=joint.dia.LinkView):(b=this.options.elementView,c=joint.dia.ElementView);var g=b.prototype instanceof Backbone.View?f||b:b.call(this,a)||f||c;return new g({model:a,interactive:this.options.interactive})},onCellAdded:function(a,b,c){if(this.options.async&&c.async!==!1&&joint.util.isNumber(c.position)){if(this._asyncCells=this._asyncCells||[],this._asyncCells.push(a),0==c.position){if(this._frameId)throw new Error("another asynchronous rendering in progress");this.asyncRenderViews(this._asyncCells,c),delete this._asyncCells}}else this.renderView(a)},removeView:function(a){var b=this._views[a.id];return b&&(b.remove(),delete this._views[a.id]),b},renderView:function(a){var b=this._views[a.id]=this.createViewForModel(a);return V(this.viewport).append(b.el),b.paper=this,b.render(),b},onImageDragStart:function(){return!1},beforeRenderViews:function(a){return a.sort(function(a){return a.isLink()?1:-1}),a},afterRenderViews:function(){this.sortViews()},resetViews:function(a,b){this.removeViews();var c=a.models.slice();if(c=this.beforeRenderViews(c,b)||c,this.cancelRenderViews(),this.options.async)this.asyncRenderViews(c,b);else{for(var d=0,e=c.length;d(e.get("z")||0)?1:-1})},scale:function(a,b,c,d){if(void 0===a)return V.matrixToScale(this.matrix());void 0===b&&(b=a),void 0===c&&(c=0,d=0);var e=this.translate();if(c||d||e.tx||e.ty){var f=e.tx-c*(a-1),g=e.ty-d*(b-1);this.translate(f,g)}var h=this.matrix();return h.a=a||0,h.d=b||0,this.matrix(h),this.trigger("scale",a,b,c,d),this},rotate:function(a,b,c){if(void 0===a)return V.matrixToRotate(this.matrix());if(void 0===b){var d=this.viewport.getBBox();b=d.width/2,c=d.height/2}var e=this.matrix().translate(b,c).rotate(a).translate(-b,-c);return this.matrix(e),this},translate:function(a,b){if(void 0===a)return V.matrixToTranslate(this.matrix());var c=this.matrix();c.e=a||0,c.f=b||0,this.matrix(c);var d=this.translate(),e=this.options.origin;return e.x=d.tx,e.y=d.ty,this.trigger("translate",d.tx,d.ty),this.options.drawGrid&&this.drawGrid(),this},findView:function(a){for(var b=joint.util.isString(a)?this.viewport.querySelector(a):a instanceof $?a[0]:a;b&&b!==this.el&&b!==document;){var c=b.getAttribute("model-id");if(c)return this._views[c];b=b.parentNode}},findViewByModel:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a&&a.id;return this._views[b]},findViewsFromPoint:function(a){a=g.point(a);var b=this.model.getElements().map(this.findViewByModel,this);return b.filter(function(b){return b&&b.vel.getBBox({target:this.viewport}).containsPoint(a)},this)},findViewsInArea:function(a,b){b=joint.util.defaults(b||{},{strict:!1}),a=g.rect(a);var c=this.model.getElements().map(this.findViewByModel,this),d=b.strict?"containsRect":"intersect";return c.filter(function(b){return b&&a[d](b.vel.getBBox({target:this.viewport}))},this)},removeTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"remove"),this},hideTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"hide"),this},showTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"show"),this},getModelById:function(a){return this.model.getCell(a)},snapToGrid:function(a,b){return this.clientToLocalPoint(a,b).snapToGrid(this.options.gridSize)},localToPaperPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix());return g.Point(d)},localToPaperRect:function(a,b,c,d){var e=g.Rect(a,b),f=V.transformRect(e,this.matrix());return g.Rect(f)},paperToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix().inverse());return g.Point(d)},paperToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.matrix().inverse());return g.Rect(f)},localToClientPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix());return g.Point(d)},localToClientRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix());return g.Rect(f)},clientToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix().inverse());return g.Point(d)},clientToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix().inverse());return g.Rect(f)},localToPagePoint:function(a,b){return this.localToPaperPoint(a,b).offset(this.pageOffset())},localToPageRect:function(a,b,c,d){return this.localToPaperRect(a,b,c,d).moveAndExpand(this.pageOffset())},pageToLocalPoint:function(a,b){var c=g.Point(a,b),d=c.difference(this.pageOffset());return this.paperToLocalPoint(d)},pageToLocalRect:function(a,b,c,d){var e=this.pageOffset(),f=g.Rect(a,b,c,d);return f.x-=e.x,f.y-=e.y,this.paperToLocalRect(f)},clientOffset:function(){var a=this.svg.getBoundingClientRect();return g.Point(a.left,a.top)},pageOffset:function(){return this.clientOffset().offset(window.scrollX,window.scrollY)},linkAllowed:function(a){if(!(a instanceof joint.dia.LinkView))throw new Error("Must provide a linkView.");var b=a.model,c=this.options,d=this.model,e=d.constructor.validations;return!(!c.multiLinks&&!e.multiLinks.call(this,d,b))&&(!(!c.linkPinning&&!e.linkPinning.call(this,d,b))&&!("function"==typeof c.allowLink&&!c.allowLink.call(this,a,this)))},getDefaultLink:function(a,b){return joint.util.isFunction(this.options.defaultLink)?this.options.defaultLink.call(this,a,b):this.options.defaultLink.clone()},resolveHighlighter:function(a){a=a||{};var b=a.highlighter,c=this.options;if(void 0===b){var d=["embedding","connecting","magnetAvailability","elementAvailability"].find(function(b){return!!a[b]});b=d&&c.highlighting[d]||c.highlighting.default}if(!b)return!1;joint.util.isString(b)&&(b={name:b});var e=b.name,f=c.highlighterNamespace[e];if(!f)throw new Error('Unknown highlighter ("'+e+'")');if("function"!=typeof f.highlight)throw new Error('Highlighter ("'+e+'") is missing required highlight() method');if("function"!=typeof f.unhighlight)throw new Error('Highlighter ("'+e+'") is missing required unhighlight() method');return{highlighter:f,options:b.options||{},name:e}},onCellHighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){b.id||(b.id=V.uniqueId());var d=c.name+b.id+JSON.stringify(c.options);if(!this._highlights[d]){var e=c.highlighter;e.highlight(a,b,joint.util.assign({},c.options)),this._highlights[d]={cellView:a,magnetEl:b,opt:c.options,highlighter:e}}}},onCellUnhighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){var d=c.name+b.id+JSON.stringify(c.options),e=this._highlights[d];e&&(e.highlighter.unhighlight(e.cellView,e.magnetEl,e.opt),this._highlights[d]=null)}},pointerdblclick:function(a){a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerdblclick(a,c.x,c.y):this.trigger("blank:pointerdblclick",a,c.x,c.y)}},pointerclick:function(a){if(this._mousemoved<=this.options.clickThreshold){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(this.guard(a,b))return;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerclick(a,c.x,c.y):this.trigger("blank:pointerclick",a,c.x,c.y)}},contextmenu:function(a){ +this.options.preventContextMenu&&a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.contextmenu(a,c.x,c.y):this.trigger("blank:contextmenu",a,c.x,c.y)}},pointerdown:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?(a.preventDefault(),b.pointerdown(a,c.x,c.y)):(this.options.preventDefaultBlankAction&&a.preventDefault(),this.trigger("blank:pointerdown",a,c.x,c.y)),this.delegateDragEvents(b,a.data)}},pointermove:function(a){a.preventDefault();var b=this.eventData(a);b.mousemoved||(b.mousemoved=0);var c=++b.mousemoved;if(!(c<=this.options.moveThreshold)){a=joint.util.normalizeEvent(a);var d=this.snapToGrid({x:a.clientX,y:a.clientY}),e=b.sourceView;e?e.pointermove(a,d.x,d.y):this.trigger("blank:pointermove",a,d.x,d.y),this.eventData(a,b)}},pointerup:function(a){this.undelegateDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.snapToGrid({x:a.clientX,y:a.clientY}),c=this.eventData(a).sourceView;c?c.pointerup(a,b.x,b.y):this.trigger("blank:pointerup",a,b.x,b.y),this.delegateEvents()},mouseover:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b))if(b)b.mouseover(a);else{if(this.el===a.target)return;this.trigger("blank:mouseover",a)}},mouseout:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b))if(b)b.mouseout(a);else{if(this.el===a.target)return;this.trigger("blank:mouseout",a)}},mouseenter:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.findView(a.relatedTarget);if(b){if(c===b)return;b.mouseenter(a)}else{if(c)return;this.trigger("paper:mouseenter",a)}}},mouseleave:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.findView(a.relatedTarget);if(b){if(c===b)return;b.mouseleave(a)}else{if(c)return;this.trigger("paper:mouseleave",a)}}},mousewheel:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=a.originalEvent,d=this.snapToGrid({x:c.clientX,y:c.clientY}),e=Math.max(-1,Math.min(1,c.wheelDelta||-c.detail));b?b.mousewheel(a,d.x,d.y,e):this.trigger("blank:mousewheel",a,d.x,d.y,e)}},onevent:function(a){var b=a.currentTarget,c=b.getAttribute("event");if(c){var d=this.findView(b);if(d){if(a=joint.util.normalizeEvent(a),this.guard(a,d))return;var e=this.snapToGrid({x:a.clientX,y:a.clientY});d.onevent(a,c,e.x,e.y)}}},onmagnet:function(a){var b=a.currentTarget,c=b.getAttribute("magnet");if(c){var d=this.findView(b);if(d){if(a=joint.util.normalizeEvent(a),this.guard(a,d))return;if(!this.options.validateMagnet(d,b))return;var e=this.snapToGrid(a.clientX,a.clientY);d.onmagnet(a,e.x,e.y)}}},onlabel:function(a){var b=a.currentTarget,c=this.findView(b);if(c){if(a=joint.util.normalizeEvent(a),this.guard(a,c))return;var d=this.snapToGrid(a.clientX,a.clientY);c.onlabel(a,d.x,d.y)}},delegateDragEvents:function(a,b){b||(b={}),this.eventData({data:b},{sourceView:a||null,mousemoved:0}),this.delegateDocumentEvents(null,b),this.undelegateEvents()},guard:function(a,b){return!(!this.options.guard||!this.options.guard(a,b))||(a.data&&void 0!==a.data.guarded?a.data.guarded:!(b&&b.model&&b.model instanceof joint.dia.Cell)&&(this.svg!==a.target&&this.el!==a.target&&!$.contains(this.svg,a.target)))},setGridSize:function(a){return this.options.gridSize=a,this.options.drawGrid&&this.drawGrid(),this},clearGrid:function(){return this.$grid&&this.$grid.css("backgroundImage","none"),this},_getGriRefs:function(){return this._gridCache||(this._gridCache={root:V("svg",{width:"100%",height:"100%"},V("defs")),patterns:{},add:function(a,b){V(this.root.node.childNodes[0]).append(b),this.patterns[a]=b,this.root.append(V("rect",{width:"100%",height:"100%",fill:"url(#"+a+")"}))},get:function(a){return this.patterns[a]},exist:function(a){return void 0!==this.patterns[a]}}),this._gridCache},setGrid:function(a){this.clearGrid(),this._gridCache=null,this._gridSettings=[];var b=Array.isArray(a)?a:[a||{}];return b.forEach(function(a){this._gridSettings.push.apply(this._gridSettings,this._resolveDrawGridOption(a))},this),this},_resolveDrawGridOption:function(a){var b=this.constructor.gridPatterns;if(joint.util.isString(a)&&Array.isArray(b[a]))return b[a].map(function(a){return joint.util.assign({},a)});var c=a||{args:[{}]},d=Array.isArray(c),e=c.name;if(d||e||c.markup||(e="dot"),e&&Array.isArray(b[e])){var f=b[e].map(function(a){return joint.util.assign({},a)}),g=Array.isArray(c.args)?c.args:[c.args||{}];joint.util.defaults(g[0],joint.util.omit(a,"args"));for(var h=0;h'),f=joint.util.toArray(d).map(function(a){return e({offset:a.offset,color:a.color,opacity:Number.isFinite(a.opacity)?a.opacity:1})}),g=["<"+c+">",f.join(""),""].join(""),h=joint.util.assign({id:b},a.attrs);V(g,h).appendTo(this.defs)}return b},defineMarker:function(a){if(!joint.util.isObject(a))throw new TypeError("dia.Paper: defineMarker() requires 1. argument to be an object.");var b=a.id;if(b||(b=this.svg.id+joint.util.hashCode(JSON.stringify(a))),!this.isDefined(b)){var c=joint.util.omit(a,"type","userSpaceOnUse"),d=V("marker",{id:b,orient:"auto",overflow:"visible",markerUnits:a.markerUnits||"userSpaceOnUse"},[V(a.type||"path",c)]);d.appendTo(this.defs)}return b}},{backgroundPatterns:{flipXy:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,-1,b.width,b.height),e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,1,b.width,0),e.drawImage(a,0,0,c,d),e.setTransform(1,0,0,-1,0,b.height),e.drawImage(a,0,0,c,d),b},flipX:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(2*c,0),e.scale(-1,1),e.drawImage(a,0,0,c,d),b},flipY:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(0,2*d),e.scale(1,-1),e.drawImage(a,0,0,c,d),b},watermark:function(a,b){b=b||{};var c=a.width,d=a.height,e=document.createElement("canvas");e.width=3*c,e.height=3*d;for(var f=e.getContext("2d"),h=joint.util.isNumber(b.watermarkAngle)?-b.watermarkAngle:-20,i=g.toRad(h),j=e.width/4,k=e.height/4,l=0;l<4;l++)for(var m=0;m<4;m++)(l+m)%2>0&&(f.setTransform(1,0,0,1,(2*l-1)*j,(2*m-1)*k),f.rotate(i),f.drawImage(a,-c/2,-d/2,c,d));return e}},gridPatterns:{dot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){V(a).attr({width:b.thickness*b.sx,height:b.thickness*b.sy,fill:b.color})}}],fixedDot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){var c=b.sx<=1?b.thickness*b.sx:b.thickness;V(a).attr({width:c,height:c,fill:b.color})}}],mesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}],doubleMesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}},{color:"#000000",thickness:3,scaleFactor:4,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}]}}),function(a,b,c){var d=function(b){var d=c.cloneDeep(b)||{};this.ports=[],this.groups={},this.portLayoutNamespace=a.layout.Port,this.portLabelLayoutNamespace=a.layout.PortLabel,this._init(d)};d.prototype={getPorts:function(){return this.ports},getGroup:function(a){return this.groups[a]||{}},getPortsByGroup:function(a){return this.ports.filter(function(b){return b.group===a})},getGroupPortsMetrics:function(a,b){var d=this.getGroup(a),e=this.getPortsByGroup(a),f=d.position||{},h=f.name,i=this.portLayoutNamespace;i[h]||(h="left");var j=f.args||{},k=e.map(function(a){return a&&a.position&&a.position.args}),l=i[h](k,b,j),m={ports:e,result:[]};return c.toArray(l).reduce(function(a,c,d){var e=a.ports[d];return a.result.push({portId:e.id,portTransformation:c,labelTransformation:this._getPortLabelLayout(e,g.Point(c),b),portAttrs:e.attrs,portSize:e.size,labelSize:e.label.size}),a}.bind(this),m),m.result},_getPortLabelLayout:function(a,b,c){var d=this.portLabelLayoutNamespace,e=a.label.position.name||"left";return d[e]?d[e](b,c,a.label.position.args):null},_init:function(a){if(c.isObject(a.groups))for(var b=Object.keys(a.groups),d=0,e=b.length;d0},hasPort:function(a){return this.getPortIndex(a)!==-1},getPorts:function(){return c.cloneDeep(this.prop("ports/items"))||[]},getPort:function(a){return c.cloneDeep(c.toArray(this.prop("ports/items")).find(function(b){return b.id&&b.id===a}))},getPortsPositions:function(a){var b=this._portSettingsData.getGroupPortsMetrics(a,g.Rect(this.size()));return b.reduce(function(a,b){var c=b.portTransformation;return a[b.portId]={x:c.x,y:c.y,angle:c.angle},a},{})},getPortIndex:function(a){var b=c.isObject(a)?a.id:a;return this._isValidPortId(b)?c.toArray(this.prop("ports/items")).findIndex(function(a){return a.id===b}):-1},addPort:function(a,b){if(!c.isObject(a)||Array.isArray(a))throw new Error("Element: addPort requires an object.");var d=c.assign([],this.prop("ports/items"));return d.push(a),this.prop("ports/items",d,b),this},portProp:function(a,b,d,e){var f=this.getPortIndex(a);if(f===-1)throw new Error("Element: unable to find port with id "+a);var g=Array.prototype.slice.call(arguments,1);return Array.isArray(b)?g[0]=["ports","items",f].concat(b):c.isString(b)?g[0]=["ports/items/",f,"/",b].join(""):(g=["ports/items/"+f],c.isPlainObject(b)&&(g.push(b),g.push(d))),this.prop.apply(this,g)},_validatePorts:function(){var b=this.get("ports")||{},d=[];b=b||{};var e=c.toArray(b.items);return e.forEach(function(a){"object"!=typeof a&&d.push("Element: invalid port ",a),this._isValidPortId(a.id)||(a.id=c.uuid())},this),a.util.uniq(e,"id").length!==e.length&&d.push("Element: found id duplicities in ports."),d},_isValidPortId:function(a){return null!==a&&void 0!==a&&!c.isObject(a)},addPorts:function(a,b){return a.length&&this.prop("ports/items",c.assign([],this.prop("ports/items")).concat(a),b),this},removePort:function(a,b){var d=b||{},e=c.assign([],this.prop("ports/items")),f=this.getPortIndex(a);return f!==-1&&(e.splice(f,1),d.rewrite=!0,this.prop("ports/items",e,d)),this},_createPortData:function(){var a=this._validatePorts();if(a.length>0)throw this.set("ports",this.previous("ports")),new Error(a.join(" "));var b;this._portSettingsData&&(b=this._portSettingsData.getPorts()),this._portSettingsData=new d(this.get("ports"));var c=this._portSettingsData.getPorts();if(b){var e=c.filter(function(a){if(!b.find(function(b){return b.id===a.id}))return a}),f=b.filter(function(a){if(!c.find(function(b){return b.id===a.id}))return a});f.length>0&&this.trigger("ports:remove",this,f),e.length>0&&this.trigger("ports:add",this,e)}}}),c.assign(a.dia.ElementView.prototype,{portContainerMarkup:"g",portMarkup:[{tagName:"circle",selector:"circle",attributes:{r:10,fill:"#FFFFFF",stroke:"#000000"}}],portLabelMarkup:[{tagName:"text",selector:"text",attributes:{fill:"#000000"}}],_portElementsCache:null,_initializePorts:function(){this._portElementsCache={},this.listenTo(this.model,"change:ports",function(){this._refreshPorts()})},_refreshPorts:function(){this._removePorts(),this._portElementsCache={},this._renderPorts()},_renderPorts:function(){for(var a=[],b=this._getContainerElement(),d=0,e=b.node.childNodes.length;d1?V("g").append(h):V(h.firstChild),e=g.selectors}else b=V(f),Array.isArray(b)&&(b=V("g").append(b));if(!b)throw new Error("ElementView: Invalid port markup.");b.attr({port:a.id,"port-group":a.group});var i,j=this._getPortLabelMarkup(a.label);if(Array.isArray(j)){var k=c.parseDOMJSON(j),l=k.fragment;d=l.childNodes.length>1?V("g").append(l):V(l.firstChild),i=k.selectors}else d=V(j),Array.isArray(d)&&(d=V("g").append(d));if(!d)throw new Error("ElementView: Invalid port label markup.");var m;if(e&&i){for(var n in i)if(e[n])throw new Error("ElementView: selectors within port must be unique.");m=c.assign({},e,i)}else m=e||i;var o=V(this.portContainerMarkup).addClass("joint-port").append([b.addClass("joint-port-body"),d.addClass("joint-port-label")]);return this._portElementsCache[a.id]={portElement:o,portLabelElement:d,portSelectors:m,portLabelSelectors:i,portContentElement:b,portContentSelectors:e},o},_updatePortGroup:function(a){for(var b=g.Rect(this.model.size()),c=this.model._portSettingsData.getGroupPortsMetrics(a,b),d=0,e=c.length;d'}),joint.shapes.basic.TextView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"change:attrs",this.resize)}}),joint.shapes.basic.Generic.define("basic.Text",{attrs:{text:{"font-size":18,fill:"#000000"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Circle",{size:{width:60,height:60},attrs:{circle:{fill:"#ffffff",stroke:"#000000",r:30,cx:30,cy:30},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Ellipse",{size:{width:60,height:40},attrs:{ellipse:{fill:"#ffffff",stroke:"#000000",rx:30,ry:20,cx:30,cy:20},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polygon",{size:{width:60,height:40},attrs:{polygon:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polyline",{size:{width:60,height:40},attrs:{polyline:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Image",{attrs:{text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Path",{size:{width:60,height:60},attrs:{path:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle",ref:"path","ref-x":.5,"ref-dy":10,fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Path.define("basic.Rhombus",{attrs:{path:{d:"M 30 0 L 60 30 30 60 0 30 z"},text:{"ref-y":.5,"ref-dy":null,"y-alignment":"middle"}}}),joint.shapes.basic.PortsModelInterface={initialize:function(){this.updatePortsAttrs(),this.on("change:inPorts change:outPorts",this.updatePortsAttrs,this),this.constructor.__super__.constructor.__super__.initialize.apply(this,arguments)},updatePortsAttrs:function(a){if(this._portSelectors){var b=joint.util.omit(this.get("attrs"),this._portSelectors);this.set("attrs",b,{silent:!0})}this._portSelectors=[];var c={};joint.util.toArray(this.get("inPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".inPorts","in");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),joint.util.toArray(this.get("outPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".outPorts","out");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),this.attr(c,{silent:!0}),this.processPorts(),this.trigger("process:ports")},getPortSelector:function(a){var b=".inPorts",c=this.get("inPorts").indexOf(a);if(c<0&&(b=".outPorts",c=this.get("outPorts").indexOf(a),c<0))throw new Error("getPortSelector(): Port doesn't exist.");return b+">g:nth-child("+(c+1)+")>.port-body"}},joint.shapes.basic.PortsViewInterface={initialize:function(){this.listenTo(this.model,"process:ports",this.update),joint.dia.ElementView.prototype.initialize.apply(this,arguments)},update:function(){this.renderPorts(),joint.dia.ElementView.prototype.update.apply(this,arguments)},renderPorts:function(){var a=this.$(".inPorts").empty(),b=this.$(".outPorts").empty(),c=joint.util.template(this.model.portMarkup),d=this.model.ports||[];d.filter(function(a){return"in"===a.type}).forEach(function(b,d){a.append(V(c({id:d,port:b})).node)}),d.filter(function(a){return"out"===a.type}).forEach(function(a,d){b.append(V(c({id:d,port:a})).node)})}},joint.shapes.basic.Generic.define("basic.TextBlock",{attrs:{rect:{fill:"#ffffff",stroke:"#000000",width:80,height:100},text:{fill:"#000000","font-size":14,"font-family":"Arial, helvetica, sans-serif"},".content":{text:"","ref-x":.5,"ref-y":.5,"y-alignment":"middle","x-alignment":"middle"}},content:""},{markup:['','',joint.env.test("svgforeignobject")?'
':'',""].join(""),initialize:function(){this.listenTo(this,"change:size",this.updateSize),this.listenTo(this,"change:content",this.updateContent),this.updateSize(this,this.get("size")),this.updateContent(this,this.get("content")),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateSize:function(a,b){this.attr({".fobj":joint.util.assign({},b),div:{style:joint.util.assign({},b)}})},updateContent:function(a,b){joint.env.test("svgforeignobject")?this.attr({".content":{html:joint.util.sanitizeHTML(b)}}):this.attr({".content":{text:b}})},setForeignObjectSize:function(){this.updateSize.apply(this,arguments)},setDivContent:function(){this.updateContent.apply(this,arguments)}}),joint.shapes.basic.TextBlockView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.noSVGForeignObjectElement=!joint.env.test("svgforeignobject"),joint.env.test("svgforeignobject")||this.listenTo(this.model,"change:content change:size",function(a){this.updateContent(a)})},update:function(a,b){var c=this.model;if(joint.env.test("svgforeignobject"))joint.dia.ElementView.prototype.update.call(this,c,b);else{var d=joint.util.omit(b||c.get("attrs"),".content");joint.dia.ElementView.prototype.update.call(this,c,d),b&&!joint.util.has(b,".content")||this.updateContent(c,b)}},updateContent:function(a,b){var c=joint.util.merge({},(b||a.get("attrs"))[".content"]);c=joint.util.omit(c,"text");var d=joint.util.breakText(a.get("content"),a.get("size"),c,{svgDocument:this.paper.svg}),e=joint.util.setByPath({},".content",c,"/");e[".content"].text=d,joint.dia.ElementView.prototype.update.call(this,a,e)}}),function(a,b,c,d){"use strict";var e=a.Element;e.define("standard.Rectangle",{attrs:{body:{refWidth:"100%",refHeight:"100%",strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Circle",{attrs:{body:{refCx:"50%",refCy:"50%",refR:"50%",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"circle",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Ellipse",{attrs:{body:{refCx:"50%",refCy:"50%",refRx:"50%",refRy:"50%",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"ellipse",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Path",{attrs:{body:{refD:"M 0 0 L 10 0 10 10 0 10 Z",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"path",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Polygon",{attrs:{body:{refPoints:"0 0 10 0 10 10 0 10",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"polygon",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Polyline",{attrs:{body:{refPoints:"0 0 10 0 10 10 0 10 0 0",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"polyline",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Image",{attrs:{image:{refWidth:"100%",refHeight:"100%"},label:{textVerticalAnchor:"top",textAnchor:"middle",refX:"50%",refY:"100%",refY2:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"image",selector:"image"},{tagName:"text",selector:"label"}]}),e.define("standard.BorderedImage",{attrs:{border:{refWidth:"100%",refHeight:"100%",stroke:"#333333",strokeWidth:2},image:{refWidth:-1,refHeight:-1,x:.5,y:.5},label:{textVerticalAnchor:"top",textAnchor:"middle",refX:"50%",refY:"100%",refY2:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"image",selector:"image"},{tagName:"rect",selector:"border",attributes:{fill:"none"}},{tagName:"text",selector:"label"}]}),e.define("standard.EmbeddedImage",{attrs:{body:{refWidth:"100%",refHeight:"100%",stroke:"#333333",fill:"#FFFFFF",strokeWidth:2},image:{refWidth:"30%",refHeight:-20,x:10,y:10,preserveAspectRatio:"xMidYMin"},label:{textVerticalAnchor:"top",textAnchor:"left",refX:"30%",refX2:20,refY:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"image",selector:"image"},{tagName:"text",selector:"label"}]}),e.define("standard.HeaderedRectangle",{attrs:{body:{refWidth:"100%",refHeight:"100%",strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},header:{refWidth:"100%",height:30,strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},headerText:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:15,fontSize:16,fill:"#333333"},bodyText:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",refY2:15,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"rect",selector:"header"},{tagName:"text",selector:"headerText"},{tagName:"text",selector:"bodyText"}]});var f=10;joint.dia.Element.define("standard.Cylinder",{attrs:{body:{lateralArea:f,fill:"#FFFFFF",stroke:"#333333",strokeWidth:2},top:{refCx:"50%",cy:f,refRx:"50%",ry:f,fill:"#FFFFFF",stroke:"#333333",strokeWidth:2},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"100%",refY2:15,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"path",selector:"body"},{tagName:"ellipse",selector:"top"},{tagName:"text",selector:"label"}],topRy:function(a,c){if(void 0===a)return this.attr("body/lateralArea");var d=b.isPercentage(a),e={lateralArea:a},f=d?{refCy:a,refRy:a,cy:null,ry:null}:{refCy:null,refRy:null,cy:a,ry:a};return this.attr({body:e,top:f},c)}},{attributes:{lateralArea:{set:function(a,c){var e=b.isPercentage(a);e&&(a=parseFloat(a)/100);var f=c.x,g=c.y,h=c.width,i=c.height,j=h/2,k=e?i*a:a,l=d.KAPPA,m=l*j,n=l*(e?i*a:a),o=f,p=f+h/2,q=f+h,r=g+k,s=r-k,t=g+i-k,u=g+i,v=["M",o,r,"L",o,t,"C",f,t+n,p-m,u,p,u,"C",p+m,u,q,t+n,q,t,"L",q,r,"C",q,r-n,p+m,s,p,s,"C",p-m,s,o,r-n,o,r,"Z"]; +return{d:v.join(" ")}}}}});var g={tagName:"foreignObject",selector:"foreignObject",attributes:{overflow:"hidden"},children:[{tagName:"div",namespaceURI:"http://www.w3.org/1999/xhtml",selector:"label",style:{width:"100%",height:"100%",position:"static",backgroundColor:"transparent",textAlign:"center",margin:0,padding:"0px 5px",boxSizing:"border-box",display:"flex",alignItems:"center",justifyContent:"center"}}]},h={tagName:"text",selector:"label",attributes:{"text-anchor":"middle"}};e.define("standard.TextBlock",{attrs:{body:{refWidth:"100%",refHeight:"100%",stroke:"#333333",fill:"#ffffff",strokeWidth:2},foreignObject:{refWidth:"100%",refHeight:"100%"},label:{style:{fontSize:14}}}},{markup:[{tagName:"rect",selector:"body"},c.test("svgforeignobject")?g:h]},{attributes:{text:{set:function(c,d,e,f){if(!(e instanceof HTMLElement)){var g=f.style||{},h={text:c,width:-5,height:"100%"},i=b.assign({textVerticalAnchor:"middle"},g);return a.attributes.textWrap.set.call(this,h,d,e,i),{fill:g.color||null}}e.textContent=c},position:function(a,b,c){if(c instanceof SVGElement)return b.center()}}}});var i=a.Link;i.define("standard.Link",{attrs:{line:{connection:!0,stroke:"#333333",strokeWidth:2,strokeLinejoin:"round",targetMarker:{type:"path",d:"M 10 -5 0 0 10 5 z"}},wrapper:{connection:!0,strokeWidth:10,strokeLinejoin:"round"}}},{markup:[{tagName:"path",selector:"wrapper",attributes:{fill:"none",cursor:"pointer",stroke:"transparent"}},{tagName:"path",selector:"line",attributes:{fill:"none","pointer-events":"none"}}]}),i.define("standard.DoubleLink",{attrs:{line:{connection:!0,stroke:"#DDDDDD",strokeWidth:4,strokeLinejoin:"round",targetMarker:{type:"path",stroke:"#000000",d:"M 10 -3 10 -10 -2 0 10 10 10 3"}},outline:{connection:!0,stroke:"#000000",strokeWidth:6,strokeLinejoin:"round"}}},{markup:[{tagName:"path",selector:"outline",attributes:{fill:"none"}},{tagName:"path",selector:"line",attributes:{fill:"none"}}]}),i.define("standard.ShadowLink",{attrs:{line:{connection:!0,stroke:"#FF0000",strokeWidth:20,strokeLinejoin:"round",targetMarker:{type:"path",stroke:"none",d:"M 0 -10 -10 0 0 10 z"},sourceMarker:{type:"path",stroke:"none",d:"M -10 -10 0 0 -10 10 0 10 0 -10 z"}},shadow:{connection:!0,refX:3,refY:6,stroke:"#000000",strokeOpacity:.2,strokeWidth:20,strokeLinejoin:"round",targetMarker:{type:"path",d:"M 0 -10 -10 0 0 10 z",stroke:"none"},sourceMarker:{type:"path",stroke:"none",d:"M -10 -10 0 0 -10 10 0 10 0 -10 z"}}}},{markup:[{tagName:"path",selector:"shadow",attributes:{fill:"none"}},{tagName:"path",selector:"line",attributes:{fill:"none"}}]})}(joint.dia,joint.util,joint.env,V),joint.routers.manhattan=function(a,b,c,d){"use strict";function e(a){this.map={},this.options=a,this.mapGridSize=100}function f(){this.items=[],this.hash={},this.values={},this.OPEN=1,this.CLOSE=2}function g(a,b){return b&&b.paddingBox?a.sourceBBox.clone().moveAndExpand(b.paddingBox):a.sourceBBox.clone()}function h(a,b){return b&&b.paddingBox?a.targetBBox.clone().moveAndExpand(b.paddingBox):a.targetBBox.clone()}function i(a,b){if(a.sourceAnchor)return a.sourceAnchor;var c=g(a,b);return c.center()}function j(a,b){if(a.targetAnchor)return a.targetAnchor;var c=h(a,b);return c.center()}function k(b,c,d,e,f){var g=360/d,h=b.theta(l(b,c,e,f)),i=a.normalizeAngle(h+g/2);return g*Math.floor(i/g)}function l(b,c,d,e){var f=e.step,g=c.x-b.x,h=c.y-b.y,i=g/d.x,j=h/d.y,k=i*f,l=j*f;return new a.Point(b.x+k,b.y+l)}function m(a,b){var c=Math.abs(a-b);return c>180?360-c:c}function n(a,b,c){var e=c.step;d.toArray(c.directions).forEach(function(a){a.gridOffsetX=a.offsetX/e*b.x,a.gridOffsetY=a.offsetY/e*b.y})}function o(a,b,c){return{source:b.clone(),x:p(c.x-b.x,a),y:p(c.y-b.y,a)}}function p(a,b){if(!a)return b;var c=Math.abs(a),d=Math.round(c/b);if(!d)return c;var e=d*b,f=c-e,g=f/d;return b+g}function q(b,c){var d=c.source,e=a.snapToGrid(b.x-d.x,c.x)+d.x,f=a.snapToGrid(b.y-d.y,c.y)+d.y;return new a.Point(e,f)}function r(a,b){return a?a.round(b.precision):a}function s(a){return a.clone().round().toString()}function t(b){return new a.Point(0===b.x?0:Math.abs(b.x)/b.x,0===b.y?0:Math.abs(b.y)/b.y)}function u(a,b,c,d,e,f){for(var g,h=[],i=t(e.difference(c)),j=s(c),k=a[j];k;){g=r(b[j],f);var l=t(g.difference(r(k.clone(),f)));l.equals(i)||(h.unshift(g),i=l),j=s(k),k=a[j]}var m=r(b[j],f),n=t(m.difference(d));return n.equals(i)||h.unshift(m),h}function v(a,b){for(var c=1/0,d=0,e=b.length;dj)&&(j=w,t=q(v,f))}var x=r(t,g);x&&(c.containsPoint(x)&&r(x.offset(l.x*f.x,l.y*f.y),g),d.push(x))}return d},[]);return c.containsPoint(i)||n.push(i),n}function x(b,c,e,g){var h,l;h=b instanceof a.Rect?i(this,g).clone():b.clone(),l=c instanceof a.Rect?j(this,g).clone():c.clone();var p,t,x,y,z=o(g.step,h,l);if(b instanceof a.Rect?(p=r(q(h,z),g),x=w(p,b,g.startDirections,z,g)):(p=r(q(h,z),g),x=[p]),c instanceof a.Rect?(t=r(q(l,z),g),y=w(l,c,g.endDirections,z,g)):(t=r(q(l,z),g),y=[t]),x=x.filter(e.isPointAccessible,e),y=y.filter(e.isPointAccessible,e),x.length>0&&y.length>0){for(var A=new f,B={},C={},D={},E=0,F=x.length;E0;){var Q,R=A.pop(),S=B[R],T=C[R],U=D[R],V=void 0===T,W=S.equals(p);if(Q=V?L?W?null:k(p,S,N,z,g):K:k(T,S,N,z,g),O.indexOf(R)>=0)return g.previousDirectionAngle=Q,u(C,B,S,p,t,g);for(E=0;Eg.maxAllowedDirectionChange)){var Y=S.clone().offset(I.gridOffsetX,I.gridOffsetY),Z=s(Y);if(!A.isClose(Z)&&e.isPointAccessible(Y)){if(O.indexOf(Z)>=0){r(Y,g);var $=Y.equals(t);if(!$){var _=k(Y,t,N,z,g),aa=m(X,_);if(aa>g.maxAllowedDirectionChange)continue}}var ba=I.cost,ca=W?0:g.penalties[J],da=U+ba+ca;(!A.isOpen(Z)||da90){var i=f;f=h,h=i}var j=d%90<45?f:h,k=new g.Line(a,j),l=90*Math.ceil(d/90),m=g.Point.fromPolar(k.squaredLength(),g.toRad(l+135),j),n=new g.Line(b,m),o=k.intersection(n),p=o?o:b,q=o?p:a,r=360/c.directions.length,s=q.theta(b),t=g.normalizeAngle(s+r/2),u=r*Math.floor(t/r);return c.previousDirectionAngle=u,p&&e.push(p.round()),e.push(b),e}};return function(c,d,e){if(!a.isFunction(joint.routers.manhattan))throw new Error("Metro requires the manhattan router.");return joint.routers.manhattan(c,a.assign({},b,d),e)}}(joint.util),joint.routers.normal=function(a,b,c){return a},joint.routers.oneSide=function(a,b,c){var d,e,f,g=b.side||"bottom",h=b.padding||40,i=c.sourceBBox,j=c.targetBBox,k=i.center(),l=j.center();switch(g){case"bottom":f=1,d="y",e="height";break;case"top":f=-1,d="y",e="height";break;case"left":f=-1,d="x",e="width";break;case"right":f=1,d="x",e="width";break;default:throw new Error("Router: invalid side")}return k[d]+=f*(i[e]/2+h),l[d]+=f*(j[e]/2+h),f*(k[d]-l[d])>0?l[d]=k[d]:k[d]=l[d],[k].concat(a,l)},joint.routers.orthogonal=function(a){function b(a,b,c){var d=new g.Point(a.x,b.y);return c.containsPoint(d)&&(d=new g.Point(b.x,a.y)),d}function c(a,b){return a["W"===b||"E"===b?"width":"height"]}function d(a,b){return a.x===b.x?a.y>b.y?"N":"S":a.y===b.y?a.x>b.x?"W":"E":null}function e(a){return new g.Rect(a.x,a.y,0,0)}function f(a,b){var c=b&&b.elementPadding||20;return a.sourceBBox.clone().inflate(c)}function h(a,b){var c=b&&b.elementPadding||20;return a.targetBBox.clone().inflate(c)}function i(a,b){if(a.sourceAnchor)return a.sourceAnchor;var c=f(a,b);return c.center()}function j(a,b){if(a.targetAnchor)return a.targetAnchor;var c=h(a,b);return c.center()}function k(a,b,c){var e=new g.Point(a.x,b.y),f=new g.Point(b.x,a.y),h=d(a,e),i=d(a,f),j=q[c],k=h===c||h!==j&&(i===j||i!==c)?e:f;return{points:[k],direction:d(k,b)}}function l(a,c,e){var f=b(a,c,e);return{points:[f],direction:d(f,c)}}function m(e,f,h,i){var j,k={},l=[new g.Point(e.x,f.y),new g.Point(f.x,e.y)],m=l.filter(function(a){return!h.containsPoint(a)}),n=m.filter(function(a){return d(a,e)!==i});if(n.length>0)j=n.filter(function(a){return d(e,a)===i}).pop(),j=j||n[0],k.points=[j],k.direction=d(j,f);else{j=a.difference(l,m)[0];var o=new g.Point(f).move(j,-c(h,i)/2),p=b(o,e,h);k.points=[p,o],k.direction=d(o,f)}return k}function n(a,b,e,f){var h=l(b,a,f),i=h.points[0];if(e.containsPoint(i)){h=l(a,b,e);var j=h.points[0];if(f.containsPoint(j)){var m=new g.Point(a).move(j,-c(e,d(a,j))/2),n=new g.Point(b).move(i,-c(f,d(b,i))/2),o=new g.Line(m,n).midpoint(),p=l(a,o,e),q=k(o,b,p.direction);h.points=[p.points[0],q.points[0]],h.direction=q.direction}}return h}function o(a,c,e,f,h){var i,j,k,l={},m=e.union(f).inflate(1),n=m.center().distance(c)>m.center().distance(a),o=n?c:a,p=n?a:c;return h?(i=g.Point.fromPolar(m.width+m.height,r[h],o),i=m.pointNearestToPoint(i).move(i,-1)):i=m.pointNearestToPoint(o).move(o,1),j=b(i,p,m),i.round().equals(j.round())?(j=g.Point.fromPolar(m.width+m.height,g.toRad(i.theta(o))+Math.PI/2,p),j=m.pointNearestToPoint(j).move(p,1).round(),k=b(i,j,m),l.points=n?[j,k,i]:[i,k,j]):l.points=n?[j,i]:[i,j],l.direction=n?d(i,c):d(j,c),l}function p(b,c,p){var q=c.elementPadding||20,r=f(p,c),s=h(p,c),t=i(p,c),u=j(p,c);r=r.union(e(t)),s=s.union(e(u)),b=a.toArray(b).map(g.Point),b.unshift(t),b.push(u);for(var v,w=[],x=0,y=b.length-1;x=Math.abs(a.y-b.y)){var k=(a.x+b.x)/2;j=g.Path.createSegment("C",k,a.y,k,b.y,b.x,b.y),e.appendSegment(j)}else{var l=(a.y+b.y)/2;j=g.Path.createSegment("C",a.x,l,b.x,l,b.x,b.y),e.appendSegment(j)}}return f?e:e.serialize()},joint.connectors.jumpover=function(a,b,c){function d(a,c,d){var e=[].concat(a,d,c);return e.reduce(function(a,c,d){var f=e[d+1];return null!=f&&(a[d]=b.line(c,f)),a},[])}function e(a){var b=a.paper._jumpOverUpdateList;null==b&&(b=a.paper._jumpOverUpdateList=[],a.paper.on("cell:pointerup",f),a.paper.model.on("reset",function(){b=a.paper._jumpOverUpdateList=[]})),b.indexOf(a)<0&&(b.push(a),a.listenToOnce(a.model,"change:connector remove",function(){b.splice(b.indexOf(a),1)}))}function f(){for(var a=this._jumpOverUpdateList,b=0;bw)||"jumpover"!==d.name)}),z=y.map(function(a){return s.findViewByModel(a)}),A=d(a,b,f),B=z.map(function(a){return null==a?[]:a===this?A:d(a.sourcePoint,a.targetPoint,a.route)},this),C=A.reduce(function(a,b){var c=y.reduce(function(a,c,d){if(c!==v){var e=g(b,B[d]);a.push.apply(a,e)}return a},[]).sort(function(a,c){return h(b.start,a)-h(b.start,c)});return c.length>0?a.push.apply(a,i(b,c,p)):a.push(b),a},[]),D=j(C,p,q);return o?D:D.serialize()}}(_,g,joint.util),function(a,b,c,d){function e(a,b,d){var e=a.toJSON();return e.angle=b||0,c.util.defaults({},d,e)}function f(a,c,d){return a.map(function(a,b,c){var d=this.pointAt((b+.5)/c.length);return(a.dx||a.dy)&&d.offset(a.dx||0,a.dy||0),e(d.round(),0,a)},b.line(c,d))}function g(a,c,d,f){var g=c.center(),h=c.width/c.height,i=c.topMiddle(),j=b.Ellipse.fromRect(c);return a.map(function(a,b,c){var k=d+f(b,c.length),l=i.clone().rotate(g,-k).scale(h,1,g),m=a.compensateRotation?-j.tangentTheta(l):0;return(a.dx||a.dy)&&l.offset(a.dx||0,a.dy||0),a.dr&&l.move(g,a.dr),e(l.round(),m,a)})}function h(a,c){var e=c.x;d.isString(e)&&(e=parseFloat(e)/100*a.width);var f=c.y;return d.isString(f)&&(f=parseFloat(f)/100*a.height),b.point(e||0,f||0)}c.layout.Port={absolute:function(a,b,c){return a.map(h.bind(null,b))},fn:function(a,b,c){return c.fn(a,b,c)},line:function(a,b,c){var d=h(b,c.start||b.origin()),e=h(b,c.end||b.corner());return f(a,d,e)},left:function(a,b,c){return f(a,b.origin(),b.bottomLeft())},right:function(a,b,c){return f(a,b.topRight(),b.corner())},top:function(a,b,c){return f(a,b.origin(),b.topRight())},bottom:function(a,b,c){return f(a,b.bottomLeft(),b.corner())},ellipseSpread:function(a,b,c){var d=c.startAngle||0,e=c.step||360/a.length;return g(a,b,d,function(a){return a*e})},ellipse:function(a,b,c){var d=c.startAngle||0,e=c.step||20;return g(a,b,d,function(a,b){return(a+.5-b/2)*e})}}}(_,g,joint,joint.util),function(a,b,c,d){function e(a,b){return d.defaultsDeep({},a,b,{x:0,y:0,angle:0,attrs:{".":{y:"0","text-anchor":"start"}}})}function f(a,b,c,f){f=d.defaults({},f,{offset:15});var h,i,j,k,l=b.center().theta(a),m=g(b),n=f.offset,o=0;lm[2]?(j=".3em",h=n,i=0,k="start"):lo[2]?(k=".3em",i=-m,j=0,l="end"):h-270&&i<-90?(g="start",j=i-180):g="end";var m=Math.round;return e({x:m(k.x),y:m(k.y),angle:c?j:0,attrs:{".":{y:l,"text-anchor":g}}})}c.layout.PortLabel={manual:function(a,b,c){return e(c,a)},left:function(a,b,c){return e(c,{x:-15,attrs:{".":{y:".3em","text-anchor":"end"}}})},right:function(a,b,c){return e(c,{x:15,attrs:{".":{y:".3em","text-anchor":"start"}}})},top:function(a,b,c){return e(c,{y:-15,attrs:{".":{"text-anchor":"middle"}}})},bottom:function(a,b,c){return e(c,{y:15,attrs:{".":{y:".6em","text-anchor":"middle"}}})},outsideOriented:function(a,b,c){return f(a,b,!0,c)},outside:function(a,b,c){return f(a,b,!1,c)},insideOriented:function(a,b,c){return h(a,b,!0,c)},inside:function(a,b,c){return h(a,b,!1,c)},radial:function(a,b,c){return i(a.difference(b.center()),!1,c)},radialOriented:function(a,b,c){return i(a.difference(b.center()),!0,c)}}}(_,g,joint,joint.util),joint.highlighters.addClass={className:joint.util.addClassNamePrefix("highlighted"),highlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).addClass(e)},unhighlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).removeClass(e)}},joint.highlighters.opacity={highlight:function(a,b){V(b).addClass(joint.util.addClassNamePrefix("highlight-opacity"))},unhighlight:function(a,b){V(b).removeClass(joint.util.addClassNamePrefix("highlight-opacity"))}},joint.highlighters.stroke={defaultOptions:{padding:3,rx:0,ry:0,attrs:{"stroke-width":3,stroke:"#FEB663"}},_views:{},getHighlighterId:function(a,b){return a.id+JSON.stringify(b)},removeHighlighter:function(a){this._views[a]&&(this._views[a].remove(),this._views[a]=null)},highlight:function(a,b,c){var d=this.getHighlighterId(b,c);if(!this._views[d]){var e,f=joint.util.defaults(c||{},this.defaultOptions),g=V(b);try{var h=g.convertToPathData()}catch(a){e=g.bbox(!0),h=V.rectToPath(joint.util.assign({},f,e))}var i=V("path").attr({d:h,"pointer-events":"none","vector-effect":"non-scaling-stroke",fill:"none"}).attr(f.attrs),j=g.getTransformToElement(a.el),k=f.padding;if(k){e||(e=g.bbox(!0));var l=e.x+e.width/2,m=e.y+e.height/2;e=V.transformRect(e,j);var n=Math.max(e.width,1),o=Math.max(e.height,1),p=(n+k)/n,q=(o+k)/o,r=V.createSVGMatrix({a:p,b:0,c:0,d:q,e:l-p*l,f:m-q*m});j=j.multiply(r)}i.transform(j);var s=this._views[d]=new joint.mvc.View({svgElement:!0,className:"highlight-stroke",el:i.node}),t=this.removeHighlighter.bind(this,d),u=a.model;s.listenTo(u,"remove",t),s.listenTo(u.graph,"reset",t),a.vel.append(i)}},unhighlight:function(a,b,c){this.removeHighlighter(this.getHighlighterId(b,c))}},function(a,b){function c(a){return function(c,d,e,f){var g=!!f.rotate,h=g?c.getNodeUnrotatedBBox(d):c.getNodeBBox(d),i=h[a](),j=f.dx;if(j){var k=b.isPercentage(j);j=parseFloat(j),isFinite(j)&&(k&&(j/=100,j*=h.width),i.x+=j)}var l=f.dy;if(l){var m=b.isPercentage(l);l=parseFloat(l),isFinite(l)&&(m&&(l/=100,l*=h.height),i.y+=l)}return g?i.rotate(c.model.getBBox().center(),-c.model.angle()):i}}function d(a){return function(b,c,d,e){if(d instanceof Element){var f=this.paper.findView(d),g=f.getNodeBBox(d).center();return a.call(this,b,c,g,e)}return a.apply(this,arguments)}}function e(a,b,c,d){var e=a.model.angle(),f=a.getNodeBBox(b),h=f.center(),i=f.origin(),j=f.corner(),k=d.padding;if(isFinite(k)||(k=0),i.y+k<=c.y&&c.y<=j.y-k){var l=c.y-h.y;h.x+=0===e||180===e?0:1*l/Math.tan(g.toRad(e)),h.y+=l}else if(i.x+k<=c.x&&c.x<=j.x-k){var m=c.x-h.x;h.y+=90===e||270===e?0:m*Math.tan(g.toRad(e)),h.x+=m}return h}function f(a,b,c,d){var e,f,g,h=!!d.rotate;h?(e=a.getNodeUnrotatedBBox(b),g=a.model.getBBox().center(),f=a.model.angle()):e=a.getNodeBBox(b);var i=d.padding;isFinite(i)&&e.inflate(i),h&&c.rotate(g,f);var j,k=e.sideNearestToPoint(c);switch(k){case"left":j=e.leftMiddle();break;case"right":j=e.rightMiddle();break;case"top":j=e.topMiddle();break;case"bottom":j=e.bottomMiddle()}return h?j.rotate(g,-f):j}function h(a,b){var c=a.model,d=c.getBBox(),e=d.center(),f=c.angle(),h=a.findAttribute("port",b);if(h){portGroup=c.portProp(h,"group");var i=c.getPortsPositions(portGroup),j=new g.Point(i[h]).offset(d.origin());return j.rotate(e,-f),j}return e}a.anchors={center:c("center"),top:c("topMiddle"),bottom:c("bottomMiddle"),left:c("leftMiddle"),right:c("rightMiddle"),topLeft:c("origin"),topRight:c("topRight"),bottomLeft:c("bottomLeft"),bottomRight:c("corner"),perpendicular:d(e),midSide:d(f),modelCenter:h}}(joint,joint.util),function(a,b,c,d){function e(a,c){return 1===a.length?a[0]:b.sortBy(a,function(a){return a.squaredDistance(c)})[0]}function f(a,b,c){if(!isFinite(c))return a;var d=a.distance(b);return 0===c&&d>0?a:a.move(b,-Math.min(c,d-1))}function g(a){var b=a.getAttribute("stroke-width");return null===b?0:parseFloat(b)||0}function h(a,b,c,d){return f(a.end,a.start,d.offset)}function i(a,b,c,d){var h=b.getNodeBBox(c);d.stroke&&h.inflate(g(c)/2);var i=a.intersect(h),j=i?e(i,a.start):a.end;return f(j,a.start,d.offset)}function j(a,b,c,d){var h=b.model.angle();if(0===h)return i(a,b,c,d);var j=b.getNodeUnrotatedBBox(c);d.stroke&&j.inflate(g(c)/2);var k=j.center(),l=a.clone().rotate(k,h),m=l.setLength(1e6).intersect(j),n=m?e(m,l.start).rotate(k,-h):a.end;return f(n,a.start,d.offset)}function k(a,h,i,j){var k,n,o=j.selector,p=a.end;if("string"==typeof o)k=h.findBySelector(o)[0];else if(Array.isArray(o))k=b.getByPath(i,o);else{k=i;do{var q=k.tagName.toUpperCase();if("G"===q)k=k.firstChild;else{if("TITLE"!==q)break;k=k.nextSibling}}while(k)}if(!(k instanceof Element))return p;var r=h.getNodeShape(k),s=h.getNodeMatrix(k),t=h.getRootTranslateMatrix(),u=h.getRootRotateMatrix(),v=t.multiply(u).multiply(s),w=v.inverse(),x=d.transformLine(a,w),y=x.start.clone(),z=h.getNodeData(k);if(j.insideout===!1){z[m]||(z[m]=r.bbox());var A=z[m];if(A.containsPoint(y))return p}var B;if(r instanceof c.Path){var C=j.precision||2;z[l]||(z[l]=r.getSegmentSubdivisions({precision:C})),segmentSubdivisions=z[l],B={precision:C,segmentSubdivisions:z[l]}}j.extrapolate===!0&&x.setLength(1e6),n=x.intersect(r,B),n?d.isArray(n)&&(n=e(n,y)):j.sticky===!0&&(n=r instanceof c.Rect?r.pointNearestToPoint(y):r instanceof c.Ellipse?r.intersectionWithLineFromCenterToPoint(y):r.closestPoint(y,B));var D=n?d.transformPoint(n,v):p,E=j.offset||0;return j.stroke&&(E+=g(k)/2),f(D,a.start,E)}var l="segmentSubdivisons",m="shapeBBox";a.connectionPoints={anchor:h,bbox:i,rectangle:j,boundary:k}}(joint,joint.util,g,V),function(a,b){function c(a,b){return 0===b?"0%":Math.round(a/b*100)+"%"}function d(a){return function(b,d,e,f){var g=d.model.angle(),h=d.getNodeUnrotatedBBox(e),i=d.model.getBBox().center();f.rotate(i,g);var j=f.x-h.x,k=f.y-h.y;return a&&(j=c(j,h.width),k=c(k,h.height)),b.anchor={name:"topLeft",args:{dx:j,dy:k,rotate:!0}},b}}a.connectionStrategies={useDefaults:b.noop,pinAbsolute:d(!1),pinRelative:d(!0)}}(joint,joint.util),function(a,b,c,d){function e(b,c,d){var e=a.connectionStrategies.pinRelative.call(this.paper,{},c,d,b,this.model);return e.anchor}function f(a,b,c,d,e,f){var g=f.options.snapRadius,h="source"===d,i=h?0:-1,j=this.model.vertex(i)||this.getEndAnchor(h?"target":"source");return j&&(Math.abs(j.x-a.x)0?c[a-1]:b.sourceAnchor,f=a0){var d=this.getNeighborPoints(b),e=d.prev,f=d.next;Math.abs(a.x-e.x) element of the joint.dia.Link that follows the .connection of that link. diff --git a/dist/joint.d.ts b/dist/joint.d.ts index f249fb68c..0edb9a09d 100644 --- a/dist/joint.d.ts +++ b/dist/joint.d.ts @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -25,20 +25,117 @@ export as namespace joint; export namespace g { export interface PlainPoint { + x: number; y: number; } export interface PlainRect { + x: number; y: number; width: number; height: number; } + export interface Scale { + + sx: number; + sy: number; + } + + export interface PrecisionOpt { + + precision?: number; + } + + export interface SubdivisionsOpt extends PrecisionOpt { + + subdivisions?: Curve[]; + } + + export interface SegmentSubdivisionsOpt extends PrecisionOpt { + + segmentSubdivisions?: Curve[][]; + } + + export interface PathT { + + segmentIndex: number; + value: number; + } + + export interface Segment { + + type: SegmentType; + + isSegment: boolean; + isSubpathStart: boolean; + isVisible: boolean; + + nextSegment: Segment | null; + previousSegment: Segment | null; + subpathStartSegment: Segment | null; + + start: Point | null | never; // getter, `never` for Moveto + end: Point | null; // getter or directly assigned + + bbox(): Rect | null; + + clone(): Segment; + + closestPoint(p: Point, opt?: SubdivisionsOpt): Point; + + closestPointLength(p: Point, opt?: SubdivisionsOpt): number; + + closestPointNormalizedLength(p: Point, opt?: SubdivisionsOpt): number; + + closestPointT(p: Point): number; + + closestPointTangent(p: Point): Line | null; + + equals(segment: Segment): boolean; + + getSubdivisions(): Curve[]; + + isDifferentiable(): boolean; + + length(): number; + + lengthAtT(t: number, opt?: PrecisionOpt): number; + + pointAt(ratio: number): Point; + + pointAtLength(length: number): Point; + + pointAtT(t: number): Point; + + scale(sx: number, sy: number, origin?: PlainPoint): this; + + tangentAt(ratio: number): Line | null; + + tangentAtLength(length: number): Line | null; + + tangentAtT(t: number): Line | null; + + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + + serialize(): string; + + toString(): string; + } + + export interface SegmentTypes { + + [key: string]: Segment; + } + type CardinalDirection = 'NE' | 'E' | 'SE' | 'S' | 'SW' | 'W' | 'NW' | 'N'; - type RectangleSides = 'left' | 'right' | 'top' | 'bottom'; + type RectangleSide = 'left' | 'right' | 'top' | 'bottom'; + + type SegmentType = 'L' | 'C' | 'M' | 'Z'; export function normalizeAngle(angle: number): number; @@ -48,6 +145,76 @@ export namespace g { export function toRad(deg: number, over360?: boolean): number; + class Curve { + + start: Point; + controlPoint1: Point; + controlPoint2: Point; + end: Point; + + constructor(p1: PlainPoint | string, p2: PlainPoint | string, p3: PlainPoint | string, p4: PlainPoint | string); + constructor(curve: Curve); + + bbox(): Rect; + + clone(): Curve; + + closestPoint(p: PlainPoint, opt?: SubdivisionsOpt): Point; + + closestPointLength(p: PlainPoint, opt?: SubdivisionsOpt): number; + + closestPointNormalizedLength(p: PlainPoint, opt?: SubdivisionsOpt): number; + + closestPointT(p: PlainPoint, opt?: SubdivisionsOpt): number; + + closestPointTangent(p: PlainPoint, opt?: SubdivisionsOpt): Line | null; + + divide(t: number): [Curve, Curve]; + + endpointDistance(): number; + + equals(c: Curve): boolean; + + getSkeletonPoints(t: number): [Point, Point, Point, Point, Point]; + + getSubdivisions(opt?: PrecisionOpt): Curve[]; + + isDifferentiable(): boolean; + + length(opt?: SubdivisionsOpt): number; + + lengthAtT(t: number, opt?: PrecisionOpt): number; + + pointAt(ratio: number, opt?: SubdivisionsOpt): Point; + + pointAtLength(length: number, opt?: SubdivisionsOpt): Point; + + pointAtT(t: number): Point; + + scale(sx: number, sy: number, origin?: PlainPoint | string): this; + + tangentAt(ratio: number, opt?: SubdivisionsOpt): Line | null; + + tangentAtLength(length: number, opt?: SubdivisionsOpt): Line | null; + + tangentAtT(t: number): Line | null; + + tAt(ratio: number, opt?: SubdivisionsOpt): number; + + tAtLength(length: number, opt?: SubdivisionsOpt): number; + + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + + toPoints(opt?: SubdivisionsOpt): Point[]; + + toPolyline(opt?: SubdivisionsOpt): Polyline; + + toString(): string; + + static throughPoints(points: PlainPoint[]): Curve[]; + } + class Ellipse { x: number; @@ -55,7 +222,7 @@ export namespace g { a: number; b: number; - constructor(center: PlainPoint, a: number, b: number); + constructor(center: PlainPoint | string, a: number, b: number); constructor(ellipse: Ellipse); bbox(): Rect; @@ -74,6 +241,8 @@ export namespace g { equals(ellipse: Ellipse): boolean; + intersectionWithLine(l: Line): Point[] | null; + intersectionWithLineFromCenterToPoint(p: PlainPoint, angle?: number): Point; toString(): string; @@ -88,15 +257,33 @@ export namespace g { constructor(p1: PlainPoint | string, p2: PlainPoint | string); constructor(line: Line); + constructor(); + + bbox(): Rect; bearing(): CardinalDirection; clone(): Line; + closestPoint(p: PlainPoint | string): Point; + + closestPointLength(p: PlainPoint | string): number; + + closestPointNormalizedLength(p: PlainPoint | string): number; + + closestPointTangent(p: PlainPoint | string): Line | null; + equals(line: Line): boolean; - intersect(line: Line): Point | null; + intersect(line: Line): Point | null; // Backwards compatibility, should return an array intersect(rect: Rect): Point[] | null; + intersect(ellipse: Ellipse): Point[] | null; + intersect(polyline: Polyline): Point[] | null; + intersect(path: Path, opt?: SegmentSubdivisionsOpt): Point[] | null; + + intersectionWithLine(l: Line): Point[] | null; + + isDifferentiable(): boolean; length(): number; @@ -104,17 +291,133 @@ export namespace g { pointAt(t: number): Point; - pointOffset(p: PlainPoint): number; + pointAtLength(length: number): Point; - vector(): Point; + pointOffset(p: PlainPoint | string): number; - closestPoint(p: PlainPoint | string): Point; + rotate(origin: PlainPoint, angle: number): this; - closestPointNormalizedLength(p: PlainPoint | string): number; + round(precision?: number): this; + + scale(sx: number, sy: number, origin?: PlainPoint): this; + + setLength(length: number): this; squaredLength(): number; + tangentAt(t: number): Line | null; + + tangentAtLength(length: number): Line | null; + + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + + vector(): Point; + + toString(): string; + } + + class Path { + + segments: Segment[]; + + start: Point | null; // getter + end: Point | null; // getter + + constructor(); + constructor(pathData: string); + constructor(segments: Segment[]); + constructor(objects: (Line | Curve)[]); + constructor(segment: Segment); + constructor(line: Line); + constructor(curve: Curve); + constructor(polyline: Polyline); + + appendSegment(segment: Segment): void; + appendSegment(segments: Segment[]): void; + + bbox(): Rect | null; + + clone(): Path; + + closestPoint(p: Point, opt?: SegmentSubdivisionsOpt): Point | null; + + closestPointLength(p: Point, opt?: SegmentSubdivisionsOpt): number; + + closestPointNormalizedLength(p: Point, opt?: SegmentSubdivisionsOpt): number; + + closestPointTangent(p: Point, opt?: SegmentSubdivisionsOpt): Line | null; + + equals(p: Path): boolean; + + getSegment(index: number): Segment | null; + + getSegmentSubdivisions(opt?: PrecisionOpt): Curve[][]; + + insertSegment(index: number, segment: Segment): void; + insertSegment(index: number, segments: Segment[]): void; + + intersectionWithLine(l: Line, opt?: SegmentSubdivisionsOpt): Point[] | null; + + isDifferentiable(): boolean; + + isValid(): boolean; + + length(opt?: SegmentSubdivisionsOpt): number; + + pointAt(ratio: number, opt?: SegmentSubdivisionsOpt): Point | null; + + pointAtLength(length: number, opt?: SegmentSubdivisionsOpt): Point | null; + + removeSegment(index: number): void; + + replaceSegment(index: number, segment: Segment): void; + replaceSegment(index: number, segments: Segment[]): void; + + scale(sx: number, sy: number, origin?: PlainPoint | string): this; + + segmentAt(ratio: number, opt?: SegmentSubdivisionsOpt): Segment | null; + + segmentAtLength(length: number, opt?: SegmentSubdivisionsOpt): Segment | null; + + segmentIndexAt(ratio: number, opt?: SegmentSubdivisionsOpt): number | null; + + segmentIndexAtLength(length: number, opt?: SegmentSubdivisionsOpt): number | null; + + tangentAt(ratio: number, opt?: SegmentSubdivisionsOpt): Line | null; + + tangentAtLength(length: number, opt?: SegmentSubdivisionsOpt): Line | null; + + toPoints(opt?: SegmentSubdivisionsOpt): Point[][] | null; + + toPolylines(opt?: SegmentSubdivisionsOpt): Polyline[] | null; + + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + + serialize(): string; + toString(): string; + + private closestPointT(p: Point, opt?: SegmentSubdivisionsOpt): PathT | null; + + private lengthAtT(t: PathT, opt?: SegmentSubdivisionsOpt): number; + + private pointAtT(t: PathT): Point | null; + + private tangentAtT(t: PathT): Line | null; + + private prepareSegment(segment: Segment, previousSegment?: Segment | null, nextSegment?: Segment | null): Segment; + + private updateSubpathStartSegment(segment: Segment): void; + + static createSegment(type: SegmentType, ...args: any[]): Segment; + + static parse(pathData: string): Path; + + static segmentTypes: SegmentTypes; + + static isDataSupported(pathData: string): boolean; } class Point implements PlainPoint { @@ -122,7 +425,7 @@ export namespace g { x: number; y: number; - constructor(x: number, y: number); + constructor(x?: number, y?: number); constructor(p: PlainPoint | string); adhereToRect(r: Rect): this; @@ -133,7 +436,7 @@ export namespace g { clone(): Point; - difference(dx: number, dy?: number): Point; + difference(dx?: number, dy?: number): Point; difference(p: PlainPoint): Point; distance(p: PlainPoint | string): number; @@ -142,6 +445,8 @@ export namespace g { equals(p: Point): boolean; + lerp(p: Point, t: number): Point; + magnitude(): number; manhattanDistance(p: PlainPoint): number; @@ -150,7 +455,7 @@ export namespace g { normalize(length: number): this; - offset(dx: number, dy?: number): this; + offset(dx?: number, dy?: number): this; offset(p: PlainPoint): this; reflection(ref: PlainPoint | string): Point; @@ -165,17 +470,20 @@ export namespace g { theta(p: PlainPoint | string): number; + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + angleBetween(p1: PlainPoint, p2: PlainPoint) : number; vectorAngle(p: PlainPoint) : number; toJSON(): PlainPoint; - toPolar(origin: PlainPoint | string): this; + toPolar(origin?: PlainPoint | string): this; toString(): string; - update(x: number, y?: number): this; + update(x?: number, y?: number): this; dot(p: PlainPoint): number; @@ -186,6 +494,59 @@ export namespace g { static random(x1: number, x2: number, y1: number, y2: number): Point; } + class Polyline { + + points: Point[]; + + start: Point | null; // getter + end: Point | null; // getter + + constructor(); + constructor(svgString: string); + constructor(points: Point[]); + + bbox(): Rect | null; + + clone(): Polyline; + + closestPoint(p: PlainPoint | string): Point | null; + + closestPointLength(p: PlainPoint | string): number; + + closestPointNormalizedLength(p: PlainPoint | string): number; + + closestPointTangent(p: PlainPoint | string): Line | null; + + convexHull(): Polyline; + + equals(p: Polyline): boolean; + + isDifferentiable(): boolean; + + intersectionWithLine(l: Line): Point[] | null; + + length(): number; + + pointAt(ratio: number): Point | null; + + pointAtLength(length: number): Point | null; + + scale(sx: number, sy: number, origin?: PlainPoint | string): this; + + tangentAt(ratio: number): Line | null; + + tangentAtLength(length: number): Line | null; + + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + + serialize(): string; + + toString(): string; + + static parse(svgString: string): Polyline; + } + class Rect implements PlainRect { x: number; @@ -204,6 +565,8 @@ export namespace g { bottomMiddle(): Point; + bottomRight(): Point; + center(): Point; clone(): Rect; @@ -218,6 +581,8 @@ export namespace g { intersect(r: Rect): Rect | null; + intersectionWithLine(l: Line): Point[] | null; + intersectionWithLineFromCenterToPoint(p: PlainPoint | string, angle?: number): Point; leftLine(): Line; @@ -226,7 +591,7 @@ export namespace g { moveAndExpand(r: PlainRect): this; - offset(dx: number, dy?: number): this; + offset(dx?: number, dy?: number): this; offset(p: PlainPoint): this; inflate(dx?: number, dy?: number): this; @@ -245,16 +610,25 @@ export namespace g { scale(sx: number, sy: number, origin?: PlainPoint | string): this; - sideNearestToPoint(point: PlainPoint | string): RectangleSides; + maxRectScaleToFit(rect: PlainRect, origin?: PlainPoint): Scale; + + maxRectUniformScaleToFit(rect: PlainRect, origin?: PlainPoint): number; + + sideNearestToPoint(point: PlainPoint | string): RectangleSide; snapToGrid(gx: number, gy?: number): this; + topLeft(): Point; + topLine(): Line; topMiddle(): Point; topRight(): Point; + translate(tx?: number, ty?: number): this; + translate(tx: PlainPoint): this; + toJSON(): PlainRect; toString(): string; @@ -317,9 +691,12 @@ export namespace Vectorizer { offset?: number; } + type TextVerticalAnchor = 'top' | 'bottom' | 'middle'; + interface TextOptions { eol?: string; - x?: number; + x?: number | string; + textVerticalAnchor?: TextVerticalAnchor | number | string; lineHeight?: number | string; textPath?: string | { [key: string]: any }; annotations?: TextAnnotation[]; @@ -411,15 +788,16 @@ export namespace Vectorizer { export class Vectorizer { + id: string; node: SVGElement; constructor( - svg: string | SVGElement, + el: string | SVGElement, attrs?: { [key: string]: any }, children?: Vectorizer | Vectorizer[] | SVGElement | SVGElement[] ); - getTransformToElement(elem: SVGGElement | Vectorizer): SVGMatrix; + getTransformToElement(toElem: SVGGElement | Vectorizer): SVGMatrix; transform(): SVGMatrix; transform(matrix: SVGMatrix | Vectorizer.Matrix, opt?: Vectorizer.TransformOptions): this; @@ -431,7 +809,7 @@ export class Vectorizer { rotate(angle: number, cx?: number, cy?: number, opt?: Vectorizer.RotateOptions): this; scale(): Vectorizer.Scale; - scale(sx: number, sy: number): this; + scale(sx: number, sy?: number): this; bbox(withoutTransformations?: boolean, target?: SVGElement | Vectorizer): g.Rect; @@ -446,6 +824,8 @@ export class Vectorizer { attr(name: string, value: any): this; attr(attrs: { [key: string]: any }): this; + normalizePath(): this; + remove(): this; empty(): this; @@ -461,6 +841,8 @@ export class Vectorizer { // returns either this or Vectorizer, no point in specifying this. svg(): Vectorizer; + tagName(): string; + defs(): Vectorizer | undefined; clone(): Vectorizer; @@ -555,6 +937,10 @@ export class Vectorizer { static transformPoint(p: g.PlainPoint, matrix: SVGMatrix): g.Point; + static transformLine(p: g.Line, matrix: SVGMatrix): g.Line; + + static transformPolyline(p: g.Polyline | g.PlainPoint[], matrix: SVGMatrix): g.Polyline; + static styleToObject(styleString: string): { [key: string]: string }; static createSlicePathData(innerRadius: number, outRadius: number, startAngle: number, endAngle: number): string; @@ -587,6 +973,8 @@ export class Vectorizer { static rectToPath(r: Vectorizer.RoundedRect): string; + static normalizePathData(path: string): string; + static toNode(el: SVGElement | Vectorizer | SVGElement[]): SVGElement; } @@ -611,8 +999,24 @@ export namespace dia { 'left' | 'right' | 'top' | 'bottom' | 'top-right' | 'top-left' | 'bottom-left' | 'bottom-right'; + type MarkupNodeJSON = { + tagName: string; + selector?: string; + namespaceUri?: string; + className?: string; + attributes?: attributes.NativeSVGAttributes; + style?: { [key: string]: any }; + children?: MarkupJSON + } + + type MarkupJSON = MarkupNodeJSON[]; + export namespace Graph { + interface Options { + [key: string]: any; + } + interface ConnectionOptions extends Cell.EmbeddableOptions { inbound?: boolean; outbound?: boolean; @@ -707,10 +1111,12 @@ export namespace dia { getCellsBBox(cells: Cell[], opt?: Cell.EmbeddableOptions): g.Rect | null; - hasActiveBatch(name?: string): boolean; + hasActiveBatch(name?: string | string[]): boolean; maxZIndex(): number; + minZIndex(): number; + removeCells(cells: Cell[], opt?: Cell.DisconnectableOptions): this; resize(width: number, height: number, opt?: { [key: string]: any }): this; @@ -733,10 +1139,11 @@ export namespace dia { interface GenericAttributes { attrs?: T; z?: number; + [key: string]: any; } interface Selectors { - [selector: string]: attributes.SVGAttributes; + [selector: string]: attributes.SVGAttributes | undefined; } interface Attributes extends GenericAttributes { @@ -744,10 +1151,14 @@ export namespace dia { } interface Constructor { - new (options?: { id: string }): T + new (opt?: { id: string }): T + } + + interface Options { + [key: string]: any; } - interface EmbeddableOptions { + interface EmbeddableOptions extends Options { deep?: boolean; } @@ -755,7 +1166,7 @@ export namespace dia { disconnectLinks?: boolean; } - interface TransitionOptions { + interface TransitionOptions extends Options { delay?: number; duration?: number; timingFunction?: util.timing.TimingFunction; @@ -765,7 +1176,7 @@ export namespace dia { class Cell extends Backbone.Model { - constructor(attributes?: Cell.Attributes, opt?: { [key: string]: any }); + constructor(attributes?: Cell.Attributes, opt?: Graph.Options); id: string | number; @@ -779,6 +1190,11 @@ export namespace dia { toBack(opt?: Cell.EmbeddableOptions): this; + parent(): string; + parent(parentId: string): this; + + getParentCell(): Cell | null; + getAncestors(): Cell[]; getEmbeddedCells(opt?: { deep?: boolean, breadthFirst?: boolean }): Cell[]; @@ -788,19 +1204,19 @@ export namespace dia { isEmbedded(): boolean; prop(key: string | string[]): any; - prop(object: Cell.Attributes): this; - prop(key: string | string[], value: any, opt?: { [key: string]: any }): this; + prop(object: Cell.Attributes, opt?: Cell.Options): this; + prop(key: string | string[], value: any, opt?: Cell.Options): this; - removeProp(path: string | string[], opt?: { [key: string]: any }): this; + removeProp(path: string | string[], opt?: Cell.Options): this; attr(key?: string): any; - attr(object: Cell.Selectors): this; - attr(key: string, value: any): this; + attr(object: Cell.Selectors, opt?: Cell.Options): this; + attr(key: string, value: any, opt?: Cell.Options): this; clone(): Cell; clone(opt: Cell.EmbeddableOptions): Cell | Cell[]; - removeAttr(path: string | string[], opt?: { [key: string]: any }): this; + removeAttr(path: string | string[], opt?: Cell.Options): this; transition(path: string, value?: any, opt?: Cell.TransitionOptions, delim?: string): number; @@ -808,11 +1224,11 @@ export namespace dia { stopTransitions(path?: string, delim?: string): this; - embed(cell: Cell, opt?: { [key: string]: any }): this; + embed(cell: Cell, opt?: Graph.Options): this; - unembed(cell: Cell, opt?: { [key: string]: any }): this; + unembed(cell: Cell, opt?: Graph.Options): this; - addTo(graph: Graph, opt?: { [key: string]: any }): this; + addTo(graph: Graph, opt?: Graph.Options): this; findView(paper: Paper): CellView; @@ -820,9 +1236,9 @@ export namespace dia { isElement(): boolean; - startBatch(name: string, opt?: { [key: string]: any }): this; + startBatch(name: string, opt?: Graph.Options): this; - stopBatch(name: string, opt?: { [key: string]: any }): this; + stopBatch(name: string, opt?: Graph.Options): this; static define(type: string, defaults?: any, protoProps?: any, staticProps?: any): Cell.Constructor; @@ -837,11 +1253,12 @@ export namespace dia { export namespace Element { interface GenericAttributes extends Cell.GenericAttributes { + markup?: string | MarkupJSON; position?: Point; size?: Size; angle?: number; ports?: { - groups?: { [key: string]: Port }, + groups?: { [key: string]: PortGroup}, items?: Port[] } } @@ -850,18 +1267,30 @@ export namespace dia { [key: string]: any } + type PositionType = string | { + name?: string, + args?: { [key: string]: any } + } + + interface PortGroup { + position?: PositionType, + markup?: string; + attrs?: Cell.Selectors; + label?: { + markup?: string; + position?: PositionType; + } + } + interface Port { id?: string; markup?: string; group?: string; attrs?: Cell.Selectors; args?: { [key: string]: any }; - size?: Size; label?: { - size?: Size; markup?: string; - position?: any; - args?: any; + position?: PositionType; } z?: number | 'auto'; } @@ -878,7 +1307,11 @@ export namespace dia { class Element extends Cell { - constructor(attributes?: Element.Attributes, opt?: { [key: string]: any }); + constructor(attributes?: Element.Attributes, opt?: Graph.Options); + + isElement(): boolean; + + isLink(): boolean; translate(tx: number, ty?: number, opt?: Element.TranslateOptions): this; @@ -892,17 +1325,19 @@ export namespace dia { rotate(deg: number, absolute?: boolean, origin?: Point, opt?: { [key: string]: any }): this; + angle(): number; + scale(scaleX: number, scaleY: number, origin?: Point, opt?: { [key: string]: any }): this; fitEmbeds(opt?: { deep?: boolean, padding?: Padding }): this; getBBox(opt?: Cell.EmbeddableOptions): g.Rect; - addPort(port: Element.Port, opt?: { [key: string]: any }): this; + addPort(port: Element.Port, opt?: Cell.Options): this; - addPorts(ports: Element.Port[], opt?: { [key: string]: any }): this; + addPorts(ports: Element.Port[], opt?: Cell.Options): this; - removePort(port: string | Element.Port, opt?: { [key: string]: any }): this; + removePort(port: string | Element.Port, opt?: Cell.Options): this; hasPorts(): boolean; @@ -916,7 +1351,7 @@ export namespace dia { getPortIndex(port: string | Element.Port): number; - portProp(portId: string, path: any, value?: any, opt?: { [key: string]: any }): Element; + portProp(portId: string, path: any, value?: any, opt?: Cell.Options): Element; static define(type: string, defaults?: any, protoProps?: any, staticProps?: any): Cell.Constructor; } @@ -925,14 +1360,32 @@ export namespace dia { export namespace Link { + interface EndCellArgs { + magnet?: string; + selector?: string; + port?: string; + anchor?: anchors.AnchorJSON; + connectionPoint?: connectionPoints.ConnectionPointJSON; + } + + interface EndCellJSON extends EndCellArgs { + id: number | string; + } + + interface EndPointJSON { + x: number; + y: number; + } + interface GenericAttributes extends Cell.GenericAttributes { - source?: Point | { id: string, selector?: string, port?: string }; - target?: Point | { id: string, selector?: string, port?: string }; + source?: EndCellJSON | EndPointJSON; + target?: EndCellJSON | EndPointJSON; labels?: Label[]; vertices?: Point[]; + manhattan?: boolean; + router?: routers.Router | routers.RouterJSON; smooth?: boolean; - router?: routers.RouterJSON; - connector?: connectors.ConnectorJSON; + connector?: connectors.Connector | connectors.ConnectorJSON; } interface LinkSelectors extends Cell.Selectors { @@ -951,33 +1404,80 @@ export namespace dia { } interface LabelPosition { - distance: number; - offset: number | { x: number; y: number; } + distance?: number; // optional for default labels + offset?: number | { x: number; y: number; }; + args?: LinkView.LabelOptions; } interface Label { - position: LabelPosition | number; + markup?: string; // default labels + position?: LabelPosition | number; // optional for default labels attrs?: Cell.Selectors; size?: Size; } + + interface Vertex extends Point { + [key: string]: any; + } } class Link extends Cell { markup: string; - labelMarkup: string; toolMarkup: string; + doubleToolMarkup?: string; vertexMarkup: string; arrowHeadMarkup: string; + labelMarkup?: string; // default label markup + labelProps?: Link.Label; // default label props + + constructor(attributes?: Link.Attributes, opt?: Graph.Options); - constructor(attributes?: Link.Attributes, opt?: { [key: string]: any }); + isElement(): boolean; + + isLink(): boolean; disconnect(): this; - label(index?: number): any; - label(index: number, value: Link.Label, opt?: { [key: string]: any }): this; + source(): Link.EndCellJSON | Link.EndPointJSON; + source(source: Link.EndCellJSON | Link.EndPointJSON, opt?: Cell.Options): this; + source(source: Cell, args?: Link.EndCellArgs, opt?: Cell.Options): this; + + target(): Link.EndCellJSON | Link.EndPointJSON; + target(target: Link.EndCellJSON | Link.EndPointJSON, opt?: Cell.Options): this; + target(target: Cell, args?: Link.EndCellArgs, opt?: Cell.Options): this; + + router(): routers.Router | routers.RouterJSON | null; + router(router: routers.Router | routers.RouterJSON, opt?: Cell.Options): this; + router(name: routers.RouterType, args?: routers.RouterArguments, opt?: Cell.Options): this; + + connector(): connectors.Connector | connectors.ConnectorJSON | null; + connector(connector: connectors.Connector | connectors.ConnectorJSON, opt?: Cell.Options): this; + connector(name: connectors.ConnectorType, args?: connectors.ConnectorArguments, opt?: Cell.Options): this; + + label(index?: number): Link.Label; + label(index: number, label: Link.Label, opt?: Cell.Options): this; + + labels(): Link.Label[]; + labels(labels: Link.Label[]): this; + + insertLabel(index: number, label: Link.Label, opt?: Cell.Options): Link.Label[]; + + appendLabel(label: Link.Label, opt?: Cell.Options): Link.Label[]; + + removeLabel(index?: number, opt?: Cell.Options): Link.Label[]; + + vertex(index?: number): Link.Vertex; + vertex(index: number, vertex: Link.Vertex, opt?: Cell.Options): this; + + vertices(): Link.Vertex[]; + vertices(vertices: Link.Vertex[]): this; + + insertVertex(index: number, vertex: Link.Vertex, opt?: Cell.Options): Link.Vertex[]; - reparent(opt?: { [key: string]: any }): Element; + removeVertex(index?: number, opt?: Cell.Options): Link.Vertex[]; + + reparent(opt?: Cell.Options): Element; getSourceElement(): null | Element; @@ -989,11 +1489,11 @@ export namespace dia { isRelationshipEmbeddedIn(cell: Cell): boolean; - applyToPoints(fn: (p: Point) => Point, opt?: { [key: string]: any }): this; + applyToPoints(fn: (p: Point) => Point, opt?: Cell.Options): this; - scale(sx: number, sy: number, origin?: Point, opt?: { [key: string]: any }): this; + scale(sx: number, sy: number, origin?: Point, opt?: Cell.Options): this; - translate(tx: number, ty: number, opt?: { [key: string]: any }): this; + translate(tx: number, ty: number, opt?: Cell.Options): this; static define(type: string, defaults?: any, protoProps?: any, staticProps?: any): Cell.Constructor; } @@ -1025,25 +1525,55 @@ export namespace dia { findBySelector(selector: string, root?: SVGElement | JQuery | string): JQuery; + findAttribute(attributeName: string, node: Element): string | null; + getSelector(el: SVGElement, prevSelector?: string): string; getStrokeBBox(el?: SVGElement): g.Rect; notify(eventName: string, ...eventArguments: any[]): void; - protected mouseover(evt: JQuery.Event): void; + addTools(tools: dia.ToolsView): this; - protected mousewheel(evt: JQuery.Event, x: number, y: number, delta: number): void + hasTools(name?: string): boolean; - protected pointerclick(evt: JQuery.Event, x: number, y: number): void; + removeTools(): this; + + showTools(): this; + + hideTools(): this; + + updateTools(opt?: { [key: string]: any }): this; + + protected onToolEvent(eventName: string): void; protected pointerdblclick(evt: JQuery.Event, x: number, y: number): void; + protected pointerclick(evt: JQuery.Event, x: number, y: number): void; + + protected contextmenu(evt: JQuery.Event, x: number, y: number): void; + protected pointerdown(evt: JQuery.Event, x: number, y: number): void; protected pointermove(evt: JQuery.Event, x: number, y: number): void; protected pointerup(evt: JQuery.Event, x: number, y: number): void; + + protected mouseover(evt: JQuery.Event): void; + + protected mouseout(evt: JQuery.Event): void; + + protected mouseenter(evt: JQuery.Event): void; + + protected mouseleave(evt: JQuery.Event): void; + + protected mousewheel(evt: JQuery.Event, x: number, y: number, delta: number): void; + + protected onevent(evt: JQuery.Event, eventName: string, x: number, y: number): void; + + protected onmagnet(evt: JQuery.Event, x: number, y: number): void; + + static dispatchToolsEvent(paper: dia.Paper, eventName: string): void; } class CellView extends CellViewGeneric { @@ -1065,11 +1595,31 @@ export namespace dia { getBBox(opt?: { useModelGeometry?: boolean }): g.Rect; + getNodeBBox(magnet: SVGElement): g.Rect; + + getNodeUnrotatedBBox(magnet: SVGElement): g.Rect; + update(element: Element, renderingOnlyAttrs?: { [key: string]: any }): void; setInteractivity(value: boolean | ElementView.InteractivityOptions): void; protected renderMarkup(): void; + + protected renderJSONMarkup(markup: MarkupJSON): void; + + protected renderStringMarkup(markup: string): void; + + protected dragStart(evt: JQuery.Event, x: number, y: number): void; + + protected dragMagnetStart(evt: JQuery.Event, x: number, y: number): void; + + protected drag(evt: JQuery.Event, x: number, y: number): void; + + protected dragMagnet(evt: JQuery.Event, x: number, y: number): void; + + protected dragEnd(evt: JQuery.Event, x: number, y: number): void; + + protected dragMagnetEnd(evt: JQuery.Event, x: number, y: number): void; } // dia.LinkView @@ -1095,6 +1645,16 @@ export namespace dia { end: 'source' | 'target' ): Point; } + + interface LabelOptions extends Cell.Options { + absoluteDistance?: boolean; + reverseDistance?: boolean; + absoluteOffset?: boolean; + } + + interface VertexOptions extends Cell.Options { + + } } class LinkView extends CellViewGeneric { @@ -1109,14 +1669,43 @@ export namespace dia { }; sendToken(token: SVGElement, duration?: number, callback?: () => void): void; - sendToken(token: SVGElement, opt?: { duration?: number, direction?: string; }, callback?: () => void): void; + sendToken(token: SVGElement, opt?: { duration?: number, direction?: string; connection?: string }, callback?: () => void): void; + + addLabel(coordinates: Point, opt?: LinkView.LabelOptions): number; + addLabel(x: number, y: number, opt?: LinkView.LabelOptions): number; + + addVertex(coordinates: Point, opt?: LinkView.VertexOptions): number; + addVertex(x: number, y: number, opt?: LinkView.VertexOptions): number; - addVertex(vertex: Point): number; + getConnection(): g.Path; + + getSerializedConnection(): string; + + getConnectionSubdivisions(): g.Curve[][]; getConnectionLength(): number; getPointAtLength(length: number): g.Point; + getPointAtRatio(ratio: number): g.Point; + + getTangentAtLength(length: number): g.Line; + + getTangentAtRatio(ratio: number): g.Line; + + getClosestPoint(point: Point): g.Point; + + getClosestPointLength(point: Point): number; + + getClosestPointRatio(point: Point): number; + + getLabelPosition(x: number, y: number, opt?: LinkView.LabelOptions): Link.LabelPosition; + + getLabelCoordinates(labelPosition: Link.LabelPosition): g.Point; + + getVertexIndex(x: number, y: number): number; + getVertexIndex(point: Point): number; + update(link: Link, attributes: any, opt?: { [key: string]: any }): this; setInteractivity(value: boolean | LinkView.InteractivityOptions): void; @@ -1130,6 +1719,38 @@ export namespace dia { protected onSourceChange(element: Element, sourceEnd: any, opt: { [key: string]: any }): void; protected onTargetChange(element: Element, targetEnd: any, opt: { [key: string]: any }): void; + + protected onlabel(evt: JQuery.Event, x: number, y: number): void; + + protected dragConnectionStart(evt: JQuery.Event, x: number, y: number): void; + + protected dragLabelStart(evt: JQuery.Event, x: number, y: number): void; + + protected dragVertexStart(evt: JQuery.Event, x: number, y: number): void; + + protected dragArrowheadStart(evt: JQuery.Event, x: number, y: number): void; + + protected dragStart(evt: JQuery.Event, x: number, y: number): void; + + protected dragConnection(evt: JQuery.Event, x: number, y: number): void; + + protected dragLabel(evt: JQuery.Event, x: number, y: number): void; + + protected dragVertex(evt: JQuery.Event, x: number, y: number): void; + + protected dragArrowhead(evt: JQuery.Event, x: number, y: number): void; + + protected drag(evt: JQuery.Event, x: number, y: number): void; + + protected dragConnectionEnd(evt: JQuery.Event, x: number, y: number): void; + + protected dragLabelEnd(evt: JQuery.Event, x: number, y: number): void; + + protected dragVertexEnd(evt: JQuery.Event, x: number, y: number): void; + + protected dragArrowheadEnd(evt: JQuery.Event, x: number, y: number): void; + + protected dragEnd(evt: JQuery.Event, x: number, y: number): void; } // dia.Paper @@ -1186,6 +1807,7 @@ export namespace dia { restrictTranslate?: ((elementView: ElementView) => BBox) | boolean; multiLinks?: boolean; linkPinning?: boolean; + allowLink?: ((linkView: LinkView, paper: Paper) => boolean) | null; // events guard?: (evt: JQuery.Event, view: CellView) => boolean; preventContextMenu?: boolean; @@ -1205,6 +1827,10 @@ export namespace dia { defaultLink?: ((cellView: CellView, magnet: SVGElement) => Link) | Link; defaultRouter?: routers.Router | routers.RouterJSON; defaultConnector?: connectors.Connector | connectors.ConnectorJSON; + defaultAnchor?: anchors.AnchorJSON | anchors.Anchor; + defaultConnectionPoint?: connectionPoints.ConnectionPointJSON | connectionPoints.ConnectionPoint + // connecting + connectionStrategy?: connectionStrategies.ConnectionStrategy; } interface ScaleContentOptions { @@ -1302,6 +1928,8 @@ export namespace dia { getRestrictedArea(): g.Rect | undefined; + getContentArea(): g.Rect; + getContentBBox(): g.Rect; findView(element: string | JQuery | SVGElement): T; @@ -1325,90 +1953,382 @@ export namespace dia { clearGrid(): this; - getDefaultLink(cellView: CellView, magnet: SVGElement): Link; + getDefaultLink(cellView: CellView, magnet: SVGElement): Link; + + getModelById(id: string | number | Cell): Cell; + + setDimensions(width: number, height: number): void; + + setGridSize(gridSize: number): this; + + setInteractivity(value: any): void; + + setOrigin(x: number, y: number): this; + + scale(): Vectorizer.Scale; + scale(sx: number, sy?: number, ox?: number, oy?: number): this; + + translate(): Vectorizer.Translation; + translate(tx: number, ty?: number): this; + + update(): this; + + // tools + + removeTools(): this; + + hideTools(): this; + + showTools(): this; + + // protected + protected pointerdblclick(evt: JQuery.Event): void; + + protected pointerclick(evt: JQuery.Event): void; + + protected contextmenu(evt: JQuery.Event): void; + + protected pointerdown(evt: JQuery.Event): void; + + protected pointermove(evt: JQuery.Event): void; + + protected pointerup(evt: JQuery.Event): void; + + protected mouseover(evt: JQuery.Event): void; + + protected mouseout(evt: JQuery.Event): void; + + protected mouseenter(evt: JQuery.Event): void; + + protected mouseleave(evt: JQuery.Event): void; + + protected mousewheel(evt: JQuery.Event): void; + + protected onevent(evt: JQuery.Event): void; + + protected onmagnet(evt: JQuery.Event): void; + + protected onlabel(evt: JQuery.Event): void; + + protected guard(evt: JQuery.Event, view: CellView): boolean; + + protected sortViews(): void; + + protected drawBackgroundImage(img: HTMLImageElement, opt: { [key: string]: any }): void; + + protected createViewForModel(cell: Cell): CellView; + + protected cloneOptions(): Paper.Options; + + protected afterRenderViews(): void; + + protected asyncRenderViews(cells: Cell[], opt?: { [key: string]: any }): void; + + protected beforeRenderViews(cells: Cell[]): Cell[]; + + protected init(): void; + + protected onCellAdded(cell: Cell, graph: Graph, opt: { async?: boolean, position?: number }): void; + + protected onCellHighlight(cellView: CellView, magnetEl: SVGElement, opt?: { highlighter?: highlighters.HighlighterJSON }): void; + + protected onCellUnhighlight(cellView: CellView, magnetEl: SVGElement, opt?: { highlighter?: highlighters.HighlighterJSON }): void; + + protected onRemove(): void; + + protected removeView(cell: Cell): CellView; + + protected removeViews(): void; + + protected renderView(cell: Cell): CellView; + + protected resetViews(cellsCollection: Cell[], opt: { [key: string]: any }): void; + + protected updateBackgroundColor(color: string): void; + + protected updateBackgroundImage(opt: { position?: any, size?: any }): void; + } + + namespace ToolsView { + + interface Options { + tools?: dia.ToolView[]; + name?: string | null; + relatedView?: dia.CellView; + component?: boolean; + } + } + + class ToolsView extends mvc.View { + + constructor(opt?: ToolsView.Options); + + options: ToolsView.Options; + + configure(opt?: ToolsView.Options): this; + + getName(): string | null; + + focusTool(tool: ToolView): this; + + blurTool(tool: ToolView): this; + + show(): this; + + hide(): this; + + mount(): this; + + protected simulateRelatedView(el: SVGElement): void; + } + + namespace ToolView { + + interface Options { + focusOpacity?: number; + } + } + + class ToolView extends mvc.View { + + name: string | null; + parentView: ToolsView; + relatedView: dia.CellView; + paper: Paper; + + constructor(opt?: ToolView.Options); + + configure(opt?: ToolView.Options): this; + + show(): void; + + hide(): void; + + isVisible(): boolean; - getModelById(id: string | number | Cell): Cell; + focus(): void; - setDimensions(width: number, height: number): void; + blur(): void; - setGridSize(gridSize: number): this; + update(): void; + } - setInteractivity(value: any): void; +} - setOrigin(x: number, y: number): this; +export namespace shapes { - scale(): Vectorizer.Scale; - scale(sx: number, sy?: number, ox?: number, oy?: number): this; + namespace standard { - translate(): Vectorizer.Translation; - translate(tx: number, ty?: number): this; + interface RectangleSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGRectAttributes; + label?: attributes.SVGTextAttributes; + } - update(): this; + class Rectangle extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - // protected - protected guard(evt: JQuery.Event, view: CellView): boolean; + interface CircleSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGCircleAttributes; + label?: attributes.SVGTextAttributes; + } - protected sortViews(): void; + class Circle extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected drawBackgroundImage(img: HTMLImageElement, opt: { [key: string]: any }): void; + interface EllipseSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGCircleAttributes; + label?: attributes.SVGTextAttributes; + } - protected createViewForModel(cell: Cell): CellView; + class Ellipse extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected cloneOptions(): Paper.Options; + interface PathSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGPathAttributes; + label?: attributes.SVGTextAttributes; + } - protected afterRenderViews(): void; + class Path extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected asyncRenderViews(cells: Cell[], opt?: { [key: string]: any }): void; + interface PolygonSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGPolygonAttributes; + label?: attributes.SVGTextAttributes; + } - protected beforeRenderViews(cells: Cell[]): Cell[]; + class Polygon extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected cellMouseEnter(evt: JQuery.Event): void; + interface PolylineSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGPolylineAttributes; + label?: attributes.SVGTextAttributes; + } - protected cellMouseleave(evt: JQuery.Event): void; + class Polyline extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected cellMouseout(evt: JQuery.Event): void; + interface ImageSelectors { + root?: attributes.SVGAttributes; + image?: attributes.SVGImageAttributes; + label?: attributes.SVGTextAttributes; + } - protected cellMouseover(evt: JQuery.Event): void; + class Image extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected contextmenu(evt: JQuery.Event): void; + interface BorderedImageSelectors { + root?: attributes.SVGAttributes; + border?: attributes.SVGRectAttributes; + image?: attributes.SVGImageAttributes; + label?: attributes.SVGTextAttributes; + } - protected init(): void; + class BorderedImage extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected mouseclick(evt: JQuery.Event): void; + interface EmbeddedImageSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGRectAttributes; + image?: attributes.SVGImageAttributes; + label?: attributes.SVGTextAttributes; + } - protected mousedblclick(evt: JQuery.Event): void; + class EmbeddedImage extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected mousewheel(evt: JQuery.Event): void; + interface HeaderedRectangleSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGRectAttributes; + header?: attributes.SVGRectAttributes; + headerText?: attributes.SVGTextAttributes; + bodyText?: attributes.SVGTextAttributes; + } - protected onCellAdded(cell: Cell, graph: Graph, opt: { async?: boolean, position?: number }): void; + class HeaderedRectangle extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected onCellHighlight(cellView: CellView, magnetEl: SVGElement, opt?: { highlighter?: highlighters.HighlighterJSON }): void; + interface CylinderBodyAttributes extends attributes.SVGPathAttributes { + lateralArea?: string | number; + } - protected onCellUnhighlight(cellView: CellView, magnetEl: SVGElement, opt?: { highlighter?: highlighters.HighlighterJSON }): void; + interface CylinderSelectors { + root?: attributes.SVGAttributes; + body?: CylinderBodyAttributes; + top?: attributes.SVGEllipseAttributes; + } - protected onRemove(): void; + class Cylinder extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) - protected pointerdown(evt: JQuery.Event): void; + topRy(): string | number; + topRy(t: string | number, opt?: dia.Cell.Options): this; + } - protected pointermove(evt: JQuery.Event): void; + interface TextBlockSelectors { + root?: attributes.SVGAttributes; + body?: attributes.SVGRectAttributes; + label?: { + text?: string; + style?: { [key: string]: any }; + [key: string]: any; + } + } - protected pointerup(evt: JQuery.Event): void; + class TextBlock extends dia.Element { + constructor( + attributes?: dia.Element.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected removeView(cell: Cell): CellView; + interface LinkSelectors { + root?: attributes.SVGAttributes; + line?: attributes.SVGPathAttributes; + wrapper?: attributes.SVGPathAttributes; + } - protected removeViews(): void; + class Link extends dia.Link { + constructor( + attributes?: dia.Link.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected renderView(cell: Cell): CellView; + interface DoubleLinkSelectors { + root?: attributes.SVGAttributes; + line?: attributes.SVGPathAttributes; + outline?: attributes.SVGPathAttributes; + } - protected resetViews(cellsCollection: Cell[], opt: { [key: string]: any }): void; + class DoubleLink extends dia.Link { + constructor( + attributes?: dia.Link.GenericAttributes, + opt?: dia.Graph.Options + ) + } - protected updateBackgroundColor(color: string): void; + interface ShadowLinkSelectors { + root?: attributes.SVGAttributes; + line?: attributes.SVGPathAttributes; + shadow?: attributes.SVGPathAttributes; + } - protected updateBackgroundImage(opt: { position?: any, size?: any }): void; + class ShadowLink extends dia.Link { + constructor( + attributes?: dia.Link.GenericAttributes, + opt?: dia.Graph.Options + ) + } } -} - -export namespace shapes { interface SVGTextSelector extends dia.Cell.Selectors { text?: attributes.SVGTextAttributes; @@ -2029,11 +2949,21 @@ export namespace util { export var shapePerimeterConnectionPoint: dia.LinkView.GetConnectionPoint; + export function isPercentage(val: any): boolean; + export function parseCssNumber(str: string, restrictUnits?: string[]): { value: number; unit?: string; }; - export function breakText(text: string, size: dia.Size, attrs?: attributes.NativeSVGAttributes, opt?: { svgDocument?: SVGElement }): string; + export function breakText(text: string, size: dia.Size, attrs?: attributes.NativeSVGAttributes, opt?: { svgDocument?: SVGElement; separator: string | any; eol: string }): string; + + export function sanitizeHTML(html: string): string; - export function imageToDataUri(url: string, callback: (err: Error, dataUri: string) => void): void; + export function downloadBlob(blob: Blob, fileName: string): void; + + export function downloadDataUri(dataUri: string, fileName: string): void; + + export function dataUriToBlob(dataUri: string): Blob; + + export function imageToDataUri(url: string, callback: (err: Error | null, dataUri: string) => void): void; export function getElementBBox(el: Element): dia.BBox; @@ -2221,7 +3151,7 @@ export namespace layout { rankSep?: number; marginX?: number; marginY?: number; - resizeCluster?: boolean; + resizeClusters?: boolean; clusterPadding?: dia.Padding; setPosition?: (element: dia.Element, position: dia.BBox) => void; setVertices?: boolean | ((link: dia.Link, vertices: dia.Point[]) => void); @@ -2245,6 +3175,10 @@ export namespace mvc { theme?: string; } + interface viewEventData { + [key: string]: any; + } + class View extends Backbone.View { constructor(opt?: ViewOptions); @@ -2257,10 +3191,23 @@ export namespace mvc { requireSetThemeOverride: boolean; + documentEvents?: Backbone.EventsHash; + + children?: dia.MarkupJSON; + setTheme(theme: string, opt?: { override?: boolean }): this; getEventNamespace(): string; + delegateDocumentEvents(events?: Backbone.EventsHash, data?: viewEventData): this; + + undelegateDocumentEvents(): this; + + eventData(evt: JQuery.Event): viewEventData; + eventData(evt: JQuery.Event, data: viewEventData): this; + + renderChildren(children?: dia.MarkupJSON): this; + protected init(): void; protected onRender(): void; @@ -2303,23 +3250,28 @@ export namespace routers { 'metro': ManhattanRouterArguments; 'orthogonal': OrthogonalRouterArguments; 'oneSide': OneSideRouterArguments; + [key: string]: { [key: string]: any }; } - type RouterType = string & keyof RouterArgumentsMap; + type RouterType = keyof RouterArgumentsMap; + + type GenericRouterArguments = RouterArgumentsMap[K]; interface GenericRouter { ( - points: dia.Point[], - args?: RouterArgumentsMap[K], + vertices: dia.Point[], + args?: GenericRouterArguments, linkView?: dia.LinkView ): dia.Point[]; } interface GenericRouterJSON { name: K; - args?: RouterArgumentsMap[K]; + args?: GenericRouterArguments; } + type RouterArguments = GenericRouterArguments; + type Router = GenericRouter; type RouterJSON = GenericRouterJSON; @@ -2336,20 +3288,22 @@ export namespace routers { export namespace connectors { interface NormalConnectorArguments { - + raw?: boolean; } interface RoundedConnectorArguments { - radius?: number + raw?: boolean; + radius?: number; } interface SmoothConnectorArguments { - + raw?: boolean; } interface JumpOverConnectorArguments { + raw?: boolean; size?: number; - jump?: 'arc' | 'gap' | 'cubic' + jump?: 'arc' | 'gap' | 'cubic'; } interface ConnectorArgumentsMap { @@ -2357,25 +3311,30 @@ export namespace connectors { 'rounded': RoundedConnectorArguments; 'smooth': SmoothConnectorArguments; 'jumpover': JumpOverConnectorArguments; + [key: string]: { [key: string]: any }; } - type ConnectorType = string & keyof ConnectorArgumentsMap; + type ConnectorType = keyof ConnectorArgumentsMap; + + type GenericConnectorArguments = ConnectorArgumentsMap[K]; interface GenericConnector { ( sourcePoint: dia.Point, targetPoint: dia.Point, - vertices: dia.Point[], - args?: ConnectorArgumentsMap[K], - linkView?: dia.LinkView - ): string; + routePoints: dia.Point[], + args?: GenericConnectorArguments, + //linkView?: dia.LinkView + ): string | g.Path; } interface GenericConnectorJSON { name: K; - args?: ConnectorArgumentsMap[K]; + args?: GenericConnectorArguments; } + type ConnectorArguments = GenericConnectorArguments; + type Connector = GenericConnector; type ConnectorJSON = GenericConnectorJSON; @@ -2386,6 +3345,146 @@ export namespace connectors { export var jumpover: GenericConnector<'jumpover'>; } +// anchors + +export namespace anchors { + + interface RotateAnchorArguments { + rotate?: boolean; + } + + interface BBoxAnchorArguments extends RotateAnchorArguments { + dx?: number | string; + dy?: number | string; + } + + interface PaddingAnchorArguments { + padding?: number; + } + + interface MidSideAnchorArguments extends RotateAnchorArguments, PaddingAnchorArguments { + + } + + interface ModelCenterAnchorArguments { + + } + + interface AnchorArgumentsMap { + 'center': BBoxAnchorArguments, + 'top': BBoxAnchorArguments, + 'bottom': BBoxAnchorArguments, + 'left': BBoxAnchorArguments, + 'right': BBoxAnchorArguments, + 'topLeft': BBoxAnchorArguments, + 'topRight': BBoxAnchorArguments, + 'bottomLeft': BBoxAnchorArguments, + 'bottomRight': BBoxAnchorArguments, + 'perpendicular': PaddingAnchorArguments; + 'midSide': MidSideAnchorArguments; + 'modelCenter': ModelCenterAnchorArguments; + [key: string]: { [key: string]: any }; + } + + type AnchorType = keyof AnchorArgumentsMap; + + type GenericAnchorArguments = AnchorArgumentsMap[K]; + + interface GenericAnchor { + ( + endView: dia.CellView, + endMagnet: SVGElement, + anchorReference: g.Point | SVGElement, + opt: AnchorArgumentsMap[K], + //endType: string, + //linkView: dia.LinkView + ): g.Point; + } + + interface GenericAnchorJSON { + name: K; + args?: AnchorArgumentsMap[K]; + } + + type AnchorArguments = GenericAnchorArguments; + + type Anchor = GenericAnchor; + + type AnchorJSON = GenericAnchorJSON; + + export var center: GenericAnchor<'center'>; + export var top: GenericAnchor<'top'>; + export var bottom: GenericAnchor<'bottom'>; + export var left: GenericAnchor<'left'>; + export var right: GenericAnchor<'right'>; + export var topLeft: GenericAnchor<'topLeft'>; + export var topRight: GenericAnchor<'topRight'>; + export var bottomLeft: GenericAnchor<'bottomLeft'>; + export var bottomRight: GenericAnchor<'bottomRight'>; + export var perpendicular: GenericAnchor<'perpendicular'>; + export var midSide: GenericAnchor<'midSide'>; +} + +// connection points + +export namespace connectionPoints { + + interface DefaultConnectionPointArguments { + offset?: number; + } + + interface StrokeConnectionPointArguments extends DefaultConnectionPointArguments { + stroke?: boolean; + } + + interface BoundaryConnectionPointArguments extends StrokeConnectionPointArguments { + selector?: Array | string; + precision?: number; + extrapolate?: boolean; + sticky?: boolean; + insideout?: boolean; + } + + interface ConnectionPointArgumentsMap { + 'anchor': DefaultConnectionPointArguments, + 'bbox': StrokeConnectionPointArguments, + 'rectangle': StrokeConnectionPointArguments, + 'boundary': BoundaryConnectionPointArguments, + [key: string]: { [key: string]: any }; + } + + type ConnectionPointType = keyof ConnectionPointArgumentsMap; + + type GenericConnectionPointArguments = ConnectionPointArgumentsMap[K]; + + interface GenericConnectionPoint { + ( + endPathSegmentLine: g.Line, + endView: dia.CellView, + endMagnet: SVGElement, + opt: ConnectionPointArgumentsMap[K], + //endType: string, + //linkView: dia.LinkView + ): g.Point; + } + + interface GenericConnectionPointJSON { + name: K; + args?: ConnectionPointArgumentsMap[K]; + } + + type ConnectionPointArguments = GenericConnectionPointArguments; + + type ConnectionPoint = GenericConnectionPoint; + + type ConnectionPointJSON = GenericConnectionPointJSON; + + export var anchor: GenericConnectionPoint<'anchor'>; + export var bbox: GenericConnectionPoint<'bbox'>; + export var rectangle: GenericConnectionPoint<'rectangle'>; + export var boundary: GenericConnectionPoint<'boundary'>; +} + // highlighters export namespace highlighters { @@ -2409,21 +3508,26 @@ export namespace highlighters { 'addClass': AddClassHighlighterArguments; 'opacity': OpacityHighlighterArguments; 'stroke': StrokeHighlighterArguments; + [key: string]: { [key: string]: any }; } - type HighlighterType = string & keyof HighlighterArgumentsMap; + type HighlighterType = keyof HighlighterArgumentsMap; - interface GenericHighlighterJSON { - name: K; - opt?: HighlighterArgumentsMap[K]; - } + type GenericHighlighterArguments = HighlighterArgumentsMap[K]; interface GenericHighlighter { - highlight(cellView: dia.CellView, magnetEl: SVGElement, opt?: HighlighterArgumentsMap[K]): void; + highlight(cellView: dia.CellView, magnetEl: SVGElement, opt?: GenericHighlighterArguments): void; + + unhighlight(cellView: dia.CellView, magnetEl: SVGElement, opt?: GenericHighlighterArguments): void; + } - unhighlight(cellView: dia.CellView, magnetEl: SVGElement, opt?: HighlighterArgumentsMap[K]): void; + interface GenericHighlighterJSON { + name: K; + options?: GenericHighlighterArguments; } + type HighlighterArguments = GenericHighlighterArguments; + type Highlighter = GenericHighlighter; type HighlighterJSON = GenericHighlighterJSON; @@ -2433,6 +3537,24 @@ export namespace highlighters { export var stroke: GenericHighlighter<'stroke'>; } +export namespace connectionStrategies { + + interface ConnectionStrategy { + ( + endDefinition: dia.Cell, + endView: dia.CellView, + endMagnet: SVGElement, + coords: dia.Point, + //link: dia.Link, + //endType: string + ): dia.Element; + } + + export var useDefaults: ConnectionStrategy; + export var pinAbsolute: ConnectionStrategy; + export var pinRelative: ConnectionStrategy; +} + export namespace attributes { interface SVGCoreAttributes { @@ -2523,13 +3645,20 @@ export namespace attributes { interface NativeSVGAttributes extends SVGCoreAttributes, SVGPresentationAttributes, SVGConditionalProcessingAttributes, SVGXLinkAttributes { 'class'?: string; - 'style'?: string; + 'style'?: any; 'transform'?: string; 'externalResourcesRequired'?: boolean; [key: string]: any; } + interface SVGAttributeTextWrap { + text?: string; + width?: string | number; + height?: string | number; + [key: string]: any + } + interface SVGAttributes extends NativeSVGAttributes { // Special attributes filter?: string | { [key: string]: any }; @@ -2539,31 +3668,49 @@ export namespace attributes { targetMarker?: { [key: string]: any }; vertexMarker?: { [key: string]: any }; text?: string; - textWrap?: { [key: string]: any }; + textWrap?: SVGAttributeTextWrap; lineHeight?: number | string; textPath?: any; annotations?: any; - port?: string; - style?: string; + port?: string | { [key: string]: any }; + style?: { [key: string]: any }; html?: string; ref?: string; refX?: string | number; - refy?: string | number; + refY?: string | number; refX2?: string | number; - refy2?: string | number; + refY2?: string | number; refDx?: string | number; refDy?: string | number; refWidth?: string | number; refHeight?: string | number; refRx?: string | number; refRy?: string | number; + refR?: string | number; + refRInscribed?: string | number; // alias for refR + refRCircumscribed?: string | number; refCx?: string | number; refCy?: string | number; + refD?: string; + refDResetOffset?: string; // alias for refD + refDKeepOffset?: string; + refPoints?: string; + refPointsResetOffset?: string; // alias for refPoints + refPointsKeepOffset?: string; resetOffset?: boolean; xAlignment?: 'middle' | 'right' | number | string; yAlignment?: 'middle' | 'bottom' | number | string; event?: string; magnet?: boolean | string; + title?: string; + textVerticalAnchor?: 'bottom' | 'top' | 'middle' | number | string; + connection?: boolean; + atConnectionLength?: number; + atConnectionLengthKeepGradient?: number; // alias for atConnectionLength + atConnectionLengthIgnoreGradient?: number; + atConnectionRatio?: number; + atConnectionRatioKeepGradient?: number; // alias for atConnectionRatio + atConnectionRatioIgnoreGradient?: number; // CamelCase variants of native attributes alignmentBaseline?: any; baselineShift?: any; @@ -2693,3 +3840,131 @@ export namespace attributes { } export function setTheme(theme: string): void; + + +export namespace linkTools { + + type AnchorCallback = ( + coords: g.Point, + view: dia.CellView, + magnet: SVGElement, + type: string, + linkView: dia.LinkView, + toolView: dia.ToolView + ) => T; + + namespace Vertices { + interface Options extends dia.ToolView.Options { + snapRadius?: number; + redundancyRemoval?: boolean; + vertexAdding?: boolean; + } + } + + class Vertices extends dia.ToolView { + + constructor(opt?: Vertices.Options); + } + + namespace Segments { + interface Options extends dia.ToolView.Options { + snapRadius?: number; + snapHandle?: boolean; + redundancyRemoval?: boolean; + segmentLengthThreshold?: number; + anchor?: AnchorCallback; + } + } + + class Segments extends dia.ToolView { + + constructor(opt?: Segments.Options); + } + + abstract class Arrowhead extends dia.ToolView { + + ratio: number; + arrowheadType: string; + + protected onPointerDown(evt: JQuery.Event): void; + + protected onPointerMove(evt: JQuery.Event): void; + + protected onPointerUp(evt: JQuery.Event): void; + } + + class SourceArrowhead extends Arrowhead { + + + } + + class TargetArrowhead extends Arrowhead { + + + } + + namespace Anchor { + interface Options extends dia.ToolView.Options { + snap?: AnchorCallback, + anchor?: AnchorCallback, + customAnchorAttributes?: attributes.NativeSVGAttributes; + defaultAnchorAttributes?: attributes.NativeSVGAttributes; + areaPadding?: number; + snapRadius?: number; + restrictArea?: boolean; + redundancyRemoval?: boolean; + } + } + + abstract class Anchor extends dia.ToolView { + + type: string; + + constructor(opt?: Anchor.Options); + } + + class SourceAnchor extends Anchor { + + + } + + class TargetAnchor extends Anchor { + + + } + + namespace Button { + + type ActionCallback = (evt: JQuery.Event, view: dia.LinkView) => void; + + interface Options extends dia.ToolView.Options { + distance?: number; + offset?: number; + rotate?: boolean; + action?: ActionCallback; + markup?: dia.MarkupJSON; + } + } + + class Button extends dia.ToolView { + + constructor(opt?: Button.Options); + + protected onPointerDown(evt: JQuery.Event): void; + } + + class Remove extends dia.ToolView { + + } + + namespace Boundary { + interface Options extends dia.ToolView.Options { + padding?: number; + } + } + + class Boundary extends dia.ToolView { + + constructor(opt?: Boundary.Options); + } +} diff --git a/dist/joint.js b/dist/joint.js index 136b2a3ad..1fedf1bcb 100644 --- a/dist/joint.js +++ b/dist/joint.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -435,8 +435,8 @@ Number.isNaN = Number.isNaN || function(value) { } - -// Geometry library. +// Geometry library. +// ----------------- var g = (function() { @@ -448,8 +448,8 @@ var g = (function() { var cos = math.cos; var sin = math.sin; var sqrt = math.sqrt; - var mmin = math.min; - var mmax = math.max; + var min = math.min; + var max = math.max; var atan2 = math.atan2; var round = math.round; var floor = math.floor; @@ -460,27 +460,25 @@ var g = (function() { 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). + // @deprecated // @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); - } + console.warn('deprecated'); - return path; + return new Path(Curve.throughPoints(points)).serialize(); }, // Get open-ended Bezier Spline Control Points. + // @deprecated // @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) { + console.warn('deprecated'); + var firstControlPoints = []; var secondControlPoints = []; var n = knots.length - 1; @@ -489,11 +487,17 @@ var g = (function() { // 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); + firstControlPoints[0] = new 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); + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); + return [firstControlPoints, secondControlPoints]; } @@ -505,8 +509,10 @@ var g = (function() { 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); @@ -514,50 +520,44 @@ var g = (function() { 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])); + firstControlPoints.push(new 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])); + secondControlPoints.push(new 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)); + secondControlPoints.push(new 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 }]; - }; + return [firstControlPoints, secondControlPoints]; }, // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @deprecated // @param rhs Right hand side vector. // @return Solution vector. getFirstControlPoints: function(rhs) { + console.warn('deprecated'); + var n = rhs.length; // `x` is a solution vector. var x = []; @@ -565,870 +565,981 @@ var g = (function() { 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; }, + // Divide a Bezier curve into two at point defined by value 't' <0,1>. + // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 + // @deprecated + // @param control points (start, control start, control end, end) + // @return a function that accepts t and returns 2 curves. + getCurveDivider: function(p0, p1, p2, p3) { + + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + + return function divideCurve(t) { + + var divided = curve.divide(t); + + return [{ + p0: divided[0].start, + p1: divided[0].controlPoint1, + p2: divided[0].controlPoint2, + p3: divided[0].end + }, { + p0: divided[1].start, + p1: divided[1].controlPoint1, + p2: divided[1].controlPoint2, + p3: divided[1].end + }]; + }; + }, + // 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. + // @deprecated // @param control points (start, control start, control end, end) - // @return a function accepts a point and returns t. + // @return a function that 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; - }; - } + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + 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); + + return curve.closestPointT(p); }; } }; - var Ellipse = g.Ellipse = function(c, a, b) { + var Curve = g.Curve = function(p1, p2, p3, p4) { - if (!(this instanceof Ellipse)) { - return new Ellipse(c, a, b); + if (!(this instanceof Curve)) { + return new Curve(p1, p2, p3, p4); } - if (c instanceof Ellipse) { - return new Ellipse(Point(c), c.a, c.b); + if (p1 instanceof Curve) { + return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end); } - c = Point(c); - this.x = c.x; - this.y = c.y; - this.a = a; - this.b = b; + this.start = new Point(p1); + this.controlPoint1 = new Point(p2); + this.controlPoint2 = new Point(p3); + this.end = new Point(p4); }; - g.Ellipse.fromRect = function(rect) { + // Curve passing 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} curves. + Curve.throughPoints = (function() { - rect = Rect(rect); - return Ellipse(rect.center(), rect.width / 2, rect.height / 2); - }; + // 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. + function getFirstControlPoints(rhs) { - g.Ellipse.prototype = { + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; - bbox: function() { + x[0] = rhs[0] / b; - return Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.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; + } - clone: function() { + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } - return Ellipse(this); - }, + return x; + } - /** - * @param {g.Point} point - * @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside - */ - normalizedDistance: function(point) { + // 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. + function getCurveControlPoints(knots) { - var x0 = point.x; - var y0 = point.y; - var a = this.a; - var b = this.b; - var x = this.x; - var y = this.y; + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; - return ((x0 - x) * (x0 - x)) / (a * a ) + ((y0 - y) * (y0 - y)) / (b * b); - }, + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new Point( + (2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3 + ); - // 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; - } + // P2 = 2P1 – P0 + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); - if (dy === undefined) { - dy = dx; + return [firstControlPoints, secondControlPoints]; } - this.a += 2 * dx; - this.b += 2 * dy; - - return this; - }, + // 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; + } - /** - * @param {g.Point} p - * @returns {boolean} - */ - containsPoint: function(p) { + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - return this.normalizedDistance(p) <= 1; - }, + // Get first control points X-values. + var x = getFirstControlPoints(rhs); - /** - * @returns {g.Point} - */ - center: function() { + // Set right hand side Y values. + for (i = 1; i < n - 1; ++i) { + rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; + } - return Point(this.x, this.y); - }, + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - /** 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) { + // Get first control points Y-values. + var y = getFirstControlPoints(rhs); - 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; + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(new Point(x[i], y[i])); - var q1 = x0 > center.x + a / 2; - var q3 = x0 < center.x - a / 2; + // Second control point. + if (i < n - 1) { + secondControlPoints.push(new Point( + 2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1] + )); - 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; + } else { + secondControlPoints.push(new Point( + (knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2 + )); + } } - return g.point(x, y).theta(p); + return [firstControlPoints, secondControlPoints]; + } - }, + return function(points) { - equals: function(ellipse) { + if (!points || (Array.isArray(points) && points.length < 2)) { + throw new Error('At least 2 points are required'); + } - return !!ellipse && - ellipse.x === this.x && - ellipse.y === this.y && - ellipse.a === this.a && - ellipse.b === this.b; - }, + var controlPoints = getCurveControlPoints(points); - // 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) { + var curves = []; + var n = controlPoints[0].length; + for (var i = 0; i < n; i++) { - 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 controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y); + var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y); + + curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1])); } - 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; - }, + return curves; + }; + })(); - toString: function() { + Curve.prototype = { - return Point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; - } - }; + // Returns a bbox that tightly envelops the curve. + bbox: function() { - var Line = g.Line = function(p1, p2) { + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; - if (!(this instanceof Line)) { - return new Line(p1, p2); - } + var x0 = start.x; + var y0 = start.y; + var x1 = controlPoint1.x; + var y1 = controlPoint1.y; + var x2 = controlPoint2.x; + var y2 = controlPoint2.y; + var x3 = end.x; + var y3 = end.y; - if (p1 instanceof Line) { - return Line(p1.start, p1.end); - } + var points = new Array(); // local extremes + var tvalues = new Array(); // t values of local extremes + var bounds = [new Array(), new Array()]; - this.start = Point(p1); - this.end = Point(p2); - }; + var a, b, c, t; + var t1, t2; + var b2ac, sqrtb2ac; - g.Line.prototype = { + for (var i = 0; i < 2; ++i) { - // @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() { + if (i === 0) { + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; - 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)); + } else { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } - var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']; + if (abs(a) < 1e-12) { // Numerical robustness + if (abs(b) < 1e-12) { // Numerical robustness + continue; + } - var index = brng - 22.5; - if (index < 0) - index += 360; - index = parseInt(index / 45); + t = -c / b; + if ((0 < t) && (t < 1)) tvalues.push(t); - return bearings[index]; - }, + continue; + } - clone: function() { + b2ac = b * b - 4 * c * a; + sqrtb2ac = sqrt(b2ac); - return Line(this.start, this.end); - }, + if (b2ac < 0) continue; - equals: function(l) { + t1 = (-b + sqrtb2ac) / (2 * a); + if ((0 < t1) && (t1 < 1)) tvalues.push(t1); - return !!l && - this.start.x === l.start.x && - this.start.y === l.start.y && - this.end.x === l.end.x && - this.end.y === l.end.y; - }, + t2 = (-b - sqrtb2ac) / (2 * a); + if ((0 < t2) && (t2 < 1)) tvalues.push(t2); + } - // @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) - ); + var j = tvalues.length; + var jlen = j; + var mt; + var x, y; - } else if (l instanceof Rect) { - // Passed in parameter is a rectangle. + while (j--) { + t = tvalues[j]; + mt = 1 - t; - var r = l; - var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; - var points = []; - var dedupeArr = []; - var pt, i; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; - 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()); - } - } + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; - return points.length > 0 ? points : null; + points[j] = { X: x, Y: y }; } - // Passed in parameter is neither a Line nor a Rectangle. - return null; - }, + tvalues[jlen] = 0; + tvalues[jlen + 1] = 1; - // @return {double} length of the line - length: function() { - return sqrt(this.squaredLength()); - }, + points[jlen] = { X: x0, Y: y0 }; + points[jlen + 1] = { X: x3, Y: y3 }; - // @return {point} my midpoint - midpoint: function() { - return Point( - (this.start.x + this.end.x) / 2, - (this.start.y + this.end.y) / 2 - ); - }, + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; - // @return {point} my point at 't' <0,1> - pointAt: function(t) { + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; - 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); - }, + tvalues.length = jlen + 2; + bounds[0].length = jlen + 2; + bounds[1].length = jlen + 2; + points.length = jlen + 2; - // @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) { + var left = min.apply(null, bounds[0]); + var top = min.apply(null, bounds[1]); + var right = max.apply(null, bounds[0]); + var bottom = max.apply(null, bounds[1]); - // 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 new Rect(left, top, (right - left), (bottom - top)); }, - // @return vector {point} of the line - vector: function() { + clone: function() { - return Point(this.end.x - this.start.x, this.end.y - this.start.y); + return new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); }, - // @return {point} the closest point on the line to point `p` - closestPoint: function(p) { + // Returns the point on the curve closest to point `p` + closestPoint: function(p, opt) { - return this.pointAt(this.closestPointNormalizedLength(p)); + return this.pointAtT(this.closestPointT(p, opt)); }, - // @return {number} the normalized length of the closest point on the line to point `p` - closestPointNormalizedLength: function(p) { + closestPointLength: function(p, opt) { - var product = this.vector().dot(Line(this.start, p).vector()); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - return Math.min(1, Math.max(0, product / this.squaredLength())); + return this.lengthAtT(this.closestPointT(p, localOpt), localOpt); }, - // @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; + closestPointNormalizedLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + var cpLength = this.closestPointLength(p, localOpt); + if (!cpLength) return 0; + + var length = this.length(localOpt); + if (length === 0) return 0; + + return cpLength / length; }, - toString: function() { - return this.start.toString() + ' ' + this.end.toString(); - } - }; + // Returns `t` of the point on the curve closest to point `p` + closestPointT: function(p, opt) { - // For backwards compatibility: - g.Line.prototype.intersection = g.Line.prototype.intersect; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // does not use localOpt + + // identify the subdivision that contains the point: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + var distFromStart; // distance of point from start of baseline + var distFromEnd; // distance of point from end of baseline + var minSumDist; // lowest observed sum of the two distances + var n = subdivisions.length; + var subdivisionSize = (n ? (1 / n) : 0); + for (var i = 0; i < n; i++) { - /* - Point is the most basic object consisting of x/y coordinate. + var currentSubdivision = subdivisions[i]; - Possible instantiations are: - * `Point(10, 20)` - * `new Point(10, 20)` - * `Point('10 20')` - * `Point(Point(10, 20))` - */ - var Point = g.Point = function(x, y) { + var startDist = currentSubdivision.start.distance(p); + var endDist = currentSubdivision.end.distance(p); + var sumDist = startDist + endDist; - if (!(this instanceof Point)) { - return new Point(x, y); - } + // check that the point is closest to current subdivision and not any other + if (!minSumDist || (sumDist < minSumDist)) { + investigatedSubdivision = currentSubdivision; - 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; - } + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - }; + distFromStart = startDist; + distFromEnd = endDist; - // Alternative constructor, from polar coordinates. - // @param {number} Distance. - // @param {number} Angle in radians. - // @param {point} [optional] Origin. - g.Point.fromPolar = function(distance, angle, origin) { + minSumDist = sumDist; + } + } - origin = (origin && Point(origin)) || Point(0, 0); - var x = abs(distance * cos(angle)); - var y = abs(distance * sin(angle)); - var deg = normalizeAngle(toDeg(angle)); + var precisionRatio = pow(10, -precision); - if (deg < 90) { - y = -y; - } else if (deg < 180) { - x = -x; - y = -y; - } else if (deg < 270) { - x = -x; - } + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(origin.x + x, origin.y + y); - }; + // check if we have reached required observed precision + var startPrecisionRatio; + var endPrecisionRatio; - // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. - g.Point.random = function(x1, x2, y1, y2) { + startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0); + endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0); + if ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio) < precisionRatio) { + return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT); + } - return Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); - }; + // otherwise, set up for next iteration + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - g.Point.prototype = { + var startDist1 = divided[0].start.distance(p); + var endDist1 = divided[0].end.distance(p); + var sumDist1 = startDist1 + endDist1; - // 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) { + var startDist2 = divided[1].start.distance(p); + var endDist2 = divided[1].end.distance(p); + var sumDist2 = startDist2 + endDist2; - if (r.containsPoint(this)) { - return this; - } + if (sumDist1 <= sumDist2) { + investigatedSubdivision = divided[0]; - 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; - }, + investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved - // Return the bearing between me and the given point. - bearing: function(point) { + distFromStart = startDist1; + distFromEnd = endDist1; - return Line(this, point).bearing(); - }, + } else { + investigatedSubdivision = divided[1]; - // Returns change in angle from my previous position (-dx, -dy) to my new position - // relative to ref point. - changeInAngle: function(dx, dy, ref) { + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - // Revert the translation and measure the change in angle around x-axis. - return Point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); + distFromStart = startDist2; + distFromEnd = endDist2; + } + } }, - clone: function() { + closestPointTangent: function(p, opt) { - return Point(this); + return this.tangentAtT(this.closestPointT(p, opt)); }, - difference: function(dx, dy) { + // Divides the curve into two at point defined by `t` between 0 and 1. + // Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867). + // Additional resource: https://pomax.github.io/bezierinfo/#decasteljau + divide: function(t) { - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return [ + new Curve(start, start, start, start), + new Curve(start, controlPoint1, controlPoint2, end) + ]; } - return Point(this.x - (dx || 0), this.y - (dy || 0)); - }, + if (t >= 1) { + return [ + new Curve(start, controlPoint1, controlPoint2, end), + new Curve(end, end, end, end) + ]; + } - // Returns distance between me and point `p`. - distance: function(p) { + var dividerPoints = this.getSkeletonPoints(t); + + var startControl1 = dividerPoints.startControlPoint1; + var startControl2 = dividerPoints.startControlPoint2; + var divider = dividerPoints.divider; + var dividerControl1 = dividerPoints.dividerControlPoint1; + var dividerControl2 = dividerPoints.dividerControlPoint2; - return Line(this, p).length(); + // return array with two new curves + return [ + new Curve(start, startControl1, startControl2, divider), + new Curve(divider, dividerControl1, dividerControl2, end) + ]; }, - squaredDistance: function(p) { + // Returns the distance between the curve's start and end points. + endpointDistance: function() { - return Line(this, p).squaredLength(); + return this.start.distance(this.end); }, - equals: function(p) { + // Checks whether two curves are exactly the same. + equals: function(c) { - return !!p && this.x === p.x && this.y === p.y; + return !!c && + this.start.x === c.start.x && + this.start.y === c.start.y && + this.controlPoint1.x === c.controlPoint1.x && + this.controlPoint1.y === c.controlPoint1.y && + this.controlPoint2.x === c.controlPoint2.x && + this.controlPoint2.y === c.controlPoint2.y && + this.end.x === c.end.x && + this.end.y === c.end.y; }, - magnitude: function() { + // Returns five helper points necessary for curve division. + getSkeletonPoints: function(t) { - return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; - }, + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; - // Returns a manhattan (taxi-cab) distance between me and point `p`. - manhattanDistance: function(p) { + // shortcuts for `t` values that are out of range + if (t <= 0) { + return { + startControlPoint1: start.clone(), + startControlPoint2: start.clone(), + divider: start.clone(), + dividerControlPoint1: control1.clone(), + dividerControlPoint2: control2.clone() + }; + } - return abs(p.x - this.x) + abs(p.y - this.y); - }, + if (t >= 1) { + return { + startControlPoint1: control1.clone(), + startControlPoint2: control2.clone(), + divider: end.clone(), + dividerControlPoint1: end.clone(), + dividerControlPoint2: end.clone() + }; + } - // Move point on line starting from ref ending at me by - // distance distance. - move: function(ref, distance) { + var midpoint1 = (new Line(start, control1)).pointAt(t); + var midpoint2 = (new Line(control1, control2)).pointAt(t); + var midpoint3 = (new Line(control2, end)).pointAt(t); - var theta = toRad(Point(ref).theta(this)); - return this.offset(cos(theta) * distance, -sin(theta) * distance); - }, + var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t); + var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t); - // 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 divider = (new Line(subControl1, subControl2)).pointAt(t); - var scale = (length || 1) / this.magnitude(); - return this.scale(scale, scale); + var output = { + startControlPoint1: midpoint1, + startControlPoint2: subControl1, + divider: divider, + dividerControlPoint1: subControl2, + dividerControlPoint2: midpoint3 + }; + + return output; }, - // Offset me by the specified amount. - offset: function(dx, dy) { + // Returns a list of curves whose flattened length is better than `opt.precision`. + // That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1% + // (Observed difference is not real precision, but close enough as long as special cases are covered) + // (That is why skipping iteration 1 is important) + // As a rule of thumb, increasing `precision` by 1 requires two more division operations + // - Precision 0 (endpointDistance) - total of 2^0 - 1 = 0 operations (1 subdivision) + // - Precision 1 (<10% error) - total of 2^2 - 1 = 3 operations (4 subdivisions) + // - Precision 2 (<1% error) - total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions) + // - Precision 3 (<0.1% error) - total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions) + // - Precision 4 (<0.01% error) - total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions) + // (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly) + getSubdivisions: function(opt) { - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt - this.x += dx || 0; - this.y += dy || 0; - return this; - }, + var subdivisions = [new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end)]; + if (precision === 0) return subdivisions; - // Returns a point that is the reflection of me with - // the center of inversion in ref point. - reflection: function(ref) { + var previousLength = this.endpointDistance(); - return Point(ref).move(this, this.distance(ref)); - }, + var precisionRatio = pow(10, -precision); - // Rotate point by angle around origin. - rotate: function(origin, angle) { + // recursively divide curve at `t = 0.5` + // until the difference between observed length at subsequent iterations is lower than precision + var iteration = 0; + while (true) { + iteration += 1; - 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; - }, + // divide all subdivisions + var newSubdivisions = []; + var numSubdivisions = subdivisions.length; + for (var i = 0; i < numSubdivisions; i++) { - round: function(precision) { + var currentSubdivision = subdivisions[i]; + var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!) + newSubdivisions.push(divided[0], divided[1]); + } - var f = pow(10, precision || 0); - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - return this; - }, + // measure new length + var length = 0; + var numNewSubdivisions = newSubdivisions.length; + for (var j = 0; j < numNewSubdivisions; j++) { - // Scale point with origin. - scale: function(sx, sy, origin) { + var currentNewSubdivision = newSubdivisions[j]; + length += currentNewSubdivision.endpointDistance(); + } - 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; + // check if we have reached required observed precision + // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1 + // not a problem for further iterations because cubic curves cannot have more than two local extrema + // (i.e. cubic curves cannot intersect the baseline more than once) + // therefore two subsequent iterations cannot produce sampling with equal length + var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0); + if (iteration > 1 && observedPrecisionRatio < precisionRatio) { + return newSubdivisions; + } + + // otherwise, set up for next iteration + subdivisions = newSubdivisions; + previousLength = length; + } }, - snapToGrid: function(gx, gy) { + isDifferentiable: function() { - this.x = snapToGrid(this.x, gx); - this.y = snapToGrid(this.y, gy || gx); - return this; + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); }, - // Compute the angle between me and `p` and the x axis. - // (cartesian-to-polar coordinates conversion) - // Return theta angle in degrees. - theta: function(p) { + // Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided. + length: function(opt) { - p = Point(p); - // Invert the y-axis. - var y = -(p.y - this.y); - var x = p.x - this.x; - var rad = atan2(y, x); // defined for all 0 corner cases - - // Correction for III. and IV. quadrant. - if (rad < 0) { - rad = 2 * PI + rad; - } - return 180 * rad / PI; - }, + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt - // Compute the angle between vector from me to p1 and the vector from me to p2. - // ordering of points p1 and p2 is important! - // theta function's angle convention: - // returns angles between 0 and 180 when the angle is counterclockwise - // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones - // returns NaN if any of the points p1, p2 is coincident with this point - angleBetween: function(p1, p2) { - - var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - if (angleBetween < 0) { - angleBetween += 360; // correction to keep angleBetween between 0 and 360 + var length = 0; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + length += currentSubdivision.endpointDistance(); } - return angleBetween; - }, - // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. - // Returns NaN if p is at 0,0. - vectorAngle: function(p) { - - var zero = Point(0,0); - return zero.angleBetween(this, p); + return length; }, - toJSON: function() { + // Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.) + lengthAtT: function(t, opt) { - return { x: this.x, y: this.y }; - }, + if (t <= 0) return 0; - // Converts rectangular to polar coordinates. - // An origin can be specified, otherwise it's 0@0. - toPolar: function(o) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt - 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; + var subCurve = this.divide(t)[0]; + var subCurveLength = subCurve.length({ precision: precision }); + + return subCurveLength; }, - toString: function() { + // Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + // Mirrors Line.pointAt() function. + // For a function that tracks `t`, use Curve.pointAtT(). + pointAt: function(ratio, opt) { - return this.x + '@' + this.y; + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + var t = this.tAt(ratio, opt); + + return this.pointAtT(t); }, - update: function(x, y) { + // Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + pointAtLength: function(length, opt) { - this.x = x || 0; - this.y = y || 0; - return this; + var t = this.tAtLength(length, opt); + + return this.pointAtT(t); }, - // Returns the dot product of this point with given other point - dot: function(p) { + // Returns the point at provided `t` between 0 and 1. + // `t` does not track distance along curve as it does in Line objects. + // Non-linear relationship, speeds up and slows down as curve warps! + // For linear length-based solution, use Curve.pointAt(). + pointAtT: function(t) { - return p ? (this.x * p.x + this.y * p.y) : NaN; + if (t <= 0) return this.start.clone(); + if (t >= 1) return this.end.clone(); + + return this.getSkeletonPoints(t).divider; }, - // Returns the cross product of this point relative to two other points - // this point is the common point - // point p1 lies on the first vector, point p2 lies on the second vector - // watch out for the ordering of points p1 and p2! - // positive result indicates a clockwise ("right") turn from first to second vector - // negative result indicates a counterclockwise ("left") turn from first to second vector - // note that the above directions are reversed from the usual answer on the Internet - // that is because we are in a left-handed coord system (because the y-axis points downward) - cross: function(p1, p2) { + // Default precision + PRECISION: 3, - return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; - } - }; + scale: function(sx, sy, origin) { - var Rect = g.Rect = function(x, y, w, h) { + this.start.scale(sx, sy, origin); + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - if (!(this instanceof Rect)) { - return new Rect(x, y, w, h); - } + // Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAt: function(ratio, opt) { - if ((Object(x) === x)) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } + if (!this.isDifferentiable()) return null; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - this.width = w === undefined ? 0 : w; - this.height = h === undefined ? 0 : h; - }; + if (ratio < 0) ratio = 0; + else if (ratio > 1) ratio = 1; - g.Rect.fromEllipse = function(e) { + var t = this.tAt(ratio, opt); - e = Ellipse(e); - return Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); - }; + return this.tangentAtT(t); + }, - g.Rect.prototype = { + // Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAtLength: function(length, opt) { - // 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) { + if (!this.isDifferentiable()) return null; - 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); + var t = this.tAtLength(length, opt); + + return this.tangentAtT(t); }, - bottomLeft: function() { + // Returns a tangent line at requested `t`. + tangentAtT: function(t) { - return Point(this.x, this.y + this.height); - }, + if (!this.isDifferentiable()) return null; - bottomLine: function() { + if (t < 0) t = 0; + else if (t > 1) t = 1; - return Line(this.bottomLeft(), this.corner()); - }, + var skeletonPoints = this.getSkeletonPoints(t); - bottomMiddle: function() { + var p1 = skeletonPoints.startControlPoint2; + var p2 = skeletonPoints.dividerControlPoint1; - return Point(this.x + this.width / 2, this.y + this.height); - }, + var tangentStart = skeletonPoints.divider; - center: function() { + var tangentLine = new Line(p1, p2); + tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested - return Point(this.x + this.width / 2, this.y + this.height / 2); + return tangentLine; }, - clone: function() { + // Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + tAt: function(ratio, opt) { - return Rect(this); - }, + if (ratio <= 0) return 0; + if (ratio >= 1) return 1; - // @return {bool} true if point p is insight me - containsPoint: function(p) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - p = Point(p); - return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + var curveLength = this.length(localOpt); + var length = curveLength * ratio; + + return this.tAtLength(length, localOpt); }, - // @return {bool} true if rectangle `r` is inside me. - containsRect: function(r) { + // Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + // Uses `precision` to approximate length within `precision` (always underestimates) + // Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated + // As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper + // - Precision 0 (chooses one of the two endpoints) - 0 levels + // - Precision 1 (chooses one of 5 points, <10% error) - 1 level + // - Precision 2 (<1% error) - 3 levels + // - Precision 3 (<0.1% error) - 7 levels + // - Precision 4 (<0.01% error) - 15 levels + tAtLength: function(length, opt) { - 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; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - if (!w0 || !h0 || !w1 || !h1) { - // At least one of the dimensions is 0 - return false; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + // identify the subdivision that contains the point at requested `length`: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + //var baseline; // straightened version of subdivision to investigate + //var baselinePoint; // point on the baseline that is the requested distance away from start + var baselinePointDistFromStart; // distance of baselinePoint from start of baseline + var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline + var l = 0; // length so far + var n = subdivisions.length; + var subdivisionSize = 1 / n; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var currentSubdivision = subdivisions[i]; + var d = currentSubdivision.endpointDistance(); // length of current subdivision + + if (length <= (l + d)) { + investigatedSubdivision = currentSubdivision; + + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; + + baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length)); + baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l)); + + break; + } + + l += d; } - var x0 = r0.x; - var y0 = r0.y; - var x1 = r1.x; - var y1 = r1.y; + if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t + // note that precision affects what length is recorded + // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length) + // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1 - w1 += x1; - w0 += x0; - h1 += y1; - h0 += y0; + var curveLength = this.length(localOpt); - return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; - }, + var precisionRatio = pow(10, -precision); - corner: function() { + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(this.x + this.width, this.y + this.height); - }, + // check if we have reached required observed precision + var observedPrecisionRatio; - // @return {boolean} true if rectangles are equal. - equals: function(r) { + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT; + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT; - 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; - }, + // otherwise, set up for next iteration + var newBaselinePointDistFromStart; + var newBaselinePointDistFromEnd; - // @return {rect} if rectangles intersect, {null} if not. - intersect: function(r) { + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = r.origin(); - var rCorner = r.corner(); + var baseline1Length = divided[0].endpointDistance(); + var baseline2Length = divided[1].endpointDistance(); - // No intersection found - if (rCorner.x <= myOrigin.x || - rCorner.y <= myOrigin.y || - rOrigin.x >= myCorner.x || - rOrigin.y >= myCorner.y) return null; + if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0] + investigatedSubdivision = divided[0]; + + investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart; + newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart; - var x = Math.max(myOrigin.x, rOrigin.x); - var y = Math.max(myOrigin.y, rOrigin.y); + } else { // point at requested length is inside divided[1] + investigatedSubdivision = divided[1]; - return Rect(x, y, Math.min(myCorner.x, rCorner.x) - x, Math.min(myCorner.y, rCorner.y) - y); + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length; + newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart; + } + + baselinePointDistFromStart = newBaselinePointDistFromStart; + baselinePointDistFromEnd = newBaselinePointDistFromEnd; + } }, - // 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) { + translate: function(tx, ty) { - p = Point(p); - var center = Point(this.x + this.width / 2, this.y + this.height / 2); - var result; - if (angle) p.rotate(center, angle); + this.start.translate(tx, ty); + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, - // (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); + // Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPoints: function(opt) { - for (var i = sides.length - 1; i >= 0; --i) { - var intersection = sides[i].intersection(connector); - if (intersection !== null) { - result = intersection; - break; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt + + var points = [subdivisions[0].start.clone()]; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + points.push(currentSubdivision.end.clone()); } - if (result && angle) result.rotate(center, -angle); - return result; + + return points; }, - leftLine: function() { + // Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPolyline: function(opt) { - return Line(this.origin(), this.bottomLeft()); + return new Polyline(this.toPoints(opt)); }, - leftMiddle: function() { + toString: function() { + + return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; + + 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(new Point(c.x, c.y), c.a, c.b); + } + + c = new Point(c); + this.x = c.x; + this.y = c.y; + this.a = a; + this.b = b; + }; + + Ellipse.fromRect = function(rect) { + + rect = new Rect(rect); + return new Ellipse(rect.center(), rect.width / 2, rect.height / 2); + }; - return Point(this.x , this.y + this.height / 2); + Ellipse.prototype = { + + bbox: function() { + + return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); }, - // Move and expand me. - // @param r {rectangle} representing deltas - moveAndExpand: function(r) { + clone: function() { - this.x += r.x || 0; - this.y += r.y || 0; - this.width += r.width || 0; - this.height += r.height || 0; - return this; + return new Ellipse(this); }, - // Offset me by the specified amount. - offset: function(dx, dy) { - return Point.prototype.offset.call(this, dx, dy); + /** + * @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, recompute origin [x, y] + // 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 @@ -1441,9043 +1552,15234 @@ var g = (function() { dy = dx; } - this.x -= dx; - this.y -= dy; - this.width += 2 * dx; - this.height += 2 * dy; + this.a += 2 * dx; + this.b += 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() { + /** + * @param {g.Point} p + * @returns {boolean} + */ + containsPoint: function(p) { - return Point(this.x, this.y); + return this.normalizedDistance(p) <= 1; }, - // @return {point} a point on my boundary nearest to the given point. - // @see Squeak Smalltalk, Rectangle>>pointNearestTo: - pointNearestToPoint: function(point) { + /** + * @returns {g.Point} + */ + center: function() { - 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); + return new Point(this.x, this.y); }, - rightLine: function() { + /** 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) { - return Line(this.topRight(), this.corner()); - }, + 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; - rightMiddle: function() { + var q1 = x0 > center.x + a / 2; + var q3 = x0 < center.x - a / 2; - return Point(this.x + this.width, this.y + this.height / 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; - round: function(precision) { + } 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 (new Point(x, y)).theta(p); - 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) { + equals: function(ellipse) { - origin = this.origin().scale(sx, sy, origin); - this.x = origin.x; - this.y = origin.y; - this.width *= sx; - this.height *= sy; - return this; + return !!ellipse && + ellipse.x === this.x && + ellipse.y === this.y && + ellipse.a === this.a && + ellipse.b === this.b; }, - maxRectScaleToFit: function(rect, origin) { - - rect = g.Rect(rect); - origin || (origin = rect.center()); + intersectionWithLine: function(line) { - var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; - var ox = origin.x; - var oy = origin.y; + var intersections = []; + var a1 = line.start; + var a2 = line.end; + var rx = this.a; + var ry = this.b; + var dir = line.vector(); + var diff = a1.difference(new Point(this)); + var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)); + var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)); - // 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. + var a = dir.dot(mDir); + var b = dir.dot(mDiff); + var c = diff.dot(mDiff) - 1.0; + var d = b * b - a * c; - sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; + if (d < 0) { + return null; + } else if (d > 0) { + var root = sqrt(d); + var ta = (-b - root) / a; + var tb = (-b + root) / a; - // 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 ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) { + // if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside + return null; + } else { + if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta)); + if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb)); + } + } else { + var t = -b / a; + if (0 <= t && t <= 1) { + intersections.push(a1.lerp(a2, t)); + } else { + // outside + return null; + } } - if (p4.y > oy) { - sy4 = (this.y + this.height - oy) / (p4.y - oy); + + return intersections; + }, + + // 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 = new Point(p); + + if (angle) p.rotate(new 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(new Point(this.x, this.y), -angle); + return result; } - return { - sx: Math.min(sx1, sx2, sx3, sx4), - sy: Math.min(sy1, sy2, sy3, sy4) - }; + 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 = new Point(this.x + x, this.y + y); + + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; }, - maxRectUniformScaleToFit: function(rect, origin) { + toString: function() { - var scale = this.maxRectScaleToFit(rect, origin); - return Math.min(scale.sx, scale.sy); + return (new 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); + } + + if (p1 instanceof Line) { + return new Line(p1.start, p1.end); + } + + this.start = new Point(p1); + this.end = new Point(p2); + }; + + Line.prototype = { + + bbox: function() { + + var left = min(this.start.x, this.end.x); + var top = min(this.start.y, this.end.y); + var right = max(this.start.x, this.end.x); + var bottom = max(this.start.y, this.end.y); + + return new Rect(left, top, (right - left), (bottom - top)); }, - // @return {string} (left|right|top|bottom) side which is nearest to point - // @see Squeak Smalltalk, Rectangle>>sideNearestTo: - sideNearestToPoint: function(point) { + // @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() { - 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'; + 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)); - if (distToRight < closest) { - closest = distToRight; - side = 'right'; - } - if (distToTop < closest) { - closest = distToTop; - side = 'top'; - } - if (distToBottom < closest) { - closest = distToBottom; - side = 'bottom'; - } - return side; + 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]; }, - snapToGrid: function(gx, gy) { + clone: function() { - 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; + return new Line(this.start, this.end); }, - topLine: function() { + // @return {point} the closest point on the line to point `p` + closestPoint: function(p) { - return Line(this.origin(), this.topRight()); + return this.pointAt(this.closestPointNormalizedLength(p)); }, - topMiddle: function() { + closestPointLength: function(p) { - return Point(this.x + this.width / 2, this.y); + return this.closestPointNormalizedLength(p) * this.length(); }, - topRight: function() { + // @return {number} the normalized length of the closest point on the line to point `p` + closestPointNormalizedLength: function(p) { + + var product = this.vector().dot((new Line(this.start, p)).vector()); + var cpNormalizedLength = min(1, max(0, product / this.squaredLength())); - return Point(this.x + this.width, this.y); + // cpNormalizedLength returns `NaN` if this line has zero length + // we can work with that - if `NaN`, return 0 + if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN` + // (`NaN` is the only value that is not equal to itself) + + return cpNormalizedLength; }, - toJSON: function() { + closestPointTangent: function(p) { - return { x: this.x, y: this.y, width: this.width, height: this.height }; + return this.tangentAt(this.closestPointNormalizedLength(p)); }, - toString: function() { + equals: function(l) { - return this.origin().toString() + ' ' + this.corner().toString(); + return !!l && + 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 {rect} representing the union of both rectangles. - union: function(rect) { + intersectionWithLine: function(line) { - rect = Rect(rect); - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = rect.origin(); - var rCorner = rect.corner(); + var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y); + var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y); + var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); + var deltaPt = new Point(line.start.x - this.start.x, line.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); - 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); + if (det === 0 || alpha * det < 0 || beta * det < 0) { + // No intersection found. + return null; + } - return Rect(originX, originY, cornerX - originX, cornerY - originY); - } - }; + if (det > 0) { + if (alpha > det || beta > det) { + return null; + } - var Polyline = g.Polyline = function(points) { + } else { + if (alpha < det || beta < det) { + return null; + } + } - if (!(this instanceof Polyline)) { - return new Polyline(points); - } + return [new Point( + this.start.x + (alpha * pt1Dir.x / det), + this.start.y + (alpha * pt1Dir.y / det) + )]; + }, - this.points = (Array.isArray(points)) ? points.map(Point) : []; - }; + // @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(shape, opt) { - Polyline.prototype = { + if (shape instanceof Line || + shape instanceof Rect || + shape instanceof Polyline || + shape instanceof Ellipse || + shape instanceof Path + ) { + var intersection = shape.intersectionWithLine(this, opt); - pointAtLength: function(length) { - var points = this.points; - var l = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var a = points[i]; - var b = points[i+1]; - var d = a.distance(b); - l += d; - if (length <= l) { - return Line(b, a).pointAt(d ? (l - length) / d : 0); + // Backwards compatibility + if (intersection && (shape instanceof Line)) { + intersection = intersection[0]; } + + return intersection; } + return null; }, + isDifferentiable: function() { + + return !this.start.equals(this.end); + }, + + // @return {double} length of the line length: function() { - var points = this.points; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - length += points[i].distance(points[i+1]); - } - return length; + + return sqrt(this.squaredLength()); }, - closestPoint: function(p) { - return this.pointAtLength(this.closestPointLength(p)); + // @return {point} my midpoint + midpoint: function() { + + return new Point( + (this.start.x + this.end.x) / 2, + (this.start.y + this.end.y) / 2 + ); }, - closestPointLength: function(p) { - var points = this.points; - var pointLength; - var minSqrDistance = Infinity; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var line = Line(points[i], points[i+1]); - var lineLength = line.length(); - var cpNormalizedLength = line.closestPointNormalizedLength(p); - var cp = line.pointAt(cpNormalizedLength); - var sqrDistance = cp.squaredDistance(p); - if (sqrDistance < minSqrDistance) { - minSqrDistance = sqrDistance; - pointLength = length + cpNormalizedLength * lineLength; - } - length += lineLength; - } - return pointLength; - }, - - toString: function() { + // @return {point} my point at 't' <0,1> + pointAt: function(t) { - return this.points + ''; - }, + var start = this.start; + var end = this.end; - // Returns a convex-hull polyline from this polyline. - // this function implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan) - // output polyline starts at the first element of the original polyline that is on the hull - // output polyline then continues clockwise from that point - convexHull: function() { + if (t <= 0) return start.clone(); + if (t >= 1) return end.clone(); - var i; - var n; + return start.lerp(end, t); + }, - var points = this.points; + pointAtLength: function(length) { - // step 1: find the starting point - point with the lowest y (if equality, highest x) - var startPoint; - n = points.length; - for (i = 0; i < n; i++) { - if (startPoint === undefined) { - // if this is the first point we see, set it as start point - startPoint = points[i]; - } else if (points[i].y < startPoint.y) { - // start point should have lowest y from all points - startPoint = points[i]; - } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { - // if two points have the lowest y, choose the one that has highest x - // there are no points to the right of startPoint - no ambiguity about theta 0 - // if there are several coincident start point candidates, first one is reported - startPoint = points[i]; - } - } + var start = this.start; + var end = this.end; - // step 2: sort the list of points - // sorting by angle between line from startPoint to point and the x-axis (theta) - - // step 2a: create the point records = [point, originalIndex, angle] - var sortedPointRecords = []; - n = points.length; - for (i = 0; i < n; i++) { - var angle = startPoint.theta(points[i]); - if (angle === 0) { - angle = 360; // give highest angle to start point - // the start point will end up at end of sorted list - // the start point will end up at beginning of hull points list - } - - var entry = [points[i], i, angle]; - sortedPointRecords.push(entry); + fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - // step 2b: sort the list in place - sortedPointRecords.sort(function(record1, record2) { - // returning a negative number here sorts record1 before record2 - // if first angle is smaller than second, first angle should come before second - var sortOutput = record1[2] - record2[2]; // negative if first angle smaller - if (sortOutput === 0) { - // if the two angles are equal, sort by originalIndex - sortOutput = record2[1] - record1[1]; // negative if first index larger - // coincident points will be sorted in reverse-numerical order - // so the coincident points with lower original index will be considered first - } - return sortOutput; - }); + var lineLength = this.length(); + if (length >= lineLength) return (fromStart ? end.clone() : start.clone()); - // step 2c: duplicate start record from the top of the stack to the bottom of the stack - if (sortedPointRecords.length > 2) { - var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; - sortedPointRecords.unshift(startPointRecord); - } + return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength); + }, - // step 3a: go through sorted points in order and find those with right turns - // we want to get our results in clockwise order - var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull - var hullPointRecords = []; // stack of records with right turns - hull point candidates + // @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) { - var currentPointRecord; - var currentPoint; - var lastHullPointRecord; - var lastHullPoint; - var secondLastHullPointRecord; - var secondLastHullPoint; - while (sortedPointRecords.length !== 0) { - currentPointRecord = sortedPointRecords.pop(); - currentPoint = currentPointRecord[0]; + // Find the sign of the determinant of vectors (start,end), where p is the query point. + p = new g.Point(p); + var start = this.start; + var end = this.end; + var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x)); - // check if point has already been discarded - // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' - if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { - // this point had an incorrect turn at some previous iteration of this loop - // this disqualifies it from possibly being on the hull - continue; - } + return determinant / this.length(); + }, - var correctTurnFound = false; - while (!correctTurnFound) { - if (hullPointRecords.length < 2) { - // not enough points for comparison, just add current point - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; - - } else { - lastHullPointRecord = hullPointRecords.pop(); - lastHullPoint = lastHullPointRecord[0]; - secondLastHullPointRecord = hullPointRecords.pop(); - secondLastHullPoint = secondLastHullPointRecord[0]; + rotate: function(origin, angle) { - var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); + this.start.rotate(origin, angle); + this.end.rotate(origin, angle); + return this; + }, - if (crossProduct < 0) { - // found a right turn - hullPointRecords.push(secondLastHullPointRecord); - hullPointRecords.push(lastHullPointRecord); - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; + round: function(precision) { - } else if (crossProduct === 0) { - // the three points are collinear - // three options: - // there may be a 180 or 0 degree angle at lastHullPoint - // or two of the three points are coincident - var THRESHOLD = 1e-10; // we have to take rounding errors into account - var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); - if (Math.abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 - // if the cross product is 0 because the angle is 180 degrees - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { - // if the cross product is 0 because two points are the same - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (Math.abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 - // if the cross product is 0 because the angle is 0 degrees - // remove last hull point from hull BUT do not discard it - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // put last hull point back into the sorted point records list - sortedPointRecords.push(lastHullPointRecord); - // we are switching the order of the 0deg and 180deg points - // correct turn not found - } + var f = pow(10, precision || 0); + this.start.x = round(this.start.x * f) / f; + this.start.y = round(this.start.y * f) / f; + this.end.x = round(this.end.x * f) / f; + this.end.y = round(this.end.y * f) / f; + return this; + }, - } else { - // found a left turn - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter of loop) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - } - } - } - } - // at this point, hullPointRecords contains the output points in clockwise order - // the points start with lowest-y,highest-x startPoint, and end at the same point + scale: function(sx, sy, origin) { - // step 3b: remove duplicated startPointRecord from the end of the array - if (hullPointRecords.length > 2) { - hullPointRecords.pop(); - } + this.start.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - // step 4: find the lowest originalIndex record and put it at the beginning of hull - var lowestHullIndex; // the lowest originalIndex on the hull - var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex - n = hullPointRecords.length; - for (i = 0; i < n; i++) { - var currentHullIndex = hullPointRecords[i][1]; + // @return {number} scale the line so that it has the requested length + setLength: function(length) { - if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { - lowestHullIndex = currentHullIndex; - indexOfLowestHullIndexRecord = i; - } - } + var currentLength = this.length(); + if (!currentLength) return this; - var hullPointRecordsReordered = []; - if (indexOfLowestHullIndexRecord > 0) { - var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); - var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); - hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - } else { - hullPointRecordsReordered = hullPointRecords; - } + var scaleFactor = length / currentLength; + return this.scale(scaleFactor, scaleFactor, this.start); + }, - var hullPoints = []; - n = hullPointRecordsReordered.length; - for (i = 0; i < n; i++) { - hullPoints.push(hullPointRecordsReordered[i][0]); - } + // @return {integer} length without sqrt + // @note for applications where the exact length is not necessary (e.g. compare only) + squaredLength: function() { - return Polyline(hullPoints); - } - }; + 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; + }, + tangentAt: function(t) { - g.scale = { + if (!this.isDifferentiable()) return null; - // Return the `value` from the `domain` interval scaled to the `range` interval. - linear: function(domain, range, value) { + var start = this.start; + var end = this.end; - var domainSpan = domain[1] - domain[0]; - var rangeSpan = range[1] - range[0]; - return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; - } - }; + var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1 - var normalizeAngle = g.normalizeAngle = function(angle) { + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - return (angle % 360) + (angle < 0 ? 360 : 0); - }; + return tangentLine; + }, - var snapToGrid = g.snapToGrid = function(value, gridSize) { + tangentAtLength: function(length) { - return gridSize * Math.round(value / gridSize); - }; + if (!this.isDifferentiable()) return null; - var toDeg = g.toDeg = function(rad) { + var start = this.start; + var end = this.end; - return (180 * rad / PI) % 360; - }; + var tangentStart = this.pointAtLength(length); - var toRad = g.toRad = function(deg, over360) { + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - over360 = over360 || false; - deg = over360 ? deg : (deg % 360); - return deg * PI / 180; - }; + return tangentLine; + }, - // For backwards compatibility: - g.ellipse = g.Ellipse; - g.line = g.Line; - g.point = g.Point; - g.rect = g.Rect; + translate: function(tx, ty) { - return g; + this.start.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, -})(); + // @return vector {point} of the line + vector: function() { -// Vectorizer. -// ----------- + return new Point(this.end.x - this.start.x, this.end.y - this.start.y); + }, -// A tiny library for making your life easier when dealing with SVG. -// The only Vectorizer dependency is the Geometry library. + toString: function() { + return this.start.toString() + ' ' + this.end.toString(); + } + }; -var V; -var Vectorizer; + // For backwards compatibility: + Line.prototype.intersection = Line.prototype.intersect; -V = Vectorizer = (function() { + // Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. + // Path created is not guaranteed to be a valid (serializable) path (might not start with an M). + var Path = g.Path = function(arg) { - 'use strict'; + if (!(this instanceof Path)) { + return new Path(arg); + } - var hasSvg = typeof window === 'object' && - !!( - window.SVGAngle || - document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') - ); + if (typeof arg === 'string') { // create from a path data string + return new Path.parse(arg); + } - // SVG support is required. - if (!hasSvg) { + this.segments = []; - // Return a function that throws an error when it is used. - return function() { - throw new Error('SVG is required to use Vectorizer.'); - }; - } + var i; + var n; - // 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' - }; + if (!arg) { + // don't do anything - var SVGversion = '1.1'; + } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array + n = arg.length; + if (arg[0].isSegment) { // create from an array of segments + for (i = 0; i < n; i++) { - var V = function(el, attrs, children) { + var segment = arg[i]; - // This allows using V() without the new keyword. - if (!(this instanceof V)) { - return V.apply(Object.create(V.prototype), arguments); - } + this.appendSegment(segment); + } - if (!el) return; + } else { // create from an array of Curves and/or Lines + var previousObj = null; + for (i = 0; i < n; i++) { - if (V.isV(el)) { - el = el.node; - } + var obj = arg[i]; - attrs = attrs || {}; + if (!((obj instanceof Line) || (obj instanceof Curve))) { + throw new Error('Cannot construct a path segment from the provided object.'); + } - if (V.isString(el)) { + if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); - if (el.toLowerCase() === 'svg') { + // if objects do not link up, moveto segments are inserted to cover the gaps + if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); - // Create a new SVG canvas. - el = V.createSvgDocument(); + if (obj instanceof Line) { + this.appendSegment(Path.createSegment('L', obj.end)); - } else if (el[0] === '<') { + } else if (obj instanceof Curve) { + this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); + } - // Create element from an SVG string. - // Allows constructs of type: `document.appendChild(V('').node)`. + previousObj = obj; + } + } - var svgDoc = V.createSvgDocument(el); + } else if (arg.isSegment) { // create from a single segment + this.appendSegment(arg); - // 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) { + } else if (arg instanceof Line) { // create from a single Line + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('L', arg.end)); - // Map child nodes to `V`s. - var arrayOfVels = []; - var i, len; + } else if (arg instanceof Curve) { // create from a single Curve + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); - for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { + } else if (arg instanceof Polyline && arg.points && arg.points.length !== 0) { // create from a Polyline + n = arg.points.length; + for (i = 0; i < n; i++) { - var childNode = svgDoc.childNodes[i]; - arrayOfVels.push(new V(document.importNode(childNode, true))); - } + var point = arg.points[i]; - return arrayOfVels; - } + if (i === 0) this.appendSegment(Path.createSegment('M', point)); + else this.appendSegment(Path.createSegment('L', point)); + } + } + }; - el = document.importNode(svgDoc.firstChild, true); + // More permissive than V.normalizePathData and Path.prototype.serialize. + // Allows path data strings that do not start with a Moveto command (unlike SVG specification). + // Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). + // Allows for command argument chaining. + // Throws an error if wrong number of arguments is provided with a command. + // Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). + Path.parse = function(pathData) { - } else { + if (!pathData) return new Path(); - el = document.createElementNS(ns.xmlns, el); - } + var path = new Path(); - V.ensureId(el); - } + var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; + var commands = pathData.match(commandRe); - this.node = el; + var numCommands = commands.length; + for (var i = 0; i < numCommands; i++) { - this.setAttributes(attrs); + var command = commands[i]; + var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?))|(?:(?:-?\.\d+))/g; + var args = command.match(argRe); - if (children) { - this.append(children); + var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] + path.appendSegment(segment); } - return this; + return path; }; - /** - * @param {SVGGElement} toElem - * @returns {SVGMatrix} - */ - V.prototype.getTransformToElement = function(toElem) { - toElem = V.toNode(toElem); - return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); - }; + // Create a segment or an array of segments. + // Accepts unlimited points/coords arguments after `type`. + Path.createSegment = function(type) { - /** - * @param {SVGMatrix} matrix - * @param {Object=} opt - * @returns {Vectorizer|SVGMatrix} Setter / Getter - */ - V.prototype.transform = function(matrix, opt) { + if (!type) throw new Error('Type must be provided.'); - var node = this.node; - if (V.isUndefined(matrix)) { - return V.transformStringToMatrix(this.attr('transform')); - } + var segmentConstructor = Path.segmentTypes[type]; + if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); - if (opt && opt.absolute) { - return this.attr('transform', V.matrixToTransformString(matrix)); + var args = []; + var n = arguments.length; + for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array + args.push(arguments[i]); } - var svgTransform = V.createSVGTransform(matrix); - node.transform.baseVal.appendItem(svgTransform); - return this; - }; + return applyToNew(segmentConstructor, args); + }, - V.prototype.translate = function(tx, ty, opt) { + Path.prototype = { - opt = opt || {}; - ty = ty || 0; + // Accepts one segment or an array of segments as argument. + // Throws an error if argument is not a segment or an array of segments. + appendSegment: function(arg) { - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; - // Is it a getter? - if (V.isUndefined(tx)) { - return transform.translate; - } + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); + var currentSegment; - var newTx = opt.absolute ? tx : transform.translate.tx + tx; - var newTy = opt.absolute ? ty : transform.translate.ty + ty; - var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; + var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null + var nextSegment = null; - // Note that `translate()` is always the first transformation. This is - // usually the desired case. - this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); - return this; - }; + if (!Array.isArray(arg)) { // arg is a segment + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - V.prototype.rotate = function(angle, cx, cy, opt) { + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.push(currentSegment); - opt = opt || {}; + } else { // arg is an array of segments + if (!arg[0].isSegment) throw new Error('Segments required.'); - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; + var n = arg.length; + for (var i = 0; i < n; i++) { - // Is it a getter? - if (V.isUndefined(angle)) { - return transform.rotate; - } + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.push(currentSegment); + previousSegment = currentSegment; + } + } + }, - transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); + // Returns the bbox of the path. + // If path has no segments, returns null. + // If path has only invisible segments, returns bbox of the end point of last segment. + bbox: function() { - angle %= 360; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - var newAngle = opt.absolute ? angle : transform.rotate.angle + angle; - var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : ''; - var newRotate = 'rotate(' + newAngle + newOrigin + ')'; + var bbox; + for (var i = 0; i < numSegments; i++) { - this.attr('transform', (transformAttr + ' ' + newRotate).trim()); - return this; - }; + var segment = segments[i]; + if (segment.isVisible) { + var segmentBBox = segment.bbox(); + bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; + } + } - // Note that `scale` as the only transformation does not combine with previous values. - V.prototype.scale = function(sx, sy) { + if (bbox) return bbox; - sy = V.isUndefined(sy) ? sx : sy; + // if the path has only invisible elements, return end point of last segment + var lastSegment = segments[numSegments - 1]; + return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); + }, - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; + // Returns a new path that is a clone of this path. + clone: function() { - // Is it a getter? - if (V.isUndefined(sx)) { - return transform.scale; - } + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim(); + var path = new Path(); + for (var i = 0; i < numSegments; i++) { - var newScale = 'scale(' + sx + ',' + sy + ')'; + var segment = segments[i].clone(); + path.appendSegment(segment); + } - this.attr('transform', (transformAttr + ' ' + newScale).trim()); - return this; - }; + return path; + }, - // 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) { + closestPoint: function(p, opt) { - var box; - var node = this.node; - var ownerSVGElement = node.ownerSVGElement; + var t = this.closestPointT(p, opt); + if (!t) return null; - // 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); - } + return this.pointAtT(t); + }, - try { + closestPointLength: function(p, opt) { - box = node.getBBox(); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - } catch (e) { + var t = this.closestPointT(p, localOpt); + if (!t) return 0; - // Fallback for IE. - box = { - x: node.clientLeft, - y: node.clientTop, - width: node.clientWidth, - height: node.clientHeight - }; - } + return this.lengthAtT(t, localOpt); + }, - if (withoutTransformations) { - return g.Rect(box); - } + closestPointNormalizedLength: function(p, opt) { - var matrix = this.getTransformToElement(target || ownerSVGElement); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - return V.transformRect(box, matrix); - }; - - // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, - // i.e. after transformations are applied. - // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. - // Takes an (Object) `opt` argument (optional) with the following attributes: - // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this - // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); - V.prototype.getBBox = function(opt) { + var cpLength = this.closestPointLength(p, localOpt); + if (cpLength === 0) return 0; // shortcut - var options = {}; + var length = this.length(localOpt); + if (length === 0) return 0; // prevents division by zero - var outputBBox; - var node = this.node; - var ownerSVGElement = node.ownerSVGElement; + return cpLength / length; + }, - // 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); - } + // Private function. + closestPointT: function(p, opt) { - if (opt) { - if (opt.target) { // check if target exists - options.target = V.toNode(opt.target); // works for V objects, jquery objects, and node objects - } - if (opt.recursive) { - options.recursive = opt.recursive; - } - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!options.recursive) { - try { - outputBBox = node.getBBox(); - } catch (e) { - // Fallback for IE. - outputBBox = { - x: node.clientLeft, - y: node.clientTop, - width: node.clientWidth, - height: node.clientHeight - }; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - if (!options.target) { - // transform like this (that is, not at all) - return g.Rect(outputBBox); - } else { - // transform like target - var matrix = this.getTransformToElement(options.target); - return V.transformRect(outputBBox, matrix); - } - } else { // if we want to calculate the bbox recursively - // browsers report correct bbox around svg elements (one that envelops the path lines tightly) - // but some browsers fail to report the same bbox when the elements are in a group (returning a looser bbox that also includes control points, like node.getClientRect()) - // this happens even if we wrap a single svg element into a group! - // this option setting makes the function recursively enter all the groups from this and deeper, get bboxes of the elements inside, then return a union of those bboxes + var closestPointT; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { - var children = this.children(); - var n = children.length; - - if (n === 0) { - return this.getBBox({ target: options.target, recursive: false }); + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isVisible) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointT = { segmentIndex: i, value: segmentClosestPointT }; + minSquaredDistance = squaredDistance; + } + } } - // recursion's initial pass-through setting: - // recursive passes-through just keep the target as whatever was set up here during the initial pass-through - if (!options.target) { - // transform children/descendants like this (their parent/ancestor) - options.target = this; - } // else transform children/descendants like target + if (closestPointT) return closestPointT; - for (var i = 0; i < n; i++) { - var currentChild = children[i]; + // if no visible segment, return end of last segment + return { segmentIndex: numSegments - 1, value: 1 }; + }, - var childBBox; + closestPointTangent: function(p, opt) { - // if currentChild is not a group element, get its bbox with a nonrecursive call - if (currentChild.children().length === 0) { - childBBox = currentChild.getBBox({ target: options.target, recursive: false }); - } - else { - // if currentChild is a group element (determined by checking the number of children), enter it with a recursive call - childBBox = currentChild.getBBox({ target: options.target, recursive: true }); - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!outputBBox) { - // if this is the first iteration - outputBBox = childBBox; - } else { - // make a new bounding box rectangle that contains this child's bounding box and previous bounding box - outputBBox = outputBBox.union(childBBox); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var closestPointTangent; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isDifferentiable()) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointTangent = segment.tangentAtT(segmentClosestPointT); + minSquaredDistance = squaredDistance; + } } } - return outputBBox; - } - }; + if (closestPointTangent) return closestPointTangent; - V.prototype.text = function(content, opt) { + // if no valid segment, return null + return null; + }, - // 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 eol = opt.eol; - var lines = content.split('\n'); - var tspan; + // Checks whether two paths are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { - // 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'); - } + if (!p) return false; - // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. - this.attr('xml:space', 'preserve'); + var segments = this.segments; + var otherSegments = p.segments; - // Easy way to erase all `` children; - this.node.textContent = ''; + var numSegments = segments.length; + if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal - var textNode = this.node; + for (var i = 0; i < numSegments; i++) { - if (opt.textPath) { + var segment = segments[i]; + var otherSegment = otherSegments[i]; - // 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); + // as soon as an inequality is found in segments, return false + if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; } - // 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); - } + // if no inequality found in segments, return true + return true; + }, - 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); - } + // Accepts negative indices. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + getSegment: function(index) { - 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 segments = this.segments; + var numSegments = segments.length; + if (!numSegments === 0) throw new Error('Path has no segments.'); - var offset = 0; - var x = ((opt.x !== undefined) ? opt.x : this.attr('x')) || 0; + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - // Shift all the but first by one line (`1em`) - var lineHeight = opt.lineHeight || '1em'; - if (opt.lineHeight === 'auto') { - lineHeight = '1.5em'; - } + return segments[index]; + }, - var firstLineHeight = 0; - for (var i = 0; i < lines.length; i++) { + // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. + getSegmentSubdivisions: function(opt) { - var vLineAttributes = { 'class': 'v-line' }; - if (i === 0) { - vLineAttributes.dy = '0em'; - } else { - vLineAttributes.dy = lineHeight; - vLineAttributes.x = x; - } - var vLine = V('tspan', vLineAttributes); + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - var lastI = lines.length - 1; - var line = lines[i]; - if (line) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.segmentSubdivisions + // not using localOpt - // Get the line height based on the biggest font size in the annotations for this line. - var maxFontSize = 0; - if (opt.annotations) { + var segmentSubdivisions = []; + for (var i = 0; i < numSegments; i++) { - // 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 }); + var segment = segments[i]; + var subdivisions = segment.getSubdivisions({ precision: precision }); + segmentSubdivisions.push(subdivisions); + } - var lastJ = lineAnnotations.length - 1; - for (var j = 0; j < lineAnnotations.length; j++) { + return segmentSubdivisions; + }, - var annotation = lineAnnotations[j]; - if (V.isObject(annotation)) { + // Insert `arg` at given `index`. + // `index = 0` means insert at the beginning. + // `index = segments.length` means insert at the end. + // Accepts negative indices, from `-1` to `-(segments.length + 1)`. + // Accepts one segment or an array of segments as argument. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + insertSegment: function(index, arg) { - var fontSize = parseFloat(annotation.attrs['font-size']); - if (fontSize && fontSize > maxFontSize) { - maxFontSize = fontSize; - } + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - 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']); - } + // note that these are incremented comapared to getSegments() + // we can insert after last element (note that this changes the meaning of index -1) + if (index < 0) index = numSegments + index + 1; // convert negative indices to positive + if (index > numSegments || index < 0) throw new Error('Index out of range.'); - if (eol && j === lastJ && i !== lastI) { - annotation.t += eol; - } - tspan.node.textContent = annotation.t; + var currentSegment; - } else { + var previousSegment = null; + var nextSegment = null; - if (eol && j === lastJ && i !== lastI) { - annotation += eol; - } - tspan = document.createTextNode(annotation || ' '); - } - vLine.append(tspan); - } + if (numSegments !== 0) { + if (index >= 1) { + previousSegment = segments[index - 1]; + nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null - if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) { + } else { // if index === 0 + // previousSegment is null + nextSegment = segments[0]; + } + } - vLine.attr('dy', (maxFontSize * 1.2) + 'px'); - } + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - } else { + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 0, currentSegment); - if (eol && i !== lastI) { - line += eol; - } + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); - vLine.node.textContent = line; - } + var n = arg.length; + for (var i = 0; i < n; i++) { - if (i === 0) { - firstLineHeight = maxFontSize; + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; } - } 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 = '-'; + isDifferentiable: function() { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + // as soon as a differentiable segment is found in segments, return true + if (segment.isDifferentiable()) return true; } - V(textNode).append(vLine); + // if no differentiable segment is found in segments, return false + return false; + }, - offset += line.length + 1; // + 1 = newline character. - } + // Checks whether current path segments are valid. + // Note that d is allowed to be empty - should disable rendering of the path. + isValid: function() { - // `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 === null) { - this.attr('y', firstLineHeight || '0.8em'); - } + var segments = this.segments; + var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto + return isValid; + }, - return this; - }; + // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. + // If path has no segments, returns 0. + length: function(opt) { - /** - * @public - * @param {string} name - * @returns {Vectorizer} - */ - V.prototype.removeAttr = function(name) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array - var qualifiedName = V.qualifyAttr(name); - var el = this.node; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - if (qualifiedName.ns) { - if (el.hasAttributeNS(qualifiedName.ns, qualifiedName.local)) { - el.removeAttributeNS(qualifiedName.ns, qualifiedName.local); + var length = 0; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + length += segment.length({ subdivisions: subdivisions }); } - } else if (el.hasAttribute(name)) { - el.removeAttribute(name); - } - return this; - }; - V.prototype.attr = function(name, value) { + return length; + }, - if (V.isUndefined(name)) { + // Private function. + lengthAtT: function(t, opt) { - // Return all attributes. - var attributes = this.node.attributes; - var attrs = {}; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array - for (var i = 0; i < attributes.length; i++) { - attrs[attributes[i].name] = attributes[i].value; - } + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return 0; // regardless of t.value - return attrs; - } + var tValue = t.value; + if (segmentIndex >= numSegments) { + segmentIndex = numSegments - 1; + tValue = 1; + } + else if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - if (V.isString(name) && V.isUndefined(value)) { - return this.node.getAttribute(name); - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - if (typeof name === 'object') { + var subdivisions; + var length = 0; + for (var i = 0; i < segmentIndex; i++) { - for (var attrName in name) { - if (name.hasOwnProperty(attrName)) { - this.setAttribute(attrName, name[attrName]); - } + var segment = segments[i]; + subdivisions = segmentSubdivisions[i]; + length += segment.length({ precisison: precision, subdivisions: subdivisions }); } - } else { + segment = segments[segmentIndex]; + subdivisions = segmentSubdivisions[segmentIndex]; + length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); - this.setAttribute(name, value); - } + return length; + }, - return this; - }; + // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + pointAt: function(ratio, opt) { - V.prototype.remove = function() { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (this.node.parentNode) { - this.node.parentNode.removeChild(this.node); - } + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - V.prototype.empty = function() { + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - while (this.node.firstChild) { - this.node.removeChild(this.node.firstChild); - } + return this.pointAtLength(length, localOpt); + }, - return this; - }; + // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + pointAtLength: function(length, opt) { - /** - * @private - * @param {object} attrs - * @returns {Vectorizer} - */ - V.prototype.setAttributes = function(attrs) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - for (var key in attrs) { - if (attrs.hasOwnProperty(key)) { - this.setAttribute(key, attrs[key]); + if (length === 0) return this.start.clone(); + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - } - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - V.prototype.append = function(els) { + var lastVisibleSegment; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - if (!V.isArray(els)) { - els = [els]; - } + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - for (var i = 0, len = els.length; i < len; i++) { - this.node.appendChild(V.toNode(els[i])); - } + if (segment.isVisible) { + if (length <= (l + d)) { + return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } - return this; - }; + lastVisibleSegment = segment; + } - V.prototype.prepend = function(els) { + l += d; + } - var child = this.node.firstChild; - return child ? V(child).before(els) : this.append(els); - }; + // if length requested is higher than the length of the path, return last visible segment endpoint + if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); - V.prototype.before = function(els) { + // if no visible segment, return last segment end point (no matter if fromStart or no) + var lastSegment = segments[numSegments - 1]; + return lastSegment.end.clone(); + }, - var node = this.node; - var parent = node.parentNode; + // Private function. + pointAtT: function(t) { - if (parent) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!V.isArray(els)) { - els = [els]; - } + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].pointAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); - for (var i = 0, len = els.length; i < len; i++) { - parent.insertBefore(V.toNode(els[i]), node); - } - } + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - return this; - }; + return segments[segmentIndex].pointAtT(tValue); + }, - V.prototype.appendTo = function(node) { - V.toNode(node).appendChild(this.node); - return this; - }, + // Helper method for adding segments. + prepareSegment: function(segment, previousSegment, nextSegment) { - V.prototype.svg = function() { + // insert after previous segment and before previous segment's next segment + segment.previousSegment = previousSegment; + segment.nextSegment = nextSegment; + if (previousSegment) previousSegment.nextSegment = segment; + if (nextSegment) nextSegment.previousSegment = segment; - return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); - }; + var updateSubpathStart = segment; + if (segment.isSubpathStart) { + segment.subpathStartSegment = segment; // assign self as subpath start segment + updateSubpathStart = nextSegment; // start updating from next segment + } - V.prototype.defs = function() { + // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments + if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); - var defs = this.svg().node.getElementsByTagName('defs'); + return segment; + }, - return (defs && defs.length) ? V(defs[0]) : undefined; - }; + // Default precision + PRECISION: 3, - V.prototype.clone = function() { + // Remove the segment at `index`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + removeSegment: function(index) { - 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; - }; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); - V.prototype.findOne = function(selector) { + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - var found = this.node.querySelector(selector); - return found ? V(found) : undefined; - }; + var removedSegment = segments.splice(index, 1)[0]; + var previousSegment = removedSegment.previousSegment; + var nextSegment = removedSegment.nextSegment; - V.prototype.find = function(selector) { + // link the previous and next segments together (if present) + if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null + if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null - var vels = []; - var nodes = this.node.querySelectorAll(selector); + // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached + if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, - if (nodes) { + // Replace the segment at `index` with `arg`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Accepts one segment or an array of segments as argument. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + replaceSegment: function(index, arg) { - // Map DOM elements to `V`s. - for (var i = 0; i < nodes.length; i++) { - vels.push(V(nodes[i])); - } - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); - return vels; - }; + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - // Returns an array of V elements made from children of this.node. - V.prototype.children = function() { + var currentSegment; - var children = this.node.childNodes; - - var outputArray = []; - for (var i = 0; i < children.length; i++) { - var currentChild = children[i]; - if (currentChild.nodeType === 1) { - outputArray.push(V(children[i])); - } - } - return outputArray; - }; + var replacedSegment = segments[index]; + var previousSegment = replacedSegment.previousSegment; + var nextSegment = replacedSegment.nextSegment; - // Find an index of an element inside its container. - V.prototype.index = function() { + var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? - var index = 0; - var node = this.node.previousSibling; + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - while (node) { - // nodeType 1 for ELEMENT_NODE - if (node.nodeType === 1) index++; - node = node.previousSibling; - } + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 1, currentSegment); // directly replace - return index; - }; + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` - V.prototype.findParentByClass = function(className, terminator) { + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); - var ownerSVGElement = this.node.ownerSVGElement; - var node = this.node.parentNode; + segments.splice(index, 1); - while (node && node !== terminator && node !== ownerSVGElement) { + var n = arg.length; + for (var i = 0; i < n; i++) { - var vel = V(node); - if (vel.hasClass(className)) { - return vel; + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; + + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + } } - node = node.parentNode; - } + // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached + if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, - return null; - }; + scale: function(sx, sy, origin) { - // https://jsperf.com/get-common-parent - V.prototype.contains = function(el) { + var segments = this.segments; + var numSegments = segments.length; - var a = this.node; - var b = V.toNode(el); - var bup = b && b.parentNode; + for (var i = 0; i < numSegments; i++) { - return (a === bup) || !!(bup && bup.nodeType === 1 && (a.compareDocumentPosition(bup) & 16)); - }; + var segment = segments[i]; + segment.scale(sx, sy, origin); + } - // Convert global point into the coordinate space of this element. - V.prototype.toLocalPoint = function(x, y) { + return this; + }, - var svg = this.svg().node; + segmentAt: function(ratio, opt) { - var p = svg.createSVGPoint(); - p.x = x; - p.y = y; + var index = this.segmentIndexAt(ratio, opt); + if (!index) return null; - try { + return this.getSegment(index); + }, - var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse()); - var globalToLocalMatrix = this.getTransformToElement(svg).inverse(); + // Accepts negative length. + segmentAtLength: function(length, opt) { - } 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; - } + var index = this.segmentIndexAtLength(length, opt); + if (!index) return null; - return globalPoint.matrixTransform(globalToLocalMatrix); - }; + return this.getSegment(index); + }, - V.prototype.translateCenterToPoint = function(p) { + segmentIndexAt: function(ratio, opt) { - var bbox = this.getBBox({ target: this.svg() }); - var center = bbox.center(); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - this.translate(p.x - center.x, p.y - center.y); - return this; - }; + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - // 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) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - // 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 pathLength = this.length(localOpt); + var length = pathLength * ratio; - var s = this.scale(); - this.attr('transform', ''); - this.scale(s.sx, s.sy); + return this.segmentIndexAtLength(length, localOpt); + }, - var svg = this.svg().node; - var bbox = this.getBBox({ target: target || svg }); + toPoints: function(opt) { - // 1. Translate to origin. - var translateToOrigin = svg.createSVGTransform(); - translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - // 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); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + + var points = []; + var partialPoints = []; + for (var i = 0; i < numSegments; i++) { + var segment = segments[i]; + if (segment.isVisible) { + var currentSegmentSubdivisions = segmentSubdivisions[i]; + if (currentSegmentSubdivisions.length > 0) { + var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { + return curve.start; + }); + Array.prototype.push.apply(partialPoints, subdivisionPoints); + } else { + partialPoints.push(segment.start); + } + } else if (partialPoints.length > 0) { + partialPoints.push(segments[i - 1].end); + points.push(partialPoints); + partialPoints = []; + } + } - // 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)); + if (partialPoints.length > 0) { + partialPoints.push(this.end); + points.push(partialPoints); + } + return points; + }, - // 4. Apply transformations. - var ctm = this.getTransformToElement(target || svg); - var transform = svg.createSVGTransform(); - transform.setMatrix( - translateFinal.matrix.multiply( - rotateAroundOrigin.matrix.multiply( - translateToOrigin.matrix.multiply( - ctm))) - ); + toPolylines: function(opt) { - // 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 polylines = []; + var points = this.toPoints(opt); + if (!points) return null; + for (var i = 0, n = points.length; i < n; i++) { + polylines.push(new Polyline(points[i])); + } - var decomposition = V.decomposeMatrix(transform.matrix); + return polylines; + }, - 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); + intersectionWithLine: function(line, opt) { - return this; - }; + var intersection = null; + var polylines = this.toPolylines(opt); + if (!polylines) return null; + for (var i = 0, n = polylines.length; i < n; i++) { + var polyline = polylines[i]; + var polylineIntersection = line.intersect(polyline); + if (polylineIntersection) { + intersection || (intersection = []); + if (Array.isArray(polylineIntersection)) { + Array.prototype.push.apply(intersection, polylineIntersection); + } else { + intersection.push(polylineIntersection); + } + } + } - V.prototype.animateAlongPath = function(attrs, path) { + return intersection; + }, - path = V.toNode(path); + // Accepts negative length. + segmentIndexAtLength: function(length, opt) { - var id = V.ensureId(path); - var animateMotion = V('animateMotion', attrs); - var mpath = V('mpath', { 'xlink:href': '#' + id }); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - animateMotion.append(mpath); + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - 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') { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) - var animation = animateMotion.node; - animation.animators = []; + var lastVisibleSegmentIndex = null; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - var animationID = animation.getAttribute('id'); - if (animationID) id2anim[animationID] = animation; + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - 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(); + if (segment.isVisible) { + if (length <= (l + d)) return i; + lastVisibleSegmentIndex = i; } + + l += d; } - } - return this; - }; - V.prototype.hasClass = function(className) { + // if length requested is higher than the length of the path, return last visible segment index + // if no visible segment, return null + return lastVisibleSegmentIndex; + }, - return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); - }; + // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + tangentAt: function(ratio, opt) { - V.prototype.addClass = function(className) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!this.hasClass(className)) { - var prevClasses = this.node.getAttribute('class') || ''; - this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); - } + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - V.prototype.removeClass = function(className) { + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - 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.tangentAtLength(length, localOpt); + }, - return this; - }; + // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + tangentAtLength: function(length, opt) { - V.prototype.toggleClass = function(className, toAdd) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - if (toRemove) { - this.removeClass(className); - } else { - this.addClass(className); - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - return this; - }; + var lastValidSegment; // visible AND differentiable (with a tangent) + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - // 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) { + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - 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; - }; + if (segment.isDifferentiable()) { + if (length <= (l + d)) { + return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } - V.prototype.convertToPath = function() { + lastValidSegment = segment; + } - var path = V('path'); - path.attr(this.attr()); - var d = this.convertToPathData(); - if (d) { - path.attr('d', d); - } - return path; - }; + l += d; + } - V.prototype.convertToPathData = function() { + // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment + if (lastValidSegment) { + var t = (fromStart ? 1 : 0); + return lastValidSegment.tangentAtT(t); + } - var tagName = this.node.tagName.toUpperCase(); + // if no valid segment, return null + return null; + }, - 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); - } + // Private function. + tangentAtT: function(t) { - throw new Error(tagName + ' cannot be converted to PATH.'); - }; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - // 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 segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].tangentAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); - var svg = this.svg().node; - target = target || svg; - var bbox = this.getBBox({ target: target }); - var center = bbox.center(); + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; + return segments[segmentIndex].tangentAtT(tValue); + }, - var spot; - var tagName = this.node.localName.toUpperCase(); + translate: function(tx, ty) { - // 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 segments = this.segments; + var numSegments = segments.length; - 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); + for (var i = 0; i < numSegments; i++) { - } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { + var segment = segments[i]; + segment.translate(tx, ty); + } - var pathNode = (tagName === 'PATH') ? this : this.convertToPath(); - var samples = pathNode.sample(); - var minDistance = Infinity; - var closestSamples = []; + return this; + }, - var i, sample, gp, centerDistance, refDistance, distance; + // Helper method for updating subpath start of segments, starting with the one provided. + updateSubpathStartSegment: function(segment) { - for (i = 0; i < samples.length; i++) { + var previousSegment = segment.previousSegment; // may be null + while (segment && !segment.isSubpathStart) { - 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; + // assign previous segment's subpath start segment to this segment + if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null + else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! - if (distance < minDistance) { - minDistance = distance; - closestSamples = [{ sample: sample, refDistance: refDistance }]; - } else if (distance < minDistance + 1) { - closestSamples.push({ sample: sample, refDistance: refDistance }); - } + previousSegment = segment; + segment = segment.nextSegment; // move on to the segment after etc. } + }, - closestSamples.sort(function(a, b) { - return a.refDistance - b.refDistance; - }); + // Returns a string that can be used to reconstruct the path. + // Additional error checking compared to toString (must start with M segment). + serialize: function() { - if (closestSamples[0]) { - spot = closestSamples[0].sample; - } - } + if (!this.isValid()) throw new Error('Invalid path segments.'); - return spot; - }; + return this.toString(); + }, - /** - * @private - * @param {string} name - * @param {string} value - * @returns {Vectorizer} - */ - V.prototype.setAttribute = function(name, value) { + toString: function() { - var el = this.node; + var segments = this.segments; + var numSegments = segments.length; - if (value === null) { - this.removeAttr(name); - return this; - } + var pathData = ''; + for (var i = 0; i < numSegments; i++) { - var qualifiedName = V.qualifyAttr(name); + var segment = segments[i]; + pathData += segment.serialize() + ' '; + } - 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 pathData.trim(); } - - return this; - }; - - // Create an SVG document element. - // If `content` is passed, it will be used as the SVG content of the `` root element. - V.createSvgDocument = function(content) { - - var svg = '' + (content || '') + ''; - var xml = V.parseXML(svg, { async: false }); - return xml.documentElement; }; - V.idCounter = 0; + Object.defineProperty(Path.prototype, 'start', { + // Getter for the first visible endpoint of the path. - // A function returning a unique identifier for this client session with every call. - V.uniqueId = function() { + configurable: true, - return 'v-' + (++V.idCounter); - }; + enumerable: true, - V.toNode = function(el) { - return V.isV(el) ? el.node : (el.nodeName && el || el[0]); - }; + get: function() { - V.ensureId = function(node) { - node = V.toNode(node); - return node.id || (node.id = V.uniqueId()); - }; - // 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. This is used in the text() method but it is - // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests - // when you want to compare the actual DOM text content without having to add the unicode character in - // the place of all spaces. - V.sanitizeText = function(text) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; - return (text || '').replace(/ /g, '\u00A0'); - }; + for (var i = 0; i < numSegments; i++) { - V.isUndefined = function(value) { + var segment = segments[i]; + if (segment.isVisible) return segment.start; + } - return typeof value === 'undefined'; - }; + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); - V.isString = function(value) { + Object.defineProperty(Path.prototype, 'end', { + // Getter for the last visible endpoint of the path. - return typeof value === 'string'; - }; + configurable: true, - V.isObject = function(value) { + enumerable: true, - return value && (typeof value === 'object'); - }; + get: function() { - V.isArray = Array.isArray; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; - V.parseXML = function(data, opt) { + for (var i = numSegments - 1; i >= 0; i--) { - opt = opt || {}; + var segment = segments[i]; + if (segment.isVisible) return segment.end; + } - var xml; + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); - try { - var parser = new DOMParser(); + /* + Point is the most basic object consisting of x/y coordinate. - if (!V.isUndefined(opt.async)) { - parser.async = opt.async; - } + Possible instantiations are: + * `Point(10, 20)` + * `new Point(10, 20)` + * `Point('10 20')` + * `Point(Point(10, 20))` + */ + var Point = g.Point = function(x, y) { - xml = parser.parseFromString(data, 'text/xml'); - } catch (error) { - xml = undefined; + if (!(this instanceof Point)) { + return new Point(x, y); } - if (!xml || xml.getElementsByTagName('parsererror').length) { - throw new Error('Invalid XML: ' + data); + if (typeof x === 'string') { + var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@'); + x = parseFloat(xy[0]); + y = parseFloat(xy[1]); + + } else if (Object(x) === x) { + y = x.y; + x = x.x; } - return xml; + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; }; - /** - * @param {string} name - * @returns {{ns: string|null, local: string}} namespace and attribute name - */ - V.qualifyAttr = function(name) { - - if (name.indexOf(':') !== -1) { - var combinedKey = name.split(':'); - return { - ns: ns[combinedKey[0]], - local: combinedKey[1] - }; - } + // Alternative constructor, from polar coordinates. + // @param {number} Distance. + // @param {number} Angle in radians. + // @param {point} [optional] Origin. + Point.fromPolar = function(distance, angle, origin) { - return { - ns: null, - local: name - }; - }; + origin = (origin && new Point(origin)) || new Point(0, 0); + var x = abs(distance * cos(angle)); + var y = abs(distance * sin(angle)); + var deg = normalizeAngle(toDeg(angle)); - V.transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi; - V.transformSeparatorRegex = /[ ,]+/; - V.transformationListRegex = /^(\w+)\((.*)\)/; + if (deg < 90) { + y = -y; - V.transformStringToMatrix = function(transform) { + } else if (deg < 180) { + x = -x; + y = -y; - var transformationMatrix = V.createSVGMatrix(); - var matches = transform && transform.match(V.transformRegex); - if (!matches) { - return transformationMatrix; + } else if (deg < 270) { + x = -x; } - for (var i = 0, n = matches.length; i < n; i++) { - var transformationString = matches[i]; - - var transformationMatch = transformationString.match(V.transformationListRegex); - if (transformationMatch) { - var sx, sy, tx, ty, angle; - var ctm = V.createSVGMatrix(); - var args = transformationMatch[2].split(V.transformSeparatorRegex); - switch (transformationMatch[1].toLowerCase()) { - case 'scale': - sx = parseFloat(args[0]); - sy = (args[1] === undefined) ? sx : parseFloat(args[1]); - ctm = ctm.scaleNonUniform(sx, sy); - break; - case 'translate': - tx = parseFloat(args[0]); - ty = parseFloat(args[1]); - ctm = ctm.translate(tx, ty); - break; - case 'rotate': - angle = parseFloat(args[0]); - tx = parseFloat(args[1]) || 0; - ty = parseFloat(args[2]) || 0; - if (tx !== 0 || ty !== 0) { - ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty); - } else { - ctm = ctm.rotate(angle); - } - break; - case 'skewx': - angle = parseFloat(args[0]); - ctm = ctm.skewX(angle); - break; - case 'skewy': - angle = parseFloat(args[0]); - ctm = ctm.skewY(angle); - break; - case 'matrix': - ctm.a = parseFloat(args[0]); - ctm.b = parseFloat(args[1]); - ctm.c = parseFloat(args[2]); - ctm.d = parseFloat(args[3]); - ctm.e = parseFloat(args[4]); - ctm.f = parseFloat(args[5]); - break; - default: - continue; - } + return new Point(origin.x + x, origin.y + y); + }; - transformationMatrix = transformationMatrix.multiply(ctm); - } + // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. + Point.random = function(x1, x2, y1, y2) { - } - return transformationMatrix; + return new Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); }; - V.matrixToTransformString = function(matrix) { - matrix || (matrix = true); + Point.prototype = { - return 'matrix(' + - (matrix.a !== undefined ? matrix.a : 1) + ',' + - (matrix.b !== undefined ? matrix.b : 0) + ',' + - (matrix.c !== undefined ? matrix.c : 0) + ',' + - (matrix.d !== undefined ? matrix.d : 1) + ',' + - (matrix.e !== undefined ? matrix.e : 0) + ',' + - (matrix.f !== undefined ? matrix.f : 0) + - ')'; - }; + // 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) { - V.parseTransformString = function(transform) { + if (r.containsPoint(this)) { + return this; + } - var translate, rotate, scale; + this.x = min(max(this.x, r.x), r.x + r.width); + this.y = min(max(this.y, r.y), r.y + r.height); + return this; + }, - if (transform) { + // Return the bearing between me and the given point. + bearing: function(point) { - var separator = V.transformSeparatorRegex; + return (new Line(this, point)).bearing(); + }, - // Allow reading transform string with a single matrix - if (transform.trim().indexOf('matrix') >= 0) { + // Returns change in angle from my previous position (-dx, -dy) to my new position + // relative to ref point. + changeInAngle: function(dx, dy, ref) { - var matrix = V.transformStringToMatrix(transform); - var decomposedMatrix = V.decomposeMatrix(matrix); + // Revert the translation and measure the change in angle around x-axis. + return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref); + }, - translate = [decomposedMatrix.translateX, decomposedMatrix.translateY]; - scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]; - rotate = [decomposedMatrix.rotation]; + clone: function() { - var transformations = []; - if (translate[0] !== 0 || translate[0] !== 0) { - transformations.push('translate(' + translate + ')'); - } - if (scale[0] !== 1 || scale[1] !== 1) { - transformations.push('scale(' + scale + ')'); - } - if (rotate[0] !== 0) { - transformations.push('rotate(' + rotate + ')'); - } - transform = transformations.join(' '); + return new Point(this); + }, - } else { + difference: function(dx, dy) { - var translateMatch = transform.match(/translate\((.*?)\)/); - if (translateMatch) { - translate = translateMatch[1].split(separator); - } - var rotateMatch = transform.match(/rotate\((.*?)\)/); - if (rotateMatch) { - rotate = rotateMatch[1].split(separator); - } - var scaleMatch = transform.match(/scale\((.*?)\)/); - if (scaleMatch) { - scale = scaleMatch[1].split(separator); - } + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; } - } - var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; + return new Point(this.x - (dx || 0), this.y - (dy || 0)); + }, - return { - value: transform, - translate: { - tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, - ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 - }, - rotate: { - angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, - cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, - cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined - }, - scale: { - sx: sx, - sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx - } - }; - }; + // Returns distance between me and point `p`. + distance: function(p) { - V.deltaTransformPoint = function(matrix, point) { + return (new Line(this, p)).length(); + }, - var dx = point.x * matrix.a + point.y * matrix.c + 0; - var dy = point.x * matrix.b + point.y * matrix.d + 0; - return { x: dx, y: dy }; - }; + squaredDistance: function(p) { - V.decomposeMatrix = function(matrix) { + return (new Line(this, p)).squaredLength(); + }, - // @see https://gist.github.com/2052247 + equals: function(p) { - // calculate delta transform point - var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); - var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); + return !!p && + this.x === p.x && + this.y === p.y; + }, - // calculate skew - var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); - var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); + magnitude: function() { - return { + return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; + }, - translateX: matrix.e, - translateY: matrix.f, - scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), - scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), - skewX: skewX, - skewY: skewY, - rotation: skewX // rotation is the same as skew x - }; - }; + // Returns a manhattan (taxi-cab) distance between me and point `p`. + manhattanDistance: function(p) { - // Return the `scale` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToScale = function(matrix) { + return abs(p.x - this.x) + abs(p.y - this.y); + }, - var a,b,c,d; - if (matrix) { - a = V.isUndefined(matrix.a) ? 1 : matrix.a; - d = V.isUndefined(matrix.d) ? 1 : matrix.d; - b = matrix.b; - c = matrix.c; - } else { - a = d = 1; - } - return { - sx: b ? Math.sqrt(a * a + b * b) : a, - sy: c ? Math.sqrt(c * c + d * d) : d - }; - }, + // Move point on line starting from ref ending at me by + // distance distance. + move: function(ref, distance) { - // Return the `rotate` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToRotate = function(matrix) { + var theta = toRad((new Point(ref)).theta(this)); + var offset = this.offset(cos(theta) * distance, -sin(theta) * distance); + return offset; + }, - var p = { x: 0, y: 1 }; - if (matrix) { - p = V.deltaTransformPoint(matrix, p); - } + // 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) { - return { - angle: g.normalizeAngle(g.toDeg(Math.atan2(p.y, p.x)) - 90) - }; - }, + var scale = (length || 1) / this.magnitude(); + return this.scale(scale, scale); + }, - // Return the `translate` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToTranslate = function(matrix) { + // Offset me by the specified amount. + offset: function(dx, dy) { - return { - tx: (matrix && matrix.e) || 0, - ty: (matrix && matrix.f) || 0 - }; - }, + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } - V.isV = function(object) { + this.x += dx || 0; + this.y += dy || 0; + return this; + }, - return object instanceof V; - }; + // Returns a point that is the reflection of me with + // the center of inversion in ref point. + reflection: function(ref) { - // For backwards compatibility: - V.isVElement = V.isV; + return (new Point(ref)).move(this, this.distance(ref)); + }, - var svgDocument = V('svg').node; + // Rotate point by angle around origin. + // Angle is flipped because this is a left-handed coord system (y-axis points downward). + rotate: function(origin, angle) { - V.createSVGMatrix = function(matrix) { + origin = origin || new g.Point(0, 0); - var svgMatrix = svgDocument.createSVGMatrix(); - for (var component in matrix) { - svgMatrix[component] = matrix[component]; - } + angle = toRad(normalizeAngle(-angle)); + var cosAngle = cos(angle); + var sinAngle = sin(angle); - return svgMatrix; - }; + var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x; + var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y; - V.createSVGTransform = function(matrix) { + this.x = x; + this.y = y; + return this; + }, - if (!V.isUndefined(matrix)) { + round: function(precision) { - if (!(matrix instanceof SVGMatrix)) { - matrix = V.createSVGMatrix(matrix); - } + var f = pow(10, precision || 0); + this.x = round(this.x * f) / f; + this.y = round(this.y * f) / f; + return this; + }, - return svgDocument.createSVGTransformFromMatrix(matrix); - } + // Scale point with origin. + scale: function(sx, sy, origin) { - return svgDocument.createSVGTransform(); - }; + origin = (origin && new Point(origin)) || new Point(0, 0); + this.x = origin.x + sx * (this.x - origin.x); + this.y = origin.y + sy * (this.y - origin.y); + return this; + }, - V.createSVGPoint = function(x, y) { + snapToGrid: function(gx, gy) { - var p = svgDocument.createSVGPoint(); - p.x = x; - p.y = y; - return p; - }; + this.x = snapToGrid(this.x, gx); + this.y = snapToGrid(this.y, gy || gx); + return this; + }, - V.transformRect = function(r, matrix) { + // Compute the angle between me and `p` and the x axis. + // (cartesian-to-polar coordinates conversion) + // Return theta angle in degrees. + theta: function(p) { - var p = svgDocument.createSVGPoint(); + p = new Point(p); - p.x = r.x; - p.y = r.y; - var corner1 = p.matrixTransform(matrix); + // Invert the y-axis. + var y = -(p.y - this.y); + var x = p.x - this.x; + var rad = atan2(y, x); // defined for all 0 corner cases - p.x = r.x + r.width; - p.y = r.y; - var corner2 = p.matrixTransform(matrix); + // Correction for III. and IV. quadrant. + if (rad < 0) { + rad = 2 * PI + rad; + } - p.x = r.x + r.width; - p.y = r.y + r.height; - var corner3 = p.matrixTransform(matrix); + return 180 * rad / PI; + }, - p.x = r.x; - p.y = r.y + r.height; - var corner4 = p.matrixTransform(matrix); + // Compute the angle between vector from me to p1 and the vector from me to p2. + // ordering of points p1 and p2 is important! + // theta function's angle convention: + // returns angles between 0 and 180 when the angle is counterclockwise + // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones + // returns NaN if any of the points p1, p2 is coincident with this point + angleBetween: function(p1, p2) { - var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x); - var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x); - var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y); - var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y); + var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - return g.Rect(minX, minY, maxX - minX, maxY - minY); - }; + if (angleBetween < 0) { + angleBetween += 360; // correction to keep angleBetween between 0 and 360 + } - V.transformPoint = function(p, matrix) { + return angleBetween; + }, - return g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); - }; + // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. + // Returns NaN if p is at 0,0. + vectorAngle: function(p) { - // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to - // an object (`{ fill: 'blue', stroke: 'red' }`). - V.styleToObject = function(styleString) { - var ret = {}; - var styles = styleString.split(';'); - for (var i = 0; i < styles.length; i++) { - var style = styles[i]; - var pair = style.split('='); - ret[pair[0].trim()] = pair[1].trim(); - } - return ret; - }; + var zero = new Point(0,0); + return zero.angleBetween(this, p); + }, - // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js - V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { + toJSON: function() { - var svgArcMax = 2 * Math.PI - 1e-6; - var r0 = innerRadius; - var r1 = outerRadius; - var a0 = startAngle; - var a1 = endAngle; - var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); - var df = da < Math.PI ? '0' : '1'; - var c0 = Math.cos(a0); - var s0 = Math.sin(a0); - var c1 = Math.cos(a1); - var s1 = Math.sin(a1); - - return (da >= svgArcMax) - ? (r0 - ? 'M0,' + r1 - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 - + 'M0,' + r0 - + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) - + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 - + 'Z' - : 'M0,' + r1 - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 - + 'Z') - : (r0 - ? 'M' + r1 * c0 + ',' + r1 * s0 - + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 - + 'L' + r0 * c1 + ',' + r0 * s1 - + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 - + 'Z' - : 'M' + r1 * c0 + ',' + r1 * s0 - + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 - + 'L0,0' - + 'Z'); - }; - - // Merge attributes from object `b` with attributes in object `a`. - // Note that this modifies the object `a`. - // Also important to note that attributes are merged but CSS classes are concatenated. - V.mergeAttrs = function(a, b) { - - for (var attr in b) { - - if (attr === 'class') { - // Concatenate classes. - a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; - } else if (attr === 'style') { - // `style` attribute can be an object. - if (V.isObject(a[attr]) && V.isObject(b[attr])) { - // `style` stored in `a` is an object. - a[attr] = V.mergeAttrs(a[attr], b[attr]); - } else if (V.isObject(a[attr])) { - // `style` in `a` is an object but it's a string in `b`. - // Convert the style represented as a string to an object in `b`. - a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); - } else if (V.isObject(b[attr])) { - // `style` in `a` is a string, in `b` it's an object. - a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); - } else { - // Both styles are strings. - a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); - } - } else { - a[attr] = b[attr]; - } - } + return { x: this.x, y: this.y }; + }, - return a; - }; + // Converts rectangular to polar coordinates. + // An origin can be specified, otherwise it's 0@0. + toPolar: function(o) { - V.annotateString = function(t, annotations, opt) { + o = (o && new Point(o)) || new 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(new Point(x, y))); + return this; + }, - annotations = annotations || []; - opt = opt || {}; + toString: function() { - var offset = opt.offset || 0; - var compacted = []; - var batch; - var ret = []; - var item; - var prev; + return this.x + '@' + this.y; + }, - for (var i = 0; i < t.length; i++) { + update: function(x, y) { - item = ret[i] = t[i]; + this.x = x || 0; + this.y = y || 0; + return this; + }, - for (var j = 0; j < annotations.length; j++) { + // Returns the dot product of this point with given other point + dot: function(p) { - var annotation = annotations[j]; - var start = annotation.start + offset; - var end = annotation.end + offset; + return p ? (this.x * p.x + this.y * p.y) : NaN; + }, - if (i >= start && i < end) { - // Annotation applies. - if (V.isObject(item)) { - // There is more than one annotation to be applied => Merge attributes. - item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); - } else { - item = ret[i] = { t: t[i], attrs: annotation.attrs }; - } - if (opt.includeAnnotationIndices) { - (item.annotations || (item.annotations = [])).push(j); - } - } - } + // Returns the cross product of this point relative to two other points + // this point is the common point + // point p1 lies on the first vector, point p2 lies on the second vector + // watch out for the ordering of points p1 and p2! + // positive result indicates a clockwise ("right") turn from first to second vector + // negative result indicates a counterclockwise ("left") turn from first to second vector + // note that the above directions are reversed from the usual answer on the Internet + // that is because we are in a left-handed coord system (because the y-axis points downward) + cross: function(p1, p2) { - prev = ret[i - 1]; + return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; + }, - if (!prev) { - batch = item; + // Linear interpolation + lerp: function(p, t) { - } else if (V.isObject(item) && V.isObject(prev)) { - // Both previous item and the current one are annotations. If the attributes - // didn't change, merge the text. - if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { - batch.t += item.t; - } else { - compacted.push(batch); - batch = item; - } + var x = this.x; + var y = this.y; + return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y); + } + }; - } else if (V.isObject(item)) { - // Previous item was a string, current item is an annotation. - compacted.push(batch); - batch = item; + Point.prototype.translate = Point.prototype.offset; - } else if (V.isObject(prev)) { - // Previous item was an annotation, current item is a string. - compacted.push(batch); - batch = item; + var Rect = g.Rect = function(x, y, w, h) { - } else { - // Both previous and current item are strings. - batch = (batch || '') + item; - } + if (!(this instanceof Rect)) { + return new Rect(x, y, w, h); } - if (batch) { - compacted.push(batch); + if ((Object(x) === x)) { + y = x.y; + w = x.width; + h = x.height; + x = x.x; } - return compacted; + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; + this.width = w === undefined ? 0 : w; + this.height = h === undefined ? 0 : h; }; - V.findAnnotationsAtIndex = function(annotations, index) { - - var found = []; - - if (annotations) { - - annotations.forEach(function(annotation) { - - if (annotation.start < index && index <= annotation.end) { - found.push(annotation); - } - }); - } + Rect.fromEllipse = function(e) { - return found; + e = new Ellipse(e); + return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); }; - V.findAnnotationsBetweenIndexes = function(annotations, start, end) { + Rect.prototype = { - var found = []; + // 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) { - if (annotations) { + if (!angle) return this.clone(); - annotations.forEach(function(annotation) { + 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 new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); + }, - if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { - found.push(annotation); - } - }); - } + bottomLeft: function() { - return found; - }; + return new Point(this.x, this.y + this.height); + }, - // Shift all the text annotations after character `index` by `offset` positions. - V.shiftAnnotations = function(annotations, index, offset) { + bottomLine: function() { - if (annotations) { + return new Line(this.bottomLeft(), this.bottomRight()); + }, - annotations.forEach(function(annotation) { + bottomMiddle: function() { - if (annotation.start < index && annotation.end >= index) { - annotation.end += offset; - } else if (annotation.start >= index) { - annotation.start += offset; - annotation.end += offset; - } - }); - } + return new Point(this.x + this.width / 2, this.y + this.height); + }, - return annotations; - }; + center: function() { - V.convertLineToPathData = function(line) { + return new Point(this.x + this.width / 2, this.y + this.height / 2); + }, - line = V(line); - var d = [ - 'M', line.attr('x1'), line.attr('y1'), - 'L', line.attr('x2'), line.attr('y2') - ].join(' '); - return d; - }; + clone: function() { - V.convertPolygonToPathData = function(polygon) { + return new Rect(this); + }, - var points = V.getPointsFromSvgNode(V(polygon).node); + // @return {bool} true if point p is insight me + containsPoint: function(p) { - if (!(points.length > 0)) return null; + p = new Point(p); + return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + }, - return V.svgPointsToPath(points) + ' Z'; - }; + // @return {bool} true if rectangle `r` is inside me. + containsRect: function(r) { - V.convertPolylineToPathData = function(polyline) { + var r0 = new Rect(this).normalize(); + var r1 = new Rect(r).normalize(); + var w0 = r0.width; + var h0 = r0.height; + var w1 = r1.width; + var h1 = r1.height; - var points = V.getPointsFromSvgNode(V(polyline).node); + if (!w0 || !h0 || !w1 || !h1) { + // At least one of the dimensions is 0 + return false; + } - if (!(points.length > 0)) return null; + var x0 = r0.x; + var y0 = r0.y; + var x1 = r1.x; + var y1 = r1.y; - return V.svgPointsToPath(points); - }; - - V.svgPointsToPath = function(points) { + w1 += x1; + w0 += x0; + h1 += y1; + h0 += y0; - var i; + return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; + }, - for (i = 0; i < points.length; i++) { - points[i] = points[i].x + ' ' + points[i].y; - } + corner: function() { - return 'M ' + points.join(' L'); - }; + return new Point(this.x + this.width, this.y + this.height); + }, - V.getPointsFromSvgNode = function(node) { + // @return {boolean} true if rectangles are equal. + equals: function(r) { - node = V.toNode(node); - var points = []; - var nodePoints = node.points; - if (nodePoints) { - for (var i = 0; i < nodePoints.numberOfItems; i++) { - points.push(nodePoints.getItem(i)); - } - } + var mr = (new Rect(this)).normalize(); + var nr = (new Rect(r)).normalize(); + return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height; + }, - return points; - }; + // @return {rect} if rectangles intersect, {null} if not. + intersect: function(r) { - V.KAPPA = 0.5522847498307935; + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = r.origin(); + var rCorner = r.corner(); - V.convertCircleToPathData = function(circle) { + // No intersection found + if (rCorner.x <= myOrigin.x || + rCorner.y <= myOrigin.y || + rOrigin.x >= myCorner.x || + rOrigin.y >= myCorner.y) return null; - circle = V(circle); - var cx = parseFloat(circle.attr('cx')) || 0; - var cy = parseFloat(circle.attr('cy')) || 0; - var r = parseFloat(circle.attr('r')); - var cd = r * V.KAPPA; // Control distance. + var x = max(myOrigin.x, rOrigin.x); + var y = max(myOrigin.y, rOrigin.y); - var d = [ - 'M', cx, cy - r, // Move to the first point. - 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. - 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. - 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. - 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. - 'Z' - ].join(' '); - return d; - }; + return new Rect(x, y, min(myCorner.x, rCorner.x) - x, min(myCorner.y, rCorner.y) - y); + }, - V.convertEllipseToPathData = function(ellipse) { + intersectionWithLine: function(line) { - ellipse = V(ellipse); - var cx = parseFloat(ellipse.attr('cx')) || 0; - var cy = parseFloat(ellipse.attr('cy')) || 0; - var rx = parseFloat(ellipse.attr('rx')); - var ry = parseFloat(ellipse.attr('ry')) || rx; - var cdx = rx * V.KAPPA; // Control distance x. - var cdy = ry * V.KAPPA; // Control distance y. + var r = this; + var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; + var points = []; + var dedupeArr = []; + var pt, i; - var d = [ - 'M', cx, cy - ry, // Move to the first point. - 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. - 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. - 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. - 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. - 'Z' - ].join(' '); - return d; - }; + var n = rectLines.length; + for (i = 0; i < n; i ++) { - V.convertRectToPathData = function(rect) { + pt = line.intersect(rectLines[i]); + if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) { + points.push(pt); + dedupeArr.push(pt.toString()); + } + } - rect = V(rect); + return points.length > 0 ? points : null; + }, - return V.rectToPath({ - x: parseFloat(rect.attr('x')) || 0, - y: parseFloat(rect.attr('y')) || 0, - width: parseFloat(rect.attr('width')) || 0, - height: parseFloat(rect.attr('height')) || 0, - rx: parseFloat(rect.attr('rx')) || 0, - ry: parseFloat(rect.attr('ry')) || 0 - }); - }; + // 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) { - // Convert a rectangle to SVG path commands. `r` is an object of the form: - // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, - // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for - // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle - // that has only `rx` and `ry` attributes). - V.rectToPath = function(r) { + p = new Point(p); + var center = new Point(this.x + this.width / 2, this.y + this.height / 2); + var result; - var d; - var x = r.x; - var y = r.y; - var width = r.width; - var height = r.height; - var topRx = Math.min(r.rx || r['top-rx'] || 0, width / 2); - var bottomRx = Math.min(r.rx || r['bottom-rx'] || 0, width / 2); - var topRy = Math.min(r.ry || r['top-ry'] || 0, height / 2); - var bottomRy = Math.min(r.ry || r['bottom-ry'] || 0, height / 2); + if (angle) p.rotate(center, angle); - if (topRx || bottomRx || topRy || bottomRy) { - d = [ - 'M', x, y + topRy, - 'v', height - topRy - bottomRy, - 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, - 'h', width - 2 * bottomRx, - 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, - 'v', -(height - bottomRy - topRy), - 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, - 'h', -(width - 2 * topRx), - 'a', topRx, topRy, 0, 0, 0, -topRx, topRy, - 'Z' - ]; - } else { - d = [ - 'M', x, y, - 'H', x + width, - 'V', y + height, - 'H', x, - 'V', y, - 'Z' + // (clockwise, starting from the top side) + var sides = [ + this.topLine(), + this.rightLine(), + this.bottomLine(), + this.leftLine() ]; - } + var connector = new Line(center, p); - return d.join(' '); - }; + 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; + }, - V.namespace = ns; + leftLine: function() { - return V; + return new Line(this.topLeft(), this.bottomLeft()); + }, -})(); + leftMiddle: function() { + return new Point(this.x , this.y + this.height / 2); + }, -// Global namespace. + // Move and expand me. + // @param r {rectangle} representing deltas + moveAndExpand: function(r) { -var joint = { + this.x += r.x || 0; + this.y += r.y || 0; + this.width += r.width || 0; + this.height += r.height || 0; + return this; + }, - version: '2.0.1', + // Offset me by the specified amount. + offset: function(dx, dy) { - config: { - // The class name prefix config is for advanced use only. - // Be aware that if you change the prefix, the JointJS CSS will no longer function properly. - classNamePrefix: 'joint-', - defaultTheme: 'default' - }, + // pretend that this is a point and call offset() + // rewrites x and y according to dx and dy + return Point.prototype.offset.call(this, dx, dy); + }, - // `joint.dia` namespace. - dia: {}, + // 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) { - // `joint.ui` namespace. - ui: {}, + if (dx === undefined) { + dx = 0; + } - // `joint.layout` namespace. - layout: {}, + if (dy === undefined) { + dy = dx; + } - // `joint.shapes` namespace. - shapes: {}, + this.x -= dx; + this.y -= dy; + this.width += 2 * dx; + this.height += 2 * dy; - // `joint.format` namespace. - format: {}, + return this; + }, - // `joint.connectors` namespace. - connectors: {}, + // 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() { - // `joint.highlighters` namespace. - highlighters: {}, + 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; + }, - // `joint.routers` namespace. - routers: {}, + origin: function() { - // `joint.mvc` namespace. - mvc: { - views: {} - }, + return new Point(this.x, this.y); + }, - setTheme: function(theme, opt) { + // @return {point} a point on my boundary nearest to the given point. + // @see Squeak Smalltalk, Rectangle>>pointNearestTo: + pointNearestToPoint: function(point) { - opt = opt || {}; + point = new Point(point); + if (this.containsPoint(point)) { + var side = this.sideNearestToPoint(point); + switch (side){ + case 'right': return new Point(this.x + this.width, point.y); + case 'left': return new Point(this.x, point.y); + case 'bottom': return new Point(point.x, this.y + this.height); + case 'top': return new Point(point.x, this.y); + } + } + return point.adhereToRect(this); + }, - joint.util.invoke(joint.mvc.views, 'setTheme', theme, opt); + rightLine: function() { - // Update the default theme on the view prototype. - joint.mvc.View.prototype.defaultTheme = theme; - }, + return new Line(this.topRight(), this.bottomRight()); + }, - // `joint.env` namespace. - env: { + rightMiddle: function() { - _results: {}, + return new Point(this.x + this.width, this.y + this.height / 2); + }, - _tests: { + round: function(precision) { - svgforeignobject: function() { - return !!document.createElementNS && - /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); - } + 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; }, - addTest: function(name, fn) { + // Scale rectangle with origin. + scale: function(sx, sy, origin) { - return joint.env._tests[name] = fn; + origin = this.origin().scale(sx, sy, origin); + this.x = origin.x; + this.y = origin.y; + this.width *= sx; + this.height *= sy; + return this; }, - test: function(name) { + maxRectScaleToFit: function(rect, origin) { - var fn = joint.env._tests[name]; + rect = new Rect(rect); + origin || (origin = rect.center()); - if (!fn) { - throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); - } + var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; + var ox = origin.x; + var oy = origin.y; - var result = joint.env._results[name]; + // 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. - if (typeof result !== 'undefined') { - return result; - } + sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; - try { - result = fn(); - } catch (error) { - result = false; + // Top Left + var p1 = rect.topLeft(); + 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.bottomRight(); + 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); } - // Cache the test result. - joint.env._results[name] = result; + return { + sx: min(sx1, sx2, sx3, sx4), + sy: min(sy1, sy2, sy3, sy4) + }; + }, - return result; - } - }, + maxRectUniformScaleToFit: function(rect, origin) { - util: { + var scale = this.maxRectScaleToFit(rect, origin); + return min(scale.sx, scale.sy); + }, - // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. - hashCode: function(str) { + // @return {string} (left|right|top|bottom) side which is nearest to point + // @see Squeak Smalltalk, Rectangle>>sideNearestTo: + sideNearestToPoint: function(point) { - var hash = 0; - if (str.length == 0) return hash; - for (var i = 0; i < str.length; i++) { - var c = str.charCodeAt(i); - hash = ((hash << 5) - hash) + c; - hash = hash & hash; // Convert to 32bit integer + point = new 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'; } - return hash; + if (distToTop < closest) { + closest = distToTop; + side = 'top'; + } + if (distToBottom < closest) { + closest = distToBottom; + side = 'bottom'; + } + return side; }, - getByPath: function(obj, path, delim) { + snapToGrid: function(gx, gy) { - var keys = Array.isArray(path) ? path.slice() : path.split(delim || '/'); - var key; + 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; + }, - while (keys.length) { - key = keys.shift(); - if (Object(obj) === obj && key in obj) { - obj = obj[key]; - } else { - return undefined; - } - } - return obj; + topLine: function() { + + return new Line(this.topLeft(), this.topRight()); }, - setByPath: function(obj, path, value, delim) { + topMiddle: function() { - var keys = Array.isArray(path) ? path : path.split(delim || '/'); + return new Point(this.x + this.width / 2, this.y); + }, - var diver = obj; - var i = 0; + topRight: function() { - for (var len = keys.length; i < len - 1; i++) { - // diver creates an empty object if there is no nested object under such a key. - // This means that one can populate an empty nested object with setByPath(). - diver = diver[keys[i]] || (diver[keys[i]] = {}); - } - diver[keys[len - 1]] = value; + return new Point(this.x + this.width, this.y); + }, - return obj; + toJSON: function() { + + return { x: this.x, y: this.y, width: this.width, height: this.height }; }, - unsetByPath: function(obj, path, delim) { + toString: function() { - delim = delim || '/'; + return this.origin().toString() + ' ' + this.corner().toString(); + }, - var pathArray = Array.isArray(path) ? path.slice() : path.split(delim); + // @return {rect} representing the union of both rectangles. + union: function(rect) { - var propertyToRemove = pathArray.pop(); - if (pathArray.length > 0) { + rect = new Rect(rect); + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = rect.origin(); + var rCorner = rect.corner(); - // unsetting a nested attribute - var parent = joint.util.getByPath(obj, pathArray, delim); + var originX = min(myOrigin.x, rOrigin.x); + var originY = min(myOrigin.y, rOrigin.y); + var cornerX = max(myCorner.x, rCorner.x); + var cornerY = max(myCorner.y, rCorner.y); - if (parent) { - delete parent[propertyToRemove]; - } + return new Rect(originX, originY, cornerX - originX, cornerY - originY); + } + }; - } else { + Rect.prototype.bottomRight = Rect.prototype.corner; - // unsetting a primitive attribute - delete obj[propertyToRemove]; - } + Rect.prototype.topLeft = Rect.prototype.origin; - return obj; - }, + Rect.prototype.translate = Rect.prototype.offset; - flattenObject: function(obj, delim, stop) { + var Polyline = g.Polyline = function(points) { - delim = delim || '/'; - var ret = {}; + if (!(this instanceof Polyline)) { + return new Polyline(points); + } - for (var key in obj) { + if (typeof points === 'string') { + return new Polyline.parse(points); + } - if (!obj.hasOwnProperty(key)) continue; + this.points = (Array.isArray(points) ? points.map(Point) : []); + }; - var shouldGoDeeper = typeof obj[key] === 'object'; - if (shouldGoDeeper && stop && stop(obj[key])) { - shouldGoDeeper = false; - } + Polyline.parse = function(svgString) { - if (shouldGoDeeper) { + if (svgString === '') return new Polyline(); - var flatObject = this.flattenObject(obj[key], delim, stop); + var points = []; - for (var flatKey in flatObject) { - if (!flatObject.hasOwnProperty(flatKey)) continue; - ret[key + delim + flatKey] = flatObject[flatKey]; - } + var coords = svgString.split(/\s|,/); + var n = coords.length; + for (var i = 0; i < n; i += 2) { + points.push({ x: +coords[i], y: +coords[i + 1] }); + } - } else { + return new Polyline(points); + }; - ret[key] = obj[key]; - } + Polyline.prototype = { + + bbox: function() { + + var x1 = Infinity; + var x2 = -Infinity; + var y1 = Infinity; + var y2 = -Infinity; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + var x = point.x; + var y = point.y; + + if (x < x1) x1 = x; + if (x > x2) x2 = x; + if (y < y1) y1 = y; + if (y > y2) y2 = y; } - return ret; + return new Rect(x1, y1, x2 - x1, y2 - y1); }, - uuid: function() { + clone: function() { - // credit: http://stackoverflow.com/posts/2117523/revisions + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random() * 16|0; - var v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }, + var newPoints = []; + for (var i = 0; i < numPoints; i++) { - // Generate global unique id for obj and store it as a property of the object. - guid: function(obj) { + var point = points[i].clone(); + newPoints.push(point); + } - this.guid.id = this.guid.id || 1; - obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); - return obj.id; + return new Polyline(newPoints); }, - toKebabCase: function(string) { + closestPoint: function(p) { - return string.replace(/[A-Z]/g, '-$&').toLowerCase(); + var cpLength = this.closestPointLength(p); + + return this.pointAtLength(cpLength); }, - // Copy all the properties to the first argument from the following arguments. - // All the properties will be overwritten by the properties from the following - // arguments. Inherited properties are ignored. - mixin: _.assign, + closestPointLength: function(p) { - // Copy all properties to the first argument from the following - // arguments only in case if they don't exists in the first argument. - // All the function propererties in the first argument will get - // additional property base pointing to the extenders same named - // property function's call method. - supplement: _.defaults, + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + if (numPoints === 1) return 0; // if there is only one point - // Same as `mixin()` but deep version. - deepMixin: _.mixin, + var cpLength; + var minSqrDistance = Infinity; + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { - // Same as `supplement()` but deep version. - deepSupplement: _.defaultsDeep, + var line = new Line(points[i], points[i + 1]); + var lineLength = line.length(); - normalizeEvent: function(evt) { + var cpNormalizedLength = line.closestPointNormalizedLength(p); + var cp = line.pointAt(cpNormalizedLength); - var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; - if (touchEvt) { - for (var property in evt) { - // copy all the properties from the input event that are not - // defined on the touch event (functions included). - if (touchEvt[property] === undefined) { - touchEvt[property] = evt[property]; - } + var sqrDistance = cp.squaredDistance(p); + if (sqrDistance < minSqrDistance) { + minSqrDistance = sqrDistance; + cpLength = length + (cpNormalizedLength * lineLength); } - return touchEvt; + + length += lineLength; } - return evt; + return cpLength; }, - nextFrame: (function() { + closestPointNormalizedLength: function(p) { - var raf; + var cpLength = this.closestPointLength(p); + if (cpLength === 0) return 0; // shortcut - if (typeof window !== 'undefined') { + var length = this.length(); + if (length === 0) return 0; // prevents division by zero - raf = window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame; - } + return cpLength / length; + }, - if (!raf) { + closestPointTangent: function(p) { - var lastTime = 0; + var cpLength = this.closestPointLength(p); - raf = function(callback) { + return this.tangentAtLength(cpLength); + }, - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + // Returns a convex-hull polyline from this polyline. + // Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan). + // Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise. + // Minimal polyline is found (only vertices of the hull are reported, no collinear points). + convexHull: function() { - lastTime = currTime + timeToCall; + var i; + var n; - return id; - }; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + // step 1: find the starting point - point with the lowest y (if equality, highest x) + var startPoint; + for (i = 0; i < numPoints; i++) { + if (startPoint === undefined) { + // if this is the first point we see, set it as start point + startPoint = points[i]; + + } else if (points[i].y < startPoint.y) { + // start point should have lowest y from all points + startPoint = points[i]; + + } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { + // if two points have the lowest y, choose the one that has highest x + // there are no points to the right of startPoint - no ambiguity about theta 0 + // if there are several coincident start point candidates, first one is reported + startPoint = points[i]; + } } - return function(callback, context) { - return context - ? raf(callback.bind(context)) - : raf(callback); - }; + // step 2: sort the list of points + // sorting by angle between line from startPoint to point and the x-axis (theta) - })(), + // step 2a: create the point records = [point, originalIndex, angle] + var sortedPointRecords = []; + for (i = 0; i < numPoints; i++) { - cancelFrame: (function() { + var angle = startPoint.theta(points[i]); + if (angle === 0) { + angle = 360; // give highest angle to start point + // the start point will end up at end of sorted list + // the start point will end up at beginning of hull points list + } - var caf; - var client = typeof window != 'undefined'; + var entry = [points[i], i, angle]; + sortedPointRecords.push(entry); + } - if (client) { + // step 2b: sort the list in place + sortedPointRecords.sort(function(record1, record2) { + // returning a negative number here sorts record1 before record2 + // if first angle is smaller than second, first angle should come before second - caf = window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.webkitCancelRequestAnimationFrame || - window.msCancelAnimationFrame || - window.msCancelRequestAnimationFrame || - window.oCancelAnimationFrame || - window.oCancelRequestAnimationFrame || - window.mozCancelAnimationFrame || - window.mozCancelRequestAnimationFrame; + var sortOutput = record1[2] - record2[2]; // negative if first angle smaller + if (sortOutput === 0) { + // if the two angles are equal, sort by originalIndex + sortOutput = record2[1] - record1[1]; // negative if first index larger + // coincident points will be sorted in reverse-numerical order + // so the coincident points with lower original index will be considered first + } + + return sortOutput; + }); + + // step 2c: duplicate start record from the top of the stack to the bottom of the stack + if (sortedPointRecords.length > 2) { + var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; + sortedPointRecords.unshift(startPointRecord); } - caf = caf || clearTimeout; + // step 3a: go through sorted points in order and find those with right turns + // we want to get our results in clockwise order + var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull + var hullPointRecords = []; // stack of records with right turns - hull point candidates - return client ? caf.bind(window) : caf; + var currentPointRecord; + var currentPoint; + var lastHullPointRecord; + var lastHullPoint; + var secondLastHullPointRecord; + var secondLastHullPoint; + while (sortedPointRecords.length !== 0) { - })(), + currentPointRecord = sortedPointRecords.pop(); + currentPoint = currentPointRecord[0]; - shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { + // check if point has already been discarded + // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' + if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { + // this point had an incorrect turn at some previous iteration of this loop + // this disqualifies it from possibly being on the hull + continue; + } - var bbox; - var spot; + var correctTurnFound = false; + while (!correctTurnFound) { - if (!magnet) { + if (hullPointRecords.length < 2) { + // not enough points for comparison, just add current point + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; - // There is no magnet, try to make the best guess what is the - // wrapping SVG element. This is because we want this "smart" - // connection points to work out of the box without the - // programmer to put magnet marks to any of the subelements. - // For example, we want the functoin to work on basic.Path elements - // without any special treatment of such elements. - // The code below guesses the wrapping element based on - // one simple assumption. The wrapping elemnet is the - // first child of the scalable group if such a group exists - // or the first child of the rotatable group if not. - // This makese sense because usually the wrapping element - // is below any other sub element in the shapes. - var scalable = view.$('.scalable')[0]; - var rotatable = view.$('.rotatable')[0]; + } else { + lastHullPointRecord = hullPointRecords.pop(); + lastHullPoint = lastHullPointRecord[0]; + secondLastHullPointRecord = hullPointRecords.pop(); + secondLastHullPoint = secondLastHullPointRecord[0]; - if (scalable && scalable.firstChild) { + var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); - magnet = scalable.firstChild; + if (crossProduct < 0) { + // found a right turn + hullPointRecords.push(secondLastHullPointRecord); + hullPointRecords.push(lastHullPointRecord); + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; - } else if (rotatable && rotatable.firstChild) { + } else if (crossProduct === 0) { + // the three points are collinear + // three options: + // there may be a 180 or 0 degree angle at lastHullPoint + // or two of the three points are coincident + var THRESHOLD = 1e-10; // we have to take rounding errors into account + var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); + if (abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 + // if the cross product is 0 because the angle is 180 degrees + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found - magnet = rotatable.firstChild; - } - } + } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { + // if the cross product is 0 because two points are the same + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found - if (magnet) { + } else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 + // if the cross product is 0 because the angle is 0 degrees + // remove last hull point from hull BUT do not discard it + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // put last hull point back into the sorted point records list + sortedPointRecords.push(lastHullPointRecord); + // we are switching the order of the 0deg and 180deg points + // correct turn not found + } - spot = V(magnet).findIntersection(reference, linkView.paper.viewport); - if (!spot) { - bbox = V(magnet).getBBox({ target: linkView.paper.viewport }); + } else { + // found a left turn + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter of loop) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + } + } } + } + // at this point, hullPointRecords contains the output points in clockwise order + // the points start with lowest-y,highest-x startPoint, and end at the same point - } else { - - bbox = view.model.getBBox(); - spot = bbox.intersectionWithLineFromCenterToPoint(reference); + // step 3b: remove duplicated startPointRecord from the end of the array + if (hullPointRecords.length > 2) { + hullPointRecords.pop(); } - return spot || bbox.center(); - }, - parseCssNumeric: function(strValue, restrictUnits) { + // step 4: find the lowest originalIndex record and put it at the beginning of hull + var lowestHullIndex; // the lowest originalIndex on the hull + var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex + n = hullPointRecords.length; + for (i = 0; i < n; i++) { - restrictUnits = restrictUnits || []; - var cssNumeric = { value: parseFloat(strValue) }; + var currentHullIndex = hullPointRecords[i][1]; - if (Number.isNaN(cssNumeric.value)) { - return null; + if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { + lowestHullIndex = currentHullIndex; + indexOfLowestHullIndexRecord = i; + } } - var validUnitsExp = restrictUnits.join('|'); + var hullPointRecordsReordered = []; + if (indexOfLowestHullIndexRecord > 0) { + var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); + var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); + hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - if (joint.util.isString(strValue)) { - var matches = new RegExp('(\\d+)(' + validUnitsExp + ')$').exec(strValue); - if (!matches) { - return null; - } - if (matches[2]) { - cssNumeric.unit = matches[2]; - } + } else { + hullPointRecordsReordered = hullPointRecords; } - return cssNumeric; - }, - breakText: function(text, size, styles, opt) { + var hullPoints = []; + n = hullPointRecordsReordered.length; + for (i = 0; i < n; i++) { + hullPoints.push(hullPointRecordsReordered[i][0]); + } - opt = opt || {}; + return new Polyline(hullPoints); + }, - var width = size.width; - var height = size.height; + // Checks whether two polylines are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { - var svgDocument = opt.svgDocument || V('svg').node; - var textElement = V('').attr(styles || {}).node; - var textSpan = textElement.firstChild; - var textNode = document.createTextNode(''); + if (!p) return false; - // Prevent flickering - textElement.style.opacity = 0; - // Prevent FF from throwing an uncaught exception when `getBBox()` - // called on element that is not in the render tree (is not measurable). - // .getComputedTextLength() returns always 0 in this case. - // Note that the `textElement` resp. `textSpan` can become hidden - // when it's appended to the DOM and a `display: none` CSS stylesheet - // rule gets applied. - textElement.style.display = 'block'; - textSpan.style.display = 'block'; + var points = this.points; + var otherPoints = p.points; - textSpan.appendChild(textNode); - svgDocument.appendChild(textElement); + var numPoints = points.length; + if (otherPoints.length !== numPoints) return false; // if the two polylines have different number of points, they cannot be equal - if (!opt.svgDocument) { + for (var i = 0; i < numPoints; i++) { - document.body.appendChild(svgDocument); + var point = points[i]; + var otherPoint = p.points[i]; + + // as soon as an inequality is found in points, return false + if (!point.equals(otherPoint)) return false; } - var words = text.split(' '); - var full = []; - var lines = []; - var p; - var lineHeight; + // if no inequality found in points, return true + return true; + }, - for (var i = 0, l = 0, len = words.length; i < len; i++) { + isDifferentiable: function() { - var word = words[i]; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return false; - textNode.data = lines[l] ? lines[l] + ' ' + word : word; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { - if (textSpan.getComputedTextLength() <= width) { + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); - // the current line fits - lines[l] = textNode.data; + // as soon as a differentiable line is found between two points, return true + if (line.isDifferentiable()) return true; + } - if (p) { - // We were partitioning. Put rest of the word onto next line - full[l++] = true; + // if no differentiable line is found between pairs of points, return false + return false; + }, - // cancel partitioning - p = 0; - } + length: function() { - } else { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty - if (!lines[l] || p) { + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + length += points[i].distance(points[i + 1]); + } - var partition = !!p; + return length; + }, - p = word.length - 1; + pointAt: function(ratio) { - if (partition || !p) { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point - // word has only one character. - if (!p) { + if (ratio <= 0) return points[0].clone(); + if (ratio >= 1) return points[numPoints - 1].clone(); - if (!lines[l]) { + var polylineLength = this.length(); + var length = polylineLength * ratio; - // we won't fit this text within our rect - lines = []; + return this.pointAtLength(length); + }, - break; - } + pointAtLength: function(length) { - // partitioning didn't help on the non-empty line - // try again, but this time start with a new line + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point - // cancel partitions created - words.splice(i, 2, word + words[i + 1]); + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // adjust word length - len--; + var l = 0; + var n = numPoints - 1; + for (var i = (fromStart ? 0 : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - full[l++] = true; - i--; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); - continue; - } + if (length <= (l + d)) { + return line.pointAtLength((fromStart ? 1 : -1) * (length - l)); + } - // move last letter to the beginning of the next word - words[i] = word.substring(0, p); - words[i + 1] = word.substring(p) + words[i + 1]; + l += d; + } - } else { + // if length requested is higher than the length of the polyline, return last endpoint + var lastPoint = (fromStart ? points[numPoints - 1] : points[0]); + return lastPoint.clone(); + }, - // We initiate partitioning - // split the long word into two words - words.splice(i, 1, word.substring(0, p), word.substring(p)); + scale: function(sx, sy, origin) { - // adjust words length - len++; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty - if (l && !full[l - 1]) { - // if the previous line is not full, try to fit max part of - // the current word there - l--; - } - } + for (var i = 0; i < numPoints; i++) { + points[i].scale(sx, sy, origin); + } - i--; + return this; + }, - continue; - } + tangentAt: function(ratio) { - l++; - i--; - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point - // if size.height is defined we have to check whether the height of the entire - // text exceeds the rect height - if (height !== undefined) { + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - if (lineHeight === undefined) { + var polylineLength = this.length(); + var length = polylineLength * ratio; - var heightValue; + return this.tangentAtLength(length); + }, - // use the same defaults as in V.prototype.text - if (styles.lineHeight === 'auto') { - heightValue = { value: 1.5, unit: 'em' }; - } else { - heightValue = joint.util.parseCssNumeric(styles.lineHeight, ['em']) || { value: 1, unit: 'em' }; - } + tangentAtLength: function(length) { - lineHeight = heightValue.value; - if (heightValue.unit === 'em' ) { - lineHeight *= textElement.getBBox().height; - } - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point - if (lineHeight * lines.length > height) { + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // remove overflowing lines - lines.splice(Math.floor(height / lineHeight)); + var lastValidLine; // differentiable (with a tangent) + var l = 0; // length so far + var n = numPoints - 1; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - break; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (line.isDifferentiable()) { // has a tangent line (line length is not 0) + if (length <= (l + d)) { + return line.tangentAtLength((fromStart ? 1 : -1) * (length - l)); } + + lastValidLine = line; } + + l += d; } - if (opt.svgDocument) { + // if length requested is higher than the length of the polyline, return last valid endpoint + if (lastValidLine) { + var ratio = (fromStart ? 1 : 0); + return lastValidLine.tangentAt(ratio); + } - // svg document was provided, remove the text element only - svgDocument.removeChild(textElement); + // if no valid line, return null + return null; + }, - } else { + intersectionWithLine: function(l) { + var line = new Line(l); + var intersections = []; + var points = this.points; + for (var i = 0, n = points.length - 1; i < n; i++) { + var a = points[i]; + var b = points[i+1]; + var l2 = new Line(a, b); + var int = line.intersectionWithLine(l2); + if (int) intersections.push(int[0]); + } + return (intersections.length > 0) ? intersections : null; + }, - // clean svg document - document.body.removeChild(svgDocument); + translate: function(tx, ty) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + points[i].translate(tx, ty); } - return lines.join('\n'); + return this; }, - imageToDataUri: function(url, callback) { + // Return svgString that can be used to recreate this line. + serialize: function() { - if (!url || url.substr(0, 'data:'.length) === 'data:') { - // No need to convert to data uri if it is already in data uri. + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return ''; // if points array is empty - // This not only convenient but desired. For example, - // IE throws a security error if data:image/svg+xml is used to render - // an image to the canvas and an attempt is made to read out data uri. - // Now if our image is already in data uri, there is no need to render it to the canvas - // and so we can bypass this error. + var output = ''; + for (var i = 0; i < numPoints; i++) { - // Keep the async nature of the function. - return setTimeout(function() { - callback(null, url); - }, 0); + var point = points[i]; + output += point.x + ',' + point.y + ' '; } - // chrome IE10 IE11 - var modernHandler = function(xhr, callback) { + return output.trim(); + }, - if (xhr.status === 200) { + toString: function() { - var reader = new FileReader(); + return this.points + ''; + } + }; - reader.onload = function(evt) { - var dataUri = evt.target.result; - callback(null, dataUri); - }; + Object.defineProperty(Polyline.prototype, 'start', { + // Getter for the first point of the polyline. - reader.onerror = function() { - callback(new Error('Failed to load image ' + url)); - }; + configurable: true, - reader.readAsDataURL(xhr.response); - } else { - callback(new Error('Failed to load image ' + url)); - } + enumerable: true, - }; + get: function() { - var legacyHandler = function(xhr, callback) { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - var Uint8ToString = function(u8a) { - var CHUNK_SZ = 0x8000; - var c = []; - for (var i = 0; i < u8a.length; i += CHUNK_SZ) { - c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); - } - return c.join(''); - }; + return this.points[0]; + }, + }); + Object.defineProperty(Polyline.prototype, 'end', { + // Getter for the last point of the polyline. - if (xhr.status === 200) { + configurable: true, - var bytes = new Uint8Array(xhr.response); + enumerable: true, - var suffix = (url.split('.').pop()) || 'png'; - var map = { - 'svg': 'svg+xml' - }; - var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,'; - var b64encoded = meta + btoa(Uint8ToString(bytes)); - callback(null, b64encoded); - } else { - callback(new Error('Failed to load image ' + url)); - } - }; + get: function() { - var xhr = new XMLHttpRequest(); + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - xhr.open('GET', url, true); - xhr.addEventListener('error', function() { - callback(new Error('Failed to load image ' + url)); - }); + return this.points[numPoints - 1]; + }, + }); - xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer'; + g.scale = { - xhr.addEventListener('load', function() { - if (window.FileReader) { - modernHandler(xhr, callback); - } else { - legacyHandler(xhr, callback); - } - }); - - xhr.send(); - }, + // Return the `value` from the `domain` interval scaled to the `range` interval. + linear: function(domain, range, value) { - getElementBBox: function(el) { + var domainSpan = domain[1] - domain[0]; + var rangeSpan = range[1] - range[0]; + return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; + } + }; - var $el = $(el); - if ($el.length === 0) { - throw new Error('Element not found') - } + var normalizeAngle = g.normalizeAngle = function(angle) { - var element = $el[0]; - var doc = element.ownerDocument; - var clientBBox = element.getBoundingClientRect(); + return (angle % 360) + (angle < 0 ? 360 : 0); + }; - var strokeWidthX = 0; - var strokeWidthY = 0; + var snapToGrid = g.snapToGrid = function(value, gridSize) { - // Firefox correction - if (element.ownerSVGElement) { + return gridSize * round(value / gridSize); + }; - var vel = V(element); - var bbox = vel.getBBox({ target: vel.svg() }); + var toDeg = g.toDeg = function(rad) { - // if FF getBoundingClientRect includes stroke-width, getBBox doesn't. - // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value. - strokeWidthX = (clientBBox.width - bbox.width); - strokeWidthY = (clientBBox.height - bbox.height); - } + return (180 * rad / PI) % 360; + }; - return { - x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2, - y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2, - width: clientBBox.width - strokeWidthX, - height: clientBBox.height - strokeWidthY - }; - }, + var toRad = g.toRad = function(deg, over360) { + over360 = over360 || false; + deg = over360 ? deg : (deg % 360); + return deg * PI / 180; + }; - // Highly inspired by the jquery.sortElements plugin by Padolsey. - // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. - sortElements: function(elements, comparator) { + // For backwards compatibility: + g.ellipse = g.Ellipse; + g.line = g.Line; + g.point = g.Point; + g.rect = g.Rect; - var $elements = $(elements); - var placements = $elements.map(function() { + // Local helper function. + // Use an array of arguments to call a constructor (function called with `new`). + // Adapted from https://stackoverflow.com/a/8843181/2263595 + // It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). + // - If that is the case, use `new constructor(arg1, arg2)`, for example. + // It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. + // - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. + function applyToNew(constructor, argsArray) { + // The `new` keyword can only be applied to functions that take a limited number of arguments. + // - We can fake that with .bind(). + // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. + // - So `new (constructor.bind(thisArg, arg1, arg2...))` + // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. + // We need to pass in a variable number of arguments to the bind() call. + // - We can use .apply(). + // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, arg2...]))` + // - `thisArg` can still be anything because `new` overwrites it. + // Finally, to make sure that constructor.bind overwriting is not a problem, we switch to `Function.prototype.bind`. + // - So, the final version is `new (Function.prototype.bind.apply(constructor, [thisArg, arg1, arg2...]))` + + // The function expects `argsArray[0]` to be `thisArg`. + // - This means that whatever is sent as the first element will be ignored. + // - The constructor will only see arguments starting from argsArray[1]. + // - So, a new dummy element is inserted at the start of the array. + argsArray.unshift(null); + + return new (Function.prototype.bind.apply(constructor, argsArray)); + } - var sortElement = this; - var parentNode = sortElement.parentNode; - // Since the element itself will change position, we have - // to have some way of storing it's original position in - // the DOM. The easiest way is to have a 'flag' node: - var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); + // Local helper function. + // Add properties from arguments on top of properties from `obj`. + // This allows for rudimentary inheritance. + // - The `obj` argument acts as parent. + // - This function creates a new object that inherits all `obj` properties and adds/replaces those that are present in arguments. + // - A high-level example: calling `extend(Vehicle, Car)` would be akin to declaring `class Car extends Vehicle`. + function extend(obj) { + // In JavaScript, the combination of a constructor function (e.g. `g.Line = function(...) {...}`) and prototype (e.g. `g.Line.prototype = {...}) is akin to a C++ class. + // - When inheritance is not necessary, we can leave it at that. (This would be akin to calling extend with only `obj`.) + // - But, what if we wanted the `g.Line` quasiclass to inherit from another quasiclass (let's call it `g.GeometryObject`) in JavaScript? + // - First, realize that both of those quasiclasses would still have their own separate constructor function. + // - So what we are actually saying is that we want the `g.Line` prototype to inherit from `g.GeometryObject` prototype. + // - This method provides a way to do exactly that. + // - It copies parent prototype's properties, then adds extra ones from child prototype/overrides parent prototype properties with child prototype properties. + // - Therefore, to continue with the example above: + // - `g.Line.prototype = extend(g.GeometryObject.prototype, linePrototype)` + // - Where `linePrototype` is a properties object that looks just like `g.Line.prototype` does right now. + // - Then, `g.Line` would allow the programmer to access to all methods currently in `g.Line.Prototype`, plus any non-overriden methods from `g.GeometryObject.prototype`. + // - In that aspect, `g.GeometryObject` would then act like the parent of `g.Line`. + // - Multiple inheritance is also possible, if multiple arguments are provided. + // - What if we wanted to add another level of abstraction between `g.GeometryObject` and `g.Line` (let's call it `g.LinearObject`)? + // - `g.Line.prototype = extend(g.GeometryObject.prototype, g.LinearObject.prototype, linePrototype)` + // - The ancestors are applied in order of appearance. + // - That means that `g.Line` would have inherited from `g.LinearObject` that would have inherited from `g.GeometryObject`. + // - Any number of ancestors may be provided. + // - Note that neither `obj` nor any of the arguments need to actually be prototypes of any JavaScript quasiclass, that was just a simplified explanation. + // - We can create a new object composed from the properties of any number of other objects (since they do not have a constructor, we can think of those as interfaces). + // - `extend({ a: 1, b: 2 }, { b: 10, c: 20 }, { c: 100, d: 200 })` gives `{ a: 1, b: 10, c: 100, d: 200 }`. + // - Basically, with this function, we can emulate the `extends` keyword as well as the `implements` keyword. + // - Therefore, both of the following are valid: + // - `Lineto.prototype = extend(Line.prototype, segmentPrototype, linetoPrototype)` + // - `Moveto.prototype = extend(segmentPrototype, movetoPrototype)` - return function() { + var i; + var n; - if (parentNode === this) { - throw new Error('You can\'t sort elements if any one is a descendant of another.'); - } + var args = []; + n = arguments.length; + for (i = 1; i < n; i++) { // skip over obj + args.push(arguments[i]); + } - // Insert before flag: - parentNode.insertBefore(this, nextSibling); - // Remove flag: - parentNode.removeChild(nextSibling); - }; - }); + if (!obj) throw new Error('Missing a parent object.'); + var child = Object.create(obj); - return Array.prototype.sort.call($elements, comparator).each(function(i) { - placements[i].call(this); - }); - }, + n = args.length; + for (i = 0; i < n; i++) { - // Sets attributes on the given element and its descendants based on the selector. - // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} - setAttributesBySelector: function(element, attrs) { + var src = args[i]; - var $element = $(element); + var inheritedProperty; + var key; + for (key in src) { - joint.util.forIn(attrs, function(attrs, selector) { - var $elements = $element.find(selector).addBack().filter(selector); - // Make a special case for setting classes. - // We do not want to overwrite any existing class. - if (joint.util.has(attrs, 'class')) { - $elements.addClass(attrs['class']); - attrs = joint.util.omit(attrs, 'class'); + if (src.hasOwnProperty(key)) { + delete child[key]; // delete property inherited from parent + inheritedProperty = Object.getOwnPropertyDescriptor(src, key); // get new definition of property from src + Object.defineProperty(child, key, inheritedProperty); // re-add property with new definition (includes getter/setter methods) } - $elements.attr(attrs); - }); - }, + } + } - // Return a new object with all for sides (top, bottom, left and right) in it. - // Value of each side is taken from the given argument (either number or object). - // Default value for a side is 0. - // Examples: - // joint.util.normalizeSides(5) --> { top: 5, left: 5, right: 5, bottom: 5 } - // joint.util.normalizeSides({ left: 5 }) --> { top: 0, left: 5, right: 0, bottom: 0 } - normalizeSides: function(box) { + return child; + } - if (Object(box) !== box) { - box = box || 0; - return { top: box, bottom: box, left: box, right: box }; - } + // Path segment interface: + var segmentPrototype = { - return { - top: box.top || 0, - bottom: box.bottom || 0, - left: box.left || 0, - right: box.right || 0 - }; + // Redirect calls to closestPointNormalizedLength() function if closestPointT() is not defined for segment. + closestPointT: function(p) { + + if (this.closestPointNormalizedLength) return this.closestPointNormalizedLength(p); + + throw new Error('Neither closestPointT() nor closestPointNormalizedLength() function is implemented.'); }, - timing: { + isSegment: true, - linear: function(t) { - return t; - }, + isSubpathStart: false, // true for Moveto segments - quad: function(t) { - return t * t; - }, + isVisible: true, // false for Moveto segments - cubic: function(t) { - return t * t * t; - }, + nextSegment: null, // needed for subpath start segment updating - inout: function(t) { - if (t <= 0) return 0; - if (t >= 1) return 1; - var t2 = t * t; - var t3 = t2 * t; - return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); - }, + // Return a fraction of result of length() function if lengthAtT() is not defined for segment. + lengthAtT: function(t) { - exponential: function(t) { - return Math.pow(2, 10 * (t - 1)); - }, + if (t <= 0) return 0; - bounce: function(t) { - for (var a = 0, b = 1; 1; a += b, b /= 2) { - if (t >= (7 - 4 * a) / 11) { - var q = (11 - 6 * a - 11 * t) / 4; - return -q * q + b * b; - } - } - }, + var length = this.length(); - reverse: function(f) { - return function(t) { - return 1 - f(1 - t); - }; - }, + if (t >= 1) return length; - reflect: function(f) { - return function(t) { - return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); - }; - }, + return length * t; + }, - clamp: function(f, n, x) { - n = n || 0; - x = x || 1; - return function(t) { - var r = f(t); - return r < n ? n : r > x ? x : r; - }; - }, + // Redirect calls to pointAt() function if pointAtT() is not defined for segment. + pointAtT: function(t) { - back: function(s) { - if (!s) s = 1.70158; - return function(t) { - return t * t * ((s + 1) * t - s); - }; - }, + if (this.pointAt) return this.pointAt(t); - elastic: function(x) { - if (!x) x = 1.5; - return function(t) { - return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); - }; - } + throw new Error('Neither pointAtT() nor pointAt() function is implemented.'); }, - interpolate: { + previousSegment: null, // needed to get segment start property - number: function(a, b) { - var d = b - a; - return function(t) { return a + d * t; }; - }, + subpathStartSegment: null, // needed to get closepath segment end property - object: function(a, b) { - var s = Object.keys(a); - return function(t) { - var i, p; - var r = {}; - for (i = s.length - 1; i != -1; i--) { - p = s[i]; - r[p] = a[p] + (b[p] - a[p]) * t; - } - return r; - }; - }, + // Redirect calls to tangentAt() function if tangentAtT() is not defined for segment. + tangentAtT: function(t) { - hexColor: function(a, b) { + if (this.tangentAt) return this.tangentAt(t); - var ca = parseInt(a.slice(1), 16); - var cb = parseInt(b.slice(1), 16); - var ra = ca & 0x0000ff; - var rd = (cb & 0x0000ff) - ra; - var ga = ca & 0x00ff00; - var gd = (cb & 0x00ff00) - ga; - var ba = ca & 0xff0000; - var bd = (cb & 0xff0000) - ba; + throw new Error('Neither tangentAtT() nor tangentAt() function is implemented.'); + }, - return function(t) { + // VIRTUAL PROPERTIES (must be overriden by actual Segment implementations): - var r = (ra + rd * t) & 0x000000ff; - var g = (ga + gd * t) & 0x0000ff00; - var b = (ba + bd * t) & 0x00ff0000; + // type - return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); - }; - }, + // start // getter, always throws error for Moveto - unit: function(a, b) { + // end // usually directly assigned, getter for Closepath - var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; - var ma = r.exec(a); - var mb = r.exec(b); - var p = mb[1].indexOf('.'); - var f = p > 0 ? mb[1].length - p - 1 : 0; - a = +ma[1]; - var d = +mb[1] - a; - var u = ma[2]; + bbox: function() { - return function(t) { - return (a + d * t).toFixed(f) + u; - }; - } + throw new Error('Declaration missing for virtual function.'); }, - // SVG filters. - filter: { + clone: function() { - // `color` ... outline color - // `width`... outline width - // `opacity` ... outline opacity - // `margin` ... gap between outline and the element - outline: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var tpl = ''; + closestPoint: function() { - var margin = Number.isFinite(args.margin) ? args.margin : 2; - var width = Number.isFinite(args.width) ? args.width : 1; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template(tpl)({ - color: args.color || 'blue', - opacity: Number.isFinite(args.opacity) ? args.opacity : 1, - outerRadius: margin + width, - innerRadius: margin - }); - }, + closestPointLength: function() { - // `color` ... color - // `width`... width - // `blur` ... blur - // `opacity` ... opacity - highlight: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var tpl = ''; + closestPointNormalizedLength: function() { - return joint.util.template(tpl)({ - color: args.color || 'red', - width: Number.isFinite(args.width) ? args.width : 1, - blur: Number.isFinite(args.blur) ? args.blur : 0, - opacity: Number.isFinite(args.opacity) ? args.opacity : 1 - }); - }, - - // `x` ... horizontal blur - // `y` ... vertical blur (optional) - blur: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var x = Number.isFinite(args.x) ? args.x : 2; + closestPointTangent: function() { - return joint.util.template('')({ - stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `dx` ... horizontal shift - // `dy` ... vertical shift - // `blur` ... blur - // `color` ... color - // `opacity` ... opacity - dropShadow: function(args) { + equals: function() { - var tpl = 'SVGFEDropShadowElement' in window - ? '' - : ''; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template(tpl)({ - dx: args.dx || 0, - dy: args.dy || 0, - opacity: Number.isFinite(args.opacity) ? args.opacity : 1, - color: args.color || 'black', - blur: Number.isFinite(args.blur) ? args.blur : 4 - }); - }, + getSubdivisions: function() { - // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. - grayscale: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + isDifferentiable: function() { - return joint.util.template('')({ - a: 0.2126 + 0.7874 * (1 - amount), - b: 0.7152 - 0.7152 * (1 - amount), - c: 0.0722 - 0.0722 * (1 - amount), - d: 0.2126 - 0.2126 * (1 - amount), - e: 0.7152 + 0.2848 * (1 - amount), - f: 0.0722 - 0.0722 * (1 - amount), - g: 0.2126 - 0.2126 * (1 - amount), - h: 0.0722 + 0.9278 * (1 - amount) - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. - sepia: function(args) { + length: function() { - var amount = Number.isFinite(args.amount) ? args.amount : 1; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template('')({ - a: 0.393 + 0.607 * (1 - amount), - b: 0.769 - 0.769 * (1 - amount), - c: 0.189 - 0.189 * (1 - amount), - d: 0.349 - 0.349 * (1 - amount), - e: 0.686 + 0.314 * (1 - amount), - f: 0.168 - 0.168 * (1 - amount), - g: 0.272 - 0.272 * (1 - amount), - h: 0.534 - 0.534 * (1 - amount), - i: 0.131 + 0.869 * (1 - amount) - }); - }, + pointAt: function() { - // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. - saturate: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + pointAtLength: function() { - return joint.util.template('')({ - amount: 1 - amount - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `angle` ... the number of degrees around the color circle the input samples will be adjusted. - hueRotate: function(args) { + scale: function() { - return joint.util.template('')({ - angle: args.angle || 0 - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. - invert: function(args) { + tangentAt: function() { - var amount = Number.isFinite(args.amount) ? args.amount : 1; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template('')({ - amount: amount, - amount2: 1 - amount - }); - }, + tangentAtLength: function() { - // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. - brightness: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template('')({ - amount: Number.isFinite(args.amount) ? args.amount : 1 - }); - }, + translate: function() { - // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. - contrast: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + serialize: function() { - return joint.util.template('')({ - amount: amount, - amount2: .5 - amount / 2 - }); - } + throw new Error('Declaration missing for virtual function.'); }, - format: { + toString: function() { - // Formatting numbers via the Python Format Specification Mini-language. - // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. - // Heavilly inspired by the D3.js library implementation. - number: function(specifier, value, locale) { + throw new Error('Declaration missing for virtual function.'); + } + }; - locale = locale || { + // Path segment implementations: + var Lineto = function() { - currency: ['$', ''], - decimal: '.', - thousands: ',', - grouping: [3] - }; + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. - // [[fill]align][sign][symbol][0][width][,][.precision][type] - var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + if (!(this instanceof Lineto)) { // switching context of `this` to Lineto when called without `new` + return applyToNew(Lineto, args); + } - var match = re.exec(specifier); - var fill = match[1] || ' '; - var align = match[2] || '>'; - var sign = match[3] || ''; - var symbol = match[4] || ''; - var zfill = match[5]; - var width = +match[6]; - var comma = match[7]; - var precision = match[8]; - var type = match[9]; - var scale = 1; - var prefix = ''; - var suffix = ''; - var integer = false; + if (n === 0) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (none provided).'); + } - if (precision) precision = +precision.substring(1); + var outputArray; - if (zfill || fill === '0' && align === '=') { - zfill = fill = '0'; - align = '='; - if (comma) width -= Math.floor((width - 1) / 4); - } + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; - switch (type) { - case 'n': - comma = true; type = 'g'; - break; - case '%': - scale = 100; suffix = '%'; type = 'f'; - break; - case 'p': - scale = 100; suffix = '%'; type = 'r'; - break; - case 'b': - case 'o': - case 'x': - case 'X': - if (symbol === '#') prefix = '0' + type.toLowerCase(); - break; - case 'c': - case 'd': - integer = true; precision = 0; - break; - case 's': - scale = -1; type = 'r'; - break; - } + } else if (n < 2) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); - if (symbol === '$') { - prefix = locale.currency[0]; - suffix = locale.currency[1]; + } else { // this is a poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + outputArray.push(applyToNew(Lineto, segmentCoords)); } + return outputArray; + } - // If no precision is specified for `'r'`, fallback to general notation. - if (type == 'r' && !precision) type = 'g'; + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; - // Ensure that the requested precision is in the supported range. - if (precision != null) { - if (type == 'g') precision = Math.max(1, Math.min(21, precision)); - else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); + } else { // this is a poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { + + segmentPoint = args[i]; + outputArray.push(new Lineto(segmentPoint)); } + return outputArray; + } + } + }; - var zcomma = zfill && comma; + var linetoPrototype = { - // Return the empty string for floats formatted as ints. - if (integer && (value % 1)) return ''; + clone: function() { - // Convert negative to positive, and record the sign prefix. - var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; + return new Lineto(this.end); + }, - var fullSuffix = suffix; + getSubdivisions: function() { - // Apply the scale, computing it from the value's exponent for si format. - // Preserve the existing suffix, if any, such as the currency symbol. - if (scale < 0) { - var unit = this.prefix(value, precision); - value = unit.scale(value); - fullSuffix = unit.symbol + suffix; - } else { - value *= scale; - } + return []; + }, - // Convert to the desired precision. - value = this.convert(type, value, precision); + isDifferentiable: function() { - // Break the value into the integer part (before) and decimal part (after). - var i = value.lastIndexOf('.'); - var before = i < 0 ? value : value.substring(0, i); - var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); + if (!this.previousSegment) return false; - function formatGroup(value) { + return !this.start.equals(this.end); + }, - var i = value.length; - var t = []; - var j = 0; - var g = locale.grouping[0]; - while (i > 0 && g > 0) { - t.push(value.substring(i -= g, i + g)); - g = locale.grouping[j = (j + 1) % locale.grouping.length]; - } - return t.reverse().join(locale.thousands); - } + scale: function(sx, sy, origin) { - // If the fill character is not `'0'`, grouping is applied before padding. - if (!zfill && comma && locale.grouping) { + this.end.scale(sx, sy, origin); + return this; + }, - before = formatGroup(before); - } + translate: function(tx, ty) { - var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); - var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; + this.end.translate(tx, ty); + return this; + }, - // If the fill character is `'0'`, grouping is applied after padding. - if (zcomma) before = formatGroup(padding + before); + type: 'L', - // Apply prefix. - negative += prefix; + serialize: function() { - // Rejoin integer and decimal parts. - value = before + after; + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, - return (align === '<' ? negative + value + padding - : align === '>' ? padding + negative + value - : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) - : negative + (zcomma ? value : padding + value)) + fullSuffix; - }, + toString: function() { - // Formatting string via the Python Format string. - // See https://docs.python.org/2/library/string.html#format-string-syntax) - string: function(formatString, value) { + return this.type + ' ' + this.start + ' ' + this.end; + } + }; - var fieldDelimiterIndex; - var fieldDelimiter = '{'; - var endPlaceholder = false; - var formattedStringArray = []; + Object.defineProperty(linetoPrototype, 'start', { + // get a reference to the end point of previous segment - while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { + configurable: true, - var pieceFormatedString, formatSpec, fieldName; + enumerable: true, - pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); + get: function() { - if (endPlaceholder) { - formatSpec = pieceFormatedString.split(':'); - fieldName = formatSpec.shift().split('.'); - pieceFormatedString = value; + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - for (var i = 0; i < fieldName.length; i++) - pieceFormatedString = pieceFormatedString[fieldName[i]]; + return this.previousSegment.end; + } + }); - if (formatSpec.length) - pieceFormatedString = this.number(formatSpec, pieceFormatedString); - } + Lineto.prototype = extend(segmentPrototype, Line.prototype, linetoPrototype); - formattedStringArray.push(pieceFormatedString); + var Curveto = function() { - formatString = formatString.slice(fieldDelimiterIndex + 1); - fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; - } - formattedStringArray.push(formatString); + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - return formattedStringArray.join(''); - }, + if (!(this instanceof Curveto)) { // switching context of `this` to Curveto when called without `new` + return applyToNew(Curveto, args); + } - convert: function(type, value, precision) { + if (n === 0) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (none provided).'); + } - switch (type) { - case 'b': return value.toString(2); - case 'c': return String.fromCharCode(value); - case 'o': return value.toString(8); - case 'x': return value.toString(16); - case 'X': return value.toString(16).toUpperCase(); - case 'g': return value.toPrecision(precision); - case 'e': return value.toExponential(precision); - case 'f': return value.toFixed(precision); - case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); - default: return value + ''; - } - }, + var outputArray; - round: function(value, precision) { + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 6) { + this.controlPoint1 = new Point(+args[0], +args[1]); + this.controlPoint2 = new Point(+args[2], +args[3]); + this.end = new Point(+args[4], +args[5]); + return this; - return precision - ? Math.round(value * (precision = Math.pow(10, precision))) / precision - : Math.round(value); - }, + } else if (n < 6) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' coordinates provided).'); - precision: function(value, precision) { + } else { // this is a poly-bezier segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 6) { // coords come in groups of six - return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); - }, + segmentCoords = args.slice(i, i + 6); // will send fewer than six coords if args.length not divisible by 6 + outputArray.push(applyToNew(Curveto, segmentCoords)); + } + return outputArray; + } - prefix: function(value, precision) { + } else { // points provided + if (n === 3) { + this.controlPoint1 = new Point(args[0]); + this.controlPoint2 = new Point(args[1]); + this.end = new Point(args[2]); + return this; - var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) { - var k = Math.pow(10, Math.abs(8 - i) * 3); - return { - scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, - symbol: d - }; - }); + } else if (n < 3) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' points provided).'); - var i = 0; - if (value) { - if (value < 0) value *= -1; - if (precision) value = this.round(value, this.precision(value, precision)); - i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); - i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); + } else { // this is a poly-bezier segment + var segmentPoints; + outputArray = []; + for (i = 0; i < n; i += 3) { // points come in groups of three + + segmentPoints = args.slice(i, i + 3); // will send fewer than three points if args.length is not divisible by 3 + outputArray.push(applyToNew(Curveto, segmentPoints)); } - return prefixes[8 + i / 3]; + return outputArray; } - }, + } + }; - /* - Pre-compile the HTML to be used as a template. - */ - template: function(html) { + var curvetoPrototype = { - /* - Must support the variation in templating syntax found here: - https://lodash.com/docs#template - */ - var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; + clone: function() { - return function(data) { + return new Curveto(this.controlPoint1, this.controlPoint2, this.end); + }, - data = data || {}; + isDifferentiable: function() { - return html.replace(regex, function(match) { + if (!this.previousSegment) return false; - var args = Array.from(arguments); - var attr = args.slice(1, 4).find(function(_attr) { - return !!_attr; - }); + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; - var attrArray = attr.split('.'); - var value = data[attrArray.shift()]; + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); + }, - while (value !== undefined && attrArray.length) { - value = value[attrArray.shift()]; - } + scale: function(sx, sy, origin) { - return value !== undefined ? value : ''; - }); - }; + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; }, - /** - * @param {Element=} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default. - */ - toggleFullScreen: function(el) { + translate: function(tx, ty) { - var topDocument = window.top.document; - el = el || topDocument.body; + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, - function prefixedResult(el, prop) { + type: 'C', - var prefixes = ['webkit', 'moz', 'ms', 'o', '']; - for (var i = 0; i < prefixes.length; i++) { - var prefix = prefixes[i]; - var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); - if (el[propName] !== undefined) { - return joint.util.isFunction(el[propName]) ? el[propName]() : el[propName]; - } - } - } + serialize: function() { - if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) { - prefixedResult(topDocument, 'ExitFullscreen') || // Spec. - prefixedResult(topDocument, 'CancelFullScreen'); // Firefox - } else { - prefixedResult(el, 'RequestFullscreen') || // Spec. - prefixedResult(el, 'RequestFullScreen'); // Firefox - } + var c1 = this.controlPoint1; + var c2 = this.controlPoint2; + var end = this.end; + return this.type + ' ' + c1.x + ' ' + c1.y + ' ' + c2.x + ' ' + c2.y + ' ' + end.x + ' ' + end.y; }, - addClassNamePrefix: function(className) { + toString: function() { - if (!className) return className; + return this.type + ' ' + this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; - return className.toString().split(' ').map(function(_className) { + Object.defineProperty(curvetoPrototype, 'start', { + // get a reference to the end point of previous segment - if (_className.substr(0, joint.config.classNamePrefix.length) !== joint.config.classNamePrefix) { - _className = joint.config.classNamePrefix + _className; - } + configurable: true, - return _className; + enumerable: true, - }).join(' '); - }, + get: function() { - removeClassNamePrefix: function(className) { + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - if (!className) return className; + return this.previousSegment.end; + } + }); - return className.toString().split(' ').map(function(_className) { + Curveto.prototype = extend(segmentPrototype, Curve.prototype, curvetoPrototype); - if (_className.substr(0, joint.config.classNamePrefix.length) === joint.config.classNamePrefix) { - _className = _className.substr(joint.config.classNamePrefix.length); - } + var Moveto = function() { - return _className; + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - }).join(' '); - }, + if (!(this instanceof Moveto)) { // switching context of `this` to Moveto when called without `new` + return applyToNew(Moveto, args); + } - wrapWith: function(object, methods, wrapper) { + if (n === 0) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (none provided).'); + } - if (joint.util.isString(wrapper)) { + var outputArray; - if (!joint.util.wrappers[wrapper]) { - throw new Error('Unknown wrapper: "' + wrapper + '"'); - } + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; - wrapper = joint.util.wrappers[wrapper]; - } + } else if (n < 2) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); - if (!joint.util.isFunction(wrapper)) { - throw new Error('Wrapper must be a function.'); + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + if (i === 0) outputArray.push(applyToNew(Moveto, segmentCoords)); + else outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; } - this.toArray(methods).forEach(function(method) { - object[method] = wrapper(object[method]); - }); - }, + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; - wrappers: { + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { // points come one by one - /* - Prepares a function with the following usage: + segmentPoint = args[i]; + if (i === 0) outputArray.push(new Moveto(segmentPoint)); + else outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } + }; - fn([cell, cell, cell], opt); - fn([cell, cell, cell]); - fn(cell, cell, cell, opt); - fn(cell, cell, cell); - fn(cell); - */ - cells: function(fn) { + var movetoPrototype = { - return function() { + bbox: function() { - var args = Array.from(arguments); - var n = args.length; - var cells = n > 0 && args[0] || []; - var opt = n > 1 && args[n - 1] || {}; + return null; + }, - if (!Array.isArray(cells)) { + clone: function() { - if (opt instanceof joint.dia.Cell) { - cells = args; - } else if (cells instanceof joint.dia.Cell) { - if (args.length > 1) { - args.pop(); - } - cells = args; - } - } + return new Moveto(this.end); + }, - if (opt instanceof joint.dia.Cell) { - opt = {}; - } + closestPoint: function() { - return fn.call(this, cells, opt); - }; - } + return this.end.clone(); }, - // lodash 3 vs 4 incompatible - sortedIndex: _.sortedIndexBy || _.sortedIndex, - uniq: _.uniqBy || _.uniq, - uniqueId: _.uniqueId, - sortBy: _.sortBy, - isFunction: _.isFunction, - result: _.result, - union: _.union, - invoke: _.invokeMap || _.invoke, - difference: _.difference, - intersection: _.intersection, - omit: _.omit, - pick: _.pick, - has: _.has, - bindAll: _.bindAll, - assign: _.assign, - defaults: _.defaults, - defaultsDeep: _.defaultsDeep, - isPlainObject: _.isPlainObject, - isEmpty: _.isEmpty, - isEqual: _.isEqual, - noop: function() {}, - cloneDeep: _.cloneDeep, - toArray: _.toArray, - flattenDeep: _.flattenDeep, - camelCase: _.camelCase, - groupBy: _.groupBy, - forIn: _.forIn, - without: _.without, - debounce: _.debounce, - clone: _.clone, - isBoolean: function(value) { - var toString = Object.prototype.toString; - return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]'); - }, + closestPointNormalizedLength: function() { - isObject: function(value) { - return !!value && (typeof value === 'object' || typeof value === 'function'); + return 0; }, - isNumber: function(value) { - var toString = Object.prototype.toString; - return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]'); + closestPointLength: function() { + + return 0; }, - isString: function(value) { - var toString = Object.prototype.toString; - return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]'); + closestPointT: function() { + + return 1; }, - merge: function() { - if (_.mergeWith) { - var args = Array.from(arguments); - var last = args[args.length - 1]; + closestPointTangent: function() { - var customizer = this.isFunction(last) ? last : this.noop; - args.push(function(a,b) { - var customResult = customizer(a, b); - if (customResult !== undefined) { - return customResult; - } + return null; + }, - if (Array.isArray(a) && !Array.isArray(b)) { - return b; - } - }); + equals: function(m) { - return _.mergeWith.apply(this, args) - } - return _.merge.apply(this, arguments); - } - } -}; + return this.end.equals(m.end); + }, + getSubdivisions: function() { -joint.mvc.View = Backbone.View.extend({ + return []; + }, - options: {}, - theme: null, - themeClassNamePrefix: joint.util.addClassNamePrefix('theme-'), - requireSetThemeOverride: false, - defaultTheme: joint.config.defaultTheme, + isDifferentiable: function() { - constructor: function(options) { + return false; + }, - this.requireSetThemeOverride = options && !!options.theme; - this.options = joint.util.assign({}, this.options, options); + isSubpathStart: true, - Backbone.View.call(this, options); - }, + isVisible: false, - initialize: function(options) { + length: function() { - joint.util.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); + return 0; + }, - joint.mvc.views[this.cid] = this; + lengthAtT: function() { - this.setTheme(this.options.theme || this.defaultTheme); - this.init(); - }, + return 0; + }, - // Override the Backbone `_ensureElement()` method in order to create an - // svg element (e.g., ``) node that wraps all the nodes of the Cell view. - // Expose class name setter as a separate method. - _ensureElement: function() { - if (!this.el) { - var tagName = joint.util.result(this, 'tagName'); - var attrs = joint.util.assign({}, joint.util.result(this, 'attributes')); - if (this.id) attrs.id = joint.util.result(this, 'id'); - this.setElement(this._createElement(tagName)); - this._setAttributes(attrs); - } else { - this.setElement(joint.util.result(this, 'el')); - } - this._ensureElClassName(); - }, + pointAt: function() { - _setAttributes: function(attrs) { - if (this.svgElement) { - this.vel.attr(attrs); - } else { - this.$el.attr(attrs); - } - }, + return this.end.clone(); + }, - _createElement: function(tagName) { - if (this.svgElement) { - return document.createElementNS(V.namespace.xmlns, tagName); - } else { - return document.createElement(tagName); - } - }, + pointAtLength: function() { - // Utilize an alternative DOM manipulation API by - // adding an element reference wrapped in Vectorizer. - _setElement: function(el) { - this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); - this.el = this.$el[0]; - if (this.svgElement) this.vel = V(this.el); - }, + return this.end.clone(); + }, - _ensureElClassName: function() { - var className = joint.util.result(this, 'className'); - var prefixedClassName = joint.util.addClassNamePrefix(className); - // Note: className removal here kept for backwards compatibility only - if (this.svgElement) { - this.vel.removeClass(className).addClass(prefixedClassName); - } else { - this.$el.removeClass(className).addClass(prefixedClassName); - } - }, + pointAtT: function() { - init: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + return this.end.clone(); + }, - onRender: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + scale: function(sx, sy, origin) { - setTheme: function(theme, opt) { + this.end.scale(sx, sy, origin); + return this; + }, - opt = opt || {}; + tangentAt: function() { - // Theme is already set, override is required, and override has not been set. - // Don't set the theme. - if (this.theme && this.requireSetThemeOverride && !opt.override) { - return this; - } + return null; + }, - this.removeThemeClassName(); - this.addThemeClassName(theme); - this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); - this.theme = theme; + tangentAtLength: function() { - return this; - }, + return null; + }, - addThemeClassName: function(theme) { + tangentAtT: function() { - theme = theme || this.theme; + return null; + }, - var className = this.themeClassNamePrefix + theme; + translate: function(tx, ty) { + + this.end.translate(tx, ty); + return this; + }, - this.$el.addClass(className); + type: 'M', - return this; - }, + serialize: function() { - removeThemeClassName: function(theme) { + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, - theme = theme || this.theme; + toString: function() { - var className = this.themeClassNamePrefix + theme; + return this.type + ' ' + this.end; + } + }; - this.$el.removeClass(className); + Object.defineProperty(movetoPrototype, 'start', { - return this; - }, + configurable: true, - onSetTheme: function(oldTheme, newTheme) { - // Intentionally empty. - // This method is meant to be overriden. - }, + enumerable: true, - remove: function() { + get: function() { - this.onRemove(); + throw new Error('Illegal access. Moveto segments should not need a start property.'); + } + }) - joint.mvc.views[this.cid] = null; + Moveto.prototype = extend(segmentPrototype, movetoPrototype); // does not inherit from any other geometry object - Backbone.View.prototype.remove.apply(this, arguments); + var Closepath = function() { - return this; - }, + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - onRemove: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + if (!(this instanceof Closepath)) { // switching context of `this` to Closepath when called without `new` + return applyToNew(Closepath, args); + } - getEventNamespace: function() { - // Returns a per-session unique namespace - return '.joint-event-ns-' + this.cid; - } + if (n > 0) { + throw new Error('Closepath constructor expects no arguments.'); + } -}, { + return this; + }; - extend: function() { + var closepathPrototype = { - var args = Array.from(arguments); + clone: function() { - // Deep clone the prototype and static properties objects. - // This prevents unexpected behavior where some properties are overwritten outside of this function. - var protoProps = args[0] && joint.util.assign({}, args[0]) || {}; - var staticProps = args[1] && joint.util.assign({}, args[1]) || {}; + return new Closepath(); + }, - // Need the real render method so that we can wrap it and call it later. - var renderFn = protoProps.render || (this.prototype && this.prototype.render) || null; + getSubdivisions: function() { - /* - Wrap the real render method so that: - .. `onRender` is always called. - .. `this` is always returned. - */ - protoProps.render = function() { + return []; + }, - if (renderFn) { - // Call the original render method. - renderFn.apply(this, arguments); - } + isDifferentiable: function() { - // Should always call onRender() method. - this.onRender(); + if (!this.previousSegment || !this.subpathStartSegment) return false; + + return !this.start.equals(this.end); + }, + + scale: function() { - // Should always return itself. return this; - }; + }, - return Backbone.View.extend.call(this, protoProps, staticProps); - } -}); + translate: function() { + return this; + }, + type: 'Z', -joint.dia.GraphCells = Backbone.Collection.extend({ + serialize: function() { - cellNamespace: joint.shapes, + return this.type; + }, - initialize: function(models, opt) { + toString: function() { - // Set the optional namespace where all model classes are defined. - if (opt.cellNamespace) { - this.cellNamespace = opt.cellNamespace; + return this.type + ' ' + this.start + ' ' + this.end; } + }; - this.graph = opt.graph; - }, + Object.defineProperty(closepathPrototype, 'start', { + // get a reference to the end point of previous segment - model: function(attrs, options) { + configurable: true, - var collection = options.collection; - var namespace = collection.cellNamespace; + enumerable: true, - // Find the model class in the namespace or use the default one. - var ModelClass = (attrs.type === 'link') - ? joint.dia.Link - : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; + get: function() { - var cell = new ModelClass(attrs, options); - // Add a reference to the graph. It is necessary to do this here because this is the earliest place - // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. - cell.graph = collection.graph; + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - return cell; - }, + return this.previousSegment.end; + } + }); - // `comparator` makes it easy to sort cells based on their `z` index. - comparator: function(model) { + Object.defineProperty(closepathPrototype, 'end', { + // get a reference to the end point of subpath start segment - return model.get('z') || 0; - } -}); + configurable: true, + enumerable: true, -joint.dia.Graph = Backbone.Model.extend({ + get: function() { - _batches: {}, + if (!this.subpathStartSegment) throw new Error('Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)'); - initialize: function(attrs, opt) { + return this.subpathStartSegment.end; + } + }) - opt = opt || {}; + Closepath.prototype = extend(segmentPrototype, Line.prototype, closepathPrototype); - // Passing `cellModel` function in the options object to graph allows for - // setting models based on attribute objects. This is especially handy - // when processing JSON graphs that are in a different than JointJS format. - var cells = new joint.dia.GraphCells([], { - model: opt.cellModel, - cellNamespace: opt.cellNamespace, - graph: this - }); - Backbone.Model.prototype.set.call(this, 'cells', cells); + var segmentTypes = Path.segmentTypes = { + L: Lineto, + C: Curveto, + M: Moveto, + Z: Closepath, + z: Closepath + }; - // Make all the events fired in the `cells` collection available. - // to the outside world. - cells.on('all', this.trigger, this); + Path.regexSupportedData = new RegExp('^[\\s\\d' + Object.keys(segmentTypes).join('') + ',.]*$'); - // Backbone automatically doesn't trigger re-sort if models attributes are changed later when - // they're already in the collection. Therefore, we're triggering sort manually here. - this.on('change:z', this._sortOnChangeZ, this); - this.on('batch:stop', this._onBatchStop, this); + Path.isDataSupported = function(d) { + if (typeof d !== 'string') return false; + return this.regexSupportedData.test(d); + } - // `joint.dia.Graph` keeps an internal data structure (an adjacency list) - // for fast graph queries. All changes that affect the structure of the graph - // must be reflected in the `al` object. This object provides fast answers to - // questions such as "what are the neighbours of this node" or "what - // are the sibling links of this link". + return g; - // Outgoing edges per node. Note that we use a hash-table for the list - // of outgoing edges for a faster lookup. - // [node ID] -> Object [edge] -> true - this._out = {}; - // Ingoing edges per node. - // [node ID] -> Object [edge] -> true - this._in = {}; - // `_nodes` is useful for quick lookup of all the elements in the graph, without - // having to go through the whole cells array. - // [node ID] -> true - this._nodes = {}; - // `_edges` is useful for quick lookup of all the links in the graph, without - // having to go through the whole cells array. - // [edge ID] -> true - this._edges = {}; +})(); - cells.on('add', this._restructureOnAdd, this); - cells.on('remove', this._restructureOnRemove, this); - cells.on('reset', this._restructureOnReset, this); - cells.on('change:source', this._restructureOnChangeSource, this); - cells.on('change:target', this._restructureOnChangeTarget, this); - cells.on('remove', this._removeCell, this); - }, +// Vectorizer. +// ----------- - _sortOnChangeZ: function() { +// A tiny library for making your life easier when dealing with SVG. +// The only Vectorizer dependency is the Geometry library. - if (!this.hasActiveBatch('to-front') && !this.hasActiveBatch('to-back')) { - this.get('cells').sort(); - } - }, +var V; +var Vectorizer; - _onBatchStop: function(data) { +V = Vectorizer = (function() { - var batchName = data && data.batchName; - if ((batchName === 'to-front' || batchName === 'to-back') && !this.hasActiveBatch(batchName)) { - this.get('cells').sort(); - } - }, + 'use strict'; - _restructureOnAdd: function(cell) { + var hasSvg = typeof window === 'object' && + !!( + window.SVGAngle || + document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') + ); - if (cell.isLink()) { - this._edges[cell.id] = true; - var source = cell.get('source'); - var target = cell.get('target'); - if (source.id) { - (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; - } - if (target.id) { - (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; - } - } else { - this._nodes[cell.id] = true; - } - }, + // SVG support is required. + if (!hasSvg) { - _restructureOnRemove: function(cell) { + // Return a function that throws an error when it is used. + return function() { + throw new Error('SVG is required to use Vectorizer.'); + }; + } - if (cell.isLink()) { - delete this._edges[cell.id]; - var source = cell.get('source'); - var target = cell.get('target'); - if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { - delete this._out[source.id][cell.id]; - } - if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { - delete this._in[target.id][cell.id]; - } - } else { - delete this._nodes[cell.id]; - } - }, + // 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', + xhtml: 'http://www.w3.org/1999/xhtml' + }; - _restructureOnReset: function(cells) { + var SVGversion = '1.1'; - // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. - cells = cells.models; + // Declare shorthands to the most used math functions. + var math = Math; + var PI = math.PI; + var atan2 = math.atan2; + var sqrt = math.sqrt; + var min = math.min; + var max = math.max; + var cos = math.cos; + var sin = math.sin; - this._out = {}; - this._in = {}; - this._nodes = {}; - this._edges = {}; + var V = function(el, attrs, children) { - cells.forEach(this._restructureOnAdd, this); - }, + // This allows using V() without the new keyword. + if (!(this instanceof V)) { + return V.apply(Object.create(V.prototype), arguments); + } - _restructureOnChangeSource: function(link) { + if (!el) return; - var prevSource = link.previous('source'); - if (prevSource.id && this._out[prevSource.id]) { - delete this._out[prevSource.id][link.id]; - } - var source = link.get('source'); - if (source.id) { - (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; + if (V.isV(el)) { + el = el.node; } - }, - _restructureOnChangeTarget: function(link) { + attrs = attrs || {}; - var prevTarget = link.previous('target'); - if (prevTarget.id && this._in[prevTarget.id]) { - delete this._in[prevTarget.id][link.id]; - } - var target = link.get('target'); - if (target.id) { - (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; - } - }, + if (V.isString(el)) { - // Return all outbound edges for the node. Return value is an object - // of the form: [edge] -> true - getOutboundEdges: function(node) { + if (el.toLowerCase() === 'svg') { - return (this._out && this._out[node]) || {}; - }, + // Create a new SVG canvas. + el = V.createSvgDocument(); - // Return all inbound edges for the node. Return value is an object - // of the form: [edge] -> true - getInboundEdges: function(node) { + } else if (el[0] === '<') { - return (this._in && this._in[node]) || {}; - }, + // Create element from an SVG string. + // Allows constructs of type: `document.appendChild(V('').node)`. - toJSON: function() { + var svgDoc = V.createSvgDocument(el); - // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. - // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. - var json = Backbone.Model.prototype.toJSON.apply(this, arguments); - json.cells = this.get('cells').toJSON(); - return json; - }, + // 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) { - fromJSON: function(json, opt) { + // Map child nodes to `V`s. + var arrayOfVels = []; + var i, len; - if (!json.cells) { + for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { - throw new Error('Graph JSON must contain cells array.'); - } + var childNode = svgDoc.childNodes[i]; + arrayOfVels.push(new V(document.importNode(childNode, true))); + } - return this.set(json, opt); - }, + return arrayOfVels; + } - set: function(key, val, opt) { + el = document.importNode(svgDoc.firstChild, true); - var attrs; + } else { - // Handle both `key`, value and {key: value} style arguments. - if (typeof key === 'object') { - attrs = key; - opt = val; - } else { - (attrs = {})[key] = val; - } + el = document.createElementNS(ns.xmlns, el); + } - // Make sure that `cells` attribute is handled separately via resetCells(). - if (attrs.hasOwnProperty('cells')) { - this.resetCells(attrs.cells, opt); - attrs = joint.util.omit(attrs, 'cells'); + V.ensureId(el); } - // The rest of the attributes are applied via original set method. - return Backbone.Model.prototype.set.call(this, attrs, opt); - }, - - clear: function(opt) { + this.node = el; - opt = joint.util.assign({}, opt, { clear: true }); + this.setAttributes(attrs); - var collection = this.get('cells'); + if (children) { + this.append(children); + } - if (collection.length === 0) return this; + return this; + }; - this.startBatch('clear', opt); + var VPrototype = V.prototype; - // The elements come after the links. - var cells = collection.sortBy(function(cell) { - return cell.isLink() ? 1 : 2; - }); + Object.defineProperty(VPrototype, 'id', { + enumerable: true, + get: function() { + return this.node.id; + }, + set: function(id) { + this.node.id = id; + } + }); - do { + /** + * @param {SVGGElement} toElem + * @returns {SVGMatrix} + */ + VPrototype.getTransformToElement = function(toElem) { + toElem = V.toNode(toElem); + return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); + }; - // Remove all the cells one by one. - // Note that all the links are removed first, so it's - // safe to remove the elements without removing the connected - // links first. - cells.shift().remove(opt); + /** + * @param {SVGMatrix} matrix + * @param {Object=} opt + * @returns {Vectorizer|SVGMatrix} Setter / Getter + */ + VPrototype.transform = function(matrix, opt) { - } while (cells.length > 0); + var node = this.node; + if (V.isUndefined(matrix)) { + return V.transformStringToMatrix(this.attr('transform')); + } - this.stopBatch('clear'); + if (opt && opt.absolute) { + return this.attr('transform', V.matrixToTransformString(matrix)); + } + var svgTransform = V.createSVGTransform(matrix); + node.transform.baseVal.appendItem(svgTransform); return this; - }, + }; - _prepareCell: function(cell, opt) { + VPrototype.translate = function(tx, ty, opt) { - var attrs; - if (cell instanceof Backbone.Model) { - attrs = cell.attributes; - if (!cell.graph && (!opt || !opt.dry)) { - // An element can not be member of more than one graph. - // A cell stops being the member of the graph after it's explicitely removed. - cell.graph = this; - } - } else { - // In case we're dealing with a plain JS object, we have to set the reference - // to the `graph` right after the actual model is created. This happens in the `model()` function - // of `joint.dia.GraphCells`. - attrs = cell; - } + opt = opt || {}; + ty = ty || 0; - if (!joint.util.isString(attrs.type)) { - throw new TypeError('dia.Graph: cell type must be a string.'); + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; + // Is it a getter? + if (V.isUndefined(tx)) { + return transform.translate; } - return cell; - }, - - maxZIndex: function() { + transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); - var lastCell = this.get('cells').last(); - return lastCell ? (lastCell.get('z') || 0) : 0; - }, + var newTx = opt.absolute ? tx : transform.translate.tx + tx; + var newTy = opt.absolute ? ty : transform.translate.ty + ty; + var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; - addCell: function(cell, opt) { + // Note that `translate()` is always the first transformation. This is + // usually the desired case. + this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); + return this; + }; - if (Array.isArray(cell)) { + VPrototype.rotate = function(angle, cx, cy, opt) { - return this.addCells(cell, opt); - } + opt = opt || {}; - if (cell instanceof Backbone.Model) { + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; - if (!cell.has('z')) { - cell.set('z', this.maxZIndex() + 1); - } + // Is it a getter? + if (V.isUndefined(angle)) { + return transform.rotate; + } - } else if (cell.z === undefined) { + transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); - cell.z = this.maxZIndex() + 1; - } + angle %= 360; - this.get('cells').add(this._prepareCell(cell, opt), opt || {}); + 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; - }, + }; - addCells: function(cells, opt) { + // Note that `scale` as the only transformation does not combine with previous values. + VPrototype.scale = function(sx, sy) { - if (cells.length) { + sy = V.isUndefined(sy) ? sx : sy; - cells = joint.util.flattenDeep(cells); - opt.position = cells.length; + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; - this.startBatch('add'); - cells.forEach(function(cell) { - opt.position--; - this.addCell(cell, opt); - }, this); - this.stopBatch('add'); + // 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; - }, + }; - // When adding a lot of cells, it is much more efficient to - // reset the entire cells collection in one go. - // Useful for bulk operations and optimizations. - resetCells: function(cells, opt) { + // 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. + VPrototype.bbox = function(withoutTransformations, target) { - var preparedCells = joint.util.toArray(cells).map(function(cell) { - return this._prepareCell(cell, opt); - }, this); - this.get('cells').reset(preparedCells, opt); + var box; + var node = this.node; + var ownerSVGElement = node.ownerSVGElement; - return this; - }, + // 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 new g.Rect(0, 0, 0, 0); + } - removeCells: function(cells, opt) { + try { - if (cells.length) { + box = node.getBBox(); - this.startBatch('remove'); - joint.util.invoke(cells, 'remove', opt); - this.stopBatch('remove'); + } catch (e) { + + // Fallback for IE. + box = { + x: node.clientLeft, + y: node.clientTop, + width: node.clientWidth, + height: node.clientHeight + }; } - return this; - }, + if (withoutTransformations) { + return new g.Rect(box); + } - _removeCell: function(cell, collection, options) { + var matrix = this.getTransformToElement(target || ownerSVGElement); - options = options || {}; + return V.transformRect(box, matrix); + }; - if (!options.clear) { - // Applications might provide a `disconnectLinks` option set to `true` in order to - // disconnect links when a cell is removed rather then removing them. The default - // is to remove all the associated links. - if (options.disconnectLinks) { + // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, + // i.e. after transformations are applied. + // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. + // Takes an (Object) `opt` argument (optional) with the following attributes: + // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this + // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); + VPrototype.getBBox = function(opt) { - this.disconnectLinks(cell, options); + var options = {}; - } else { + var outputBBox; + var node = this.node; + var ownerSVGElement = node.ownerSVGElement; - this.removeLinks(cell, options); - } + // 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 new g.Rect(0, 0, 0, 0); } - // Silently remove the cell from the cells collection. Silently, because - // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is - // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events - // would be triggered on the graph model. - this.get('cells').remove(cell, { silent: true }); - if (cell.graph === this) { - // Remove the element graph reference only if the cell is the member of this graph. - cell.graph = null; + if (opt) { + if (opt.target) { // check if target exists + options.target = V.toNode(opt.target); // works for V objects, jquery objects, and node objects + } + if (opt.recursive) { + options.recursive = opt.recursive; + } } - }, - - // Get a cell by `id`. - getCell: function(id) { - return this.get('cells').get(id); - }, + if (!options.recursive) { + try { + outputBBox = node.getBBox(); + } catch (e) { + // Fallback for IE. + outputBBox = { + x: node.clientLeft, + y: node.clientTop, + width: node.clientWidth, + height: node.clientHeight + }; + } - getCells: function() { + if (!options.target) { + // transform like this (that is, not at all) + return new g.Rect(outputBBox); + } else { + // transform like target + var matrix = this.getTransformToElement(options.target); + return V.transformRect(outputBBox, matrix); + } + } else { // if we want to calculate the bbox recursively + // browsers report correct bbox around svg elements (one that envelops the path lines tightly) + // but some browsers fail to report the same bbox when the elements are in a group (returning a looser bbox that also includes control points, like node.getClientRect()) + // this happens even if we wrap a single svg element into a group! + // this option setting makes the function recursively enter all the groups from this and deeper, get bboxes of the elements inside, then return a union of those bboxes - return this.get('cells').toArray(); - }, + var children = this.children(); + var n = children.length; - getElements: function() { - return Object.keys(this._nodes).map(this.getCell, this); - }, + if (n === 0) { + return this.getBBox({ target: options.target, recursive: false }); + } - getLinks: function() { - return Object.keys(this._edges).map(this.getCell, this); - }, + // recursion's initial pass-through setting: + // recursive passes-through just keep the target as whatever was set up here during the initial pass-through + if (!options.target) { + // transform children/descendants like this (their parent/ancestor) + options.target = this; + } // else transform children/descendants like target - getFirstCell: function() { + for (var i = 0; i < n; i++) { + var currentChild = children[i]; - return this.get('cells').first(); - }, + var childBBox; - getLastCell: function() { + // if currentChild is not a group element, get its bbox with a nonrecursive call + if (currentChild.children().length === 0) { + childBBox = currentChild.getBBox({ target: options.target, recursive: false }); + } + else { + // if currentChild is a group element (determined by checking the number of children), enter it with a recursive call + childBBox = currentChild.getBBox({ target: options.target, recursive: true }); + } - return this.get('cells').last(); - }, + if (!outputBBox) { + // if this is the first iteration + outputBBox = childBBox; + } else { + // make a new bounding box rectangle that contains this child's bounding box and previous bounding box + outputBBox = outputBBox.union(childBBox); + } + } - // Get all inbound and outbound links connected to the cell `model`. - getConnectedLinks: function(model, opt) { + return outputBBox; + } + }; - opt = opt || {}; + // Text() helpers - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; + function createTextPathNode(attrs, vel) { + attrs || (attrs = {}); + var textPathElement = V('textPath'); + var d = attrs.d; + if (d && attrs['xlink:href'] === undefined) { + // If `opt.attrs` 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. + // Wrap the text in the SVG element that points + // to a path defined by `opt.attrs` inside the `` element. + var linkedPath = V('path').attr('d', d).appendTo(vel.defs()); + textPathElement.attr('xlink:href', '#' + linkedPath.id); } + if (V.isObject(attrs)) { + // 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. + textPathElement.attr(attrs); + } + return textPathElement.node; + } - // The final array of connected link models. - var links = []; - // Connected edges. This hash table ([edge] -> true) serves only - // for a quick lookup to check if we already added a link. - var edges = {}; + function annotateTextLine(lineNode, lineAnnotations, opt) { + opt || (opt = {}); + var includeAnnotationIndices = opt.includeAnnotationIndices; + var eol = opt.eol; + var lineHeight = opt.lineHeight; + var baseSize = opt.baseSize; + var maxFontSize = 0; + var fontMetrics = {}; + var lastJ = lineAnnotations.length - 1; + for (var j = 0; j <= lastJ; j++) { + var annotation = lineAnnotations[j]; + var fontSize = null; + if (V.isObject(annotation)) { + var annotationAttrs = annotation.attrs; + var vTSpan = V('tspan', annotationAttrs); + var tspanNode = vTSpan.node; + var t = annotation.t; + if (eol && j === lastJ) t += eol; + tspanNode.textContent = t; + // Per annotation className + var annotationClass = annotationAttrs['class']; + if (annotationClass) vTSpan.addClass(annotationClass); + // 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. + if (includeAnnotationIndices) vTSpan.attr('annotations', annotation.annotations); + // Check for max font size + fontSize = parseFloat(annotationAttrs['font-size']); + if (fontSize === undefined) fontSize = baseSize; + if (fontSize && fontSize > maxFontSize) maxFontSize = fontSize; + } else { + if (eol && j === lastJ) annotation += eol; + tspanNode = document.createTextNode(annotation || ' '); + if (baseSize && baseSize > maxFontSize) maxFontSize = baseSize; + } + lineNode.appendChild(tspanNode); + } - if (outbound) { - joint.util.forIn(this.getOutboundEdges(model.id), function(exists, edge) { - if (!edges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + if (maxFontSize) fontMetrics.maxFontSize = maxFontSize; + if (lineHeight) { + fontMetrics.lineHeight = lineHeight; + } else if (maxFontSize) { + fontMetrics.lineHeight = (maxFontSize * 1.2); } - if (inbound) { - joint.util.forIn(this.getInboundEdges(model.id), function(exists, edge) { - // Skip links that were already added. Those must be self-loop links - // because they are both inbound and outbond edges of the same element. - if (!edges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + return fontMetrics; + } + + var emRegex = /em$/; + + function convertEmToPx(em, fontSize) { + var numerical = parseFloat(em); + if (emRegex.test(em)) return numerical * fontSize; + return numerical; + } + + function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) { + if (!Array.isArray(linesMetrics)) return 0; + var n = linesMetrics.length; + if (!n) return 0; + var lineMetrics = linesMetrics[0]; + var flMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var rLineHeights = 0; + var lineHeightPx = convertEmToPx(lineHeight, baseSizePx); + for (var i = 1; i < n; i++) { + lineMetrics = linesMetrics[i]; + var iLineHeight = convertEmToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx; + rLineHeights += iLineHeight; + } + var llMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var dy; + switch (alignment) { + case 'middle': + dy = (flMaxFont / 2) - (0.15 * llMaxFont) - (rLineHeights / 2); + break; + case 'bottom': + dy = -(0.25 * llMaxFont) - rLineHeights; + break; + default: + case 'top': + dy = (0.8 * flMaxFont) + break; } + return dy; + } - // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells - // and are not descendents themselves. - if (opt.deep) { + VPrototype.text = function(content, opt) { - var embeddedCells = model.getEmbeddedCells({ deep: true }); - // In the first round, we collect all the embedded edges so that we can exclude - // them from the final result. - var embeddedEdges = {}; - embeddedCells.forEach(function(cell) { - if (cell.isLink()) { - embeddedEdges[cell.id] = true; + if (content && typeof content !== 'string') throw new Error('Vectorizer: text() expects the first argument to be a string.'); + + // 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 = {}); + + // End of Line character + var eol = opt.eol; + // Text along path + var textPath = opt.textPath + // Vertical shift + var verticalAnchor = opt.textVerticalAnchor; + var namedVerticalAnchor = (verticalAnchor === 'middle' || verticalAnchor === 'bottom' || verticalAnchor === 'top'); + // Horizontal shift applied to all the lines but the first. + var x = opt.x; + if (x === undefined) x = this.attr('x') || 0; + // Annotations + var iai = opt.includeAnnotationIndices; + var annotations = opt.annotations; + if (annotations && !V.isArray(annotations)) annotations = [annotations]; + // Shift all the but first by one line (`1em`) + var defaultLineHeight = opt.lineHeight; + var autoLineHeight = (defaultLineHeight === 'auto'); + var lineHeight = (autoLineHeight) ? '1.5em' : (defaultLineHeight || '1em'); + // Clearing the element + this.empty(); + this.attr({ + // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. + 'xml:space': 'preserve', + // 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. + 'display': (content) ? null : 'none' + }); + // Set default font-size if none + var fontSize = parseFloat(this.attr('font-size')); + if (!fontSize) { + fontSize = 16; + if (namedVerticalAnchor || annotations) this.attr('font-size', fontSize); + } + + var doc = document; + var containerNode; + if (textPath) { + // Now all the ``s will be inside the ``. + if (typeof textPath === 'string') textPath = { d: textPath }; + containerNode = createTextPathNode(textPath, this); + } else { + containerNode = doc.createDocumentFragment(); + } + var offset = 0; + var lines = content.split('\n'); + var linesMetrics = []; + var annotatedY; + for (var i = 0, lastI = lines.length - 1; i <= lastI; i++) { + var dy = lineHeight; + var lineClassName = 'v-line'; + var lineNode = doc.createElementNS(V.namespace.xmlns, 'tspan'); + var line = lines[i]; + var lineMetrics; + if (line) { + if (annotations) { + // Find the *compacted* annotations for this line. + var lineAnnotations = V.annotateString(line, annotations, { + offset: -offset, + includeAnnotationIndices: iai + }); + lineMetrics = annotateTextLine(lineNode, lineAnnotations, { + includeAnnotationIndices: iai, + eol: (i !== lastI && eol), + lineHeight: (autoLineHeight) ? null : lineHeight, + baseSize: fontSize + }); + // Get the line height based on the biggest font size in the annotations for this line. + var iLineHeight = lineMetrics.lineHeight; + if (iLineHeight && autoLineHeight && i !== 0) dy = iLineHeight; + if (i === 0) annotatedY = lineMetrics.maxFontSize * 0.8; + } else { + if (eol && i !== lastI) line += eol; + lineNode.textContent = line; } - }); - embeddedCells.forEach(function(cell) { - if (cell.isLink()) return; - if (outbound) { - joint.util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { - if (!edges[edge] && !embeddedEdges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + } 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. + lineNode.textContent = '-'; + lineClassName += ' v-empty-line'; + // 'opacity' needs to be specified with fill, stroke. Opacity without specification + // is not applied in Firefox + var lineNodeStyle = lineNode.style; + lineNodeStyle.fillOpacity = 0; + lineNodeStyle.strokeOpacity = 0; + if (annotations) lineMetrics = {}; + } + if (lineMetrics) linesMetrics.push(lineMetrics); + if (i > 0) lineNode.setAttribute('dy', dy); + // Firefox requires 'x' to be set on the first line when inside a text path + if (i > 0 || textPath) lineNode.setAttribute('x', x); + lineNode.className.baseVal = lineClassName; + containerNode.appendChild(lineNode); + offset += line.length + 1; // + 1 = newline character. + } + // Y Alignment calculation + if (namedVerticalAnchor) { + if (annotations) { + dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight); + } else if (verticalAnchor === 'top') { + // A shortcut for top alignment. It does not depend on font-size nor line-height + dy = '0.8em'; + } else { + var rh; // remaining height + if (lastI > 0) { + rh = parseFloat(lineHeight) || 1; + rh *= lastI; + if (!emRegex.test(lineHeight)) rh /= fontSize; + } else { + // Single-line text + rh = 0; } - if (inbound) { - joint.util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { - if (!edges[edge] && !embeddedEdges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + switch (verticalAnchor) { + case 'middle': + dy = (0.3 - (rh / 2)) + 'em' + break; + case 'bottom': + dy = (-rh - 0.3) + 'em' + break; } - }, this); + } + } else { + if (verticalAnchor === 0) { + dy = '0em'; + } else if (verticalAnchor) { + dy = verticalAnchor; + } else { + // No vertical anchor is defined + dy = 0; + // Backwards compatibility - we change the `y` attribute instead of `dy`. + if (this.attr('y') === null) this.attr('y', annotatedY || '0.8em'); + } } + containerNode.firstChild.setAttribute('dy', dy); + // Appending lines to the element. + this.append(containerNode); + return this; + }; - return links; - }, - - getNeighbors: function(model, opt) { + /** + * @public + * @param {string} name + * @returns {Vectorizer} + */ + VPrototype.removeAttr = function(name) { - opt = opt || {}; + var qualifiedName = V.qualifyAttr(name); + var el = this.node; - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; + 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; + }; - var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { - - var source = link.get('source'); - var target = link.get('target'); - var loop = link.hasLoop(opt); + VPrototype.attr = function(name, value) { - // Discard if it is a point, or if the neighbor was already added. - if (inbound && joint.util.has(source, 'id') && !res[source.id]) { + if (V.isUndefined(name)) { - var sourceElement = this.getCell(source.id); + // Return all attributes. + var attributes = this.node.attributes; + var attrs = {}; - if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { - res[source.id] = sourceElement; - } + for (var i = 0; i < attributes.length; i++) { + attrs[attributes[i].name] = attributes[i].value; } - // Discard if it is a point, or if the neighbor was already added. - if (outbound && joint.util.has(target, 'id') && !res[target.id]) { + return attrs; + } - var targetElement = this.getCell(target.id); + if (V.isString(name) && V.isUndefined(value)) { + return this.node.getAttribute(name); + } - if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { - res[target.id] = targetElement; + if (typeof name === 'object') { + + for (var attrName in name) { + if (name.hasOwnProperty(attrName)) { + this.setAttribute(attrName, name[attrName]); } } - return res; - }.bind(this), {}); + } else { - return joint.util.toArray(neighbors); - }, + this.setAttribute(name, value); + } - getCommonAncestor: function(/* cells */) { + return this; + }; - var cellsAncestors = Array.from(arguments).map(function(cell) { + VPrototype.normalizePath = function() { - var ancestors = []; - var parentId = cell.get('parent'); + var tagName = this.tagName(); + if (tagName === 'PATH') { + this.attr('d', V.normalizePathData(this.attr('d'))); + } - while (parentId) { + return this; + } - ancestors.push(parentId); - parentId = this.getCell(parentId).get('parent'); - } + VPrototype.remove = function() { - return ancestors; + if (this.node.parentNode) { + this.node.parentNode.removeChild(this.node); + } - }, this); + return this; + }; - cellsAncestors = cellsAncestors.sort(function(a, b) { - return a.length - b.length; - }); + VPrototype.empty = function() { - var commonAncestor = joint.util.toArray(cellsAncestors.shift()).find(function(ancestor) { - return cellsAncestors.every(function(cellAncestors) { - return cellAncestors.includes(ancestor); - }); - }); + while (this.node.firstChild) { + this.node.removeChild(this.node.firstChild); + } - return this.getCell(commonAncestor); - }, + return this; + }; - // Find the whole branch starting at `element`. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. - getSuccessors: function(element, opt) { + /** + * @private + * @param {object} attrs + * @returns {Vectorizer} + */ + VPrototype.setAttributes = function(attrs) { - opt = opt || {}; - var res = []; - // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. - this.search(element, function(el) { - if (el !== element) { - res.push(el); + for (var key in attrs) { + if (attrs.hasOwnProperty(key)) { + this.setAttribute(key, attrs[key]); } - }, joint.util.assign({}, opt, { outbound: true })); - return res; - }, + } - // Clone `cells` returning an object that maps the original cell ID to the clone. The number - // of clones is exactly the same as the `cells.length`. - // This function simply clones all the `cells`. However, it also reconstructs - // all the `source/target` and `parent/embed` references within the `cells`. - // This is the main difference from the `cell.clone()` method. The - // `cell.clone()` method works on one single cell only. - // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` - // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. - // the source and target of the link `L2` is changed to point to `A2` and `B2`. - cloneCells: function(cells) { + return this; + }; - cells = joint.util.uniq(cells); + VPrototype.append = function(els) { - // A map of the form [original cell ID] -> [clone] helping - // us to reconstruct references for source/target and parent/embeds. - // This is also the returned value. - var cloneMap = joint.util.toArray(cells).reduce(function(map, cell) { - map[cell.id] = cell.clone(); - return map; - }, {}); + 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; + }; + + VPrototype.prepend = function(els) { + + var child = this.node.firstChild; + return child ? V(child).before(els) : this.append(els); + }; + + VPrototype.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; + }; + + VPrototype.appendTo = function(node) { + V.toNode(node).appendChild(this.node); + return this; + }, + + VPrototype.svg = function() { + + return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); + }; + + VPrototype.tagName = function() { + + return this.node.tagName.toUpperCase(); + }; + + VPrototype.defs = function() { + var context = this.svg() || this; + var defsNode = context.node.getElementsByTagName('defs')[0]; + if (defsNode) return V(defsNode); + return V('defs').appendTo(context); + }; + + VPrototype.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; + }; + + VPrototype.findOne = function(selector) { + + var found = this.node.querySelector(selector); + return found ? V(found) : undefined; + }; + + VPrototype.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; + }; + + // Returns an array of V elements made from children of this.node. + VPrototype.children = function() { + + var children = this.node.childNodes; + + var outputArray = []; + for (var i = 0; i < children.length; i++) { + var currentChild = children[i]; + if (currentChild.nodeType === 1) { + outputArray.push(V(children[i])); + } + } + return outputArray; + }; + + // Find an index of an element inside its container. + VPrototype.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; + }; + + VPrototype.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 + VPrototype.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. + VPrototype.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); + }; + + VPrototype.translateCenterToPoint = function(p) { + + var bbox = this.getBBox({ target: this.svg() }); + var center = bbox.center(); + + this.translate(p.x - center.x, p.y - center.y); + return this; + }; + + // 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. + VPrototype.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.getBBox({ target: target || svg }); + + // 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 = (new 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 = (new 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 || svg); + 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; + }; + + VPrototype.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(); + } + } + } + return this; + }; + + VPrototype.hasClass = function(className) { + + return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); + }; + + VPrototype.addClass = function(className) { + + if (!this.hasClass(className)) { + var prevClasses = this.node.getAttribute('class') || ''; + this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); + } + + return this; + }; + + VPrototype.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; + }; + + VPrototype.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). + VPrototype.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; + }; + + VPrototype.convertToPath = function() { + + var path = V('path'); + path.attr(this.attr()); + var d = this.convertToPathData(); + if (d) { + path.attr('d', d); + } + return path; + }; + + VPrototype.convertToPathData = function() { + + var tagName = this.tagName(); + + 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.'); + }; + + V.prototype.toGeometryShape = function() { + var x, y, width, height, cx, cy, r, rx, ry, points, d; + switch (this.tagName()) { + + case 'RECT': + x = parseFloat(this.attr('x')) || 0; + y = parseFloat(this.attr('y')) || 0; + width = parseFloat(this.attr('width')) || 0; + height = parseFloat(this.attr('height')) || 0; + return new g.Rect(x, y, width, height); + + case 'CIRCLE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + r = parseFloat(this.attr('r')) || 0; + return new g.Ellipse({ x: cx, y: cy }, r, r); + + case 'ELLIPSE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + rx = parseFloat(this.attr('rx')) || 0; + ry = parseFloat(this.attr('ry')) || 0; + return new g.Ellipse({ x: cx, y: cy }, rx, ry); + + case 'POLYLINE': + points = V.getPointsFromSvgNode(this); + return new g.Polyline(points); + + case 'POLYGON': + points = V.getPointsFromSvgNode(this); + if (points.length > 1) points.push(points[0]); + return new g.Polyline(points); + + case 'PATH': + d = this.attr('d'); + if (!g.Path.isDataSupported(d)) d = V.normalizePathData(d); + return new g.Path(d); + + case 'LINE': + x1 = parseFloat(this.attr('x1')) || 0; + y1 = parseFloat(this.attr('y1')) || 0; + x2 = parseFloat(this.attr('x2')) || 0; + y2 = parseFloat(this.attr('y2')) || 0; + return new g.Line({ x: x1, y: y1 }, { x: x2, y: y2 }); + } + + // Anything else is a rectangle + return this.getBBox(); + }, + + // 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. + VPrototype.findIntersection = function(ref, target) { + + var svg = this.svg().node; + target = target || svg; + var bbox = this.getBBox({ target: target }); + var center = bbox.center(); + + if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; + + var spot; + var tagName = this.tagName(); + + // 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 = new 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 = (new 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 = new 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} + */ + VPrototype.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 `` root element. + V.createSvgDocument = function(content) { + + var svg = '' + (content || '') + ''; + var xml = V.parseXML(svg, { async: false }); + return xml.documentElement; + }; + + V.idCounter = 0; + + // A function returning a unique identifier for this client session with every call. + V.uniqueId = function() { + + return 'v-' + (++V.idCounter); + }; + + V.toNode = function(el) { + + return V.isV(el) ? el.node : (el.nodeName && el || el[0]); + }; + + V.ensureId = function(node) { + + node = V.toNode(node); + return node.id || (node.id = V.uniqueId()); + }; + + // 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. This is used in the text() method but it is + // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests + // when you want to compare the actual DOM text content without having to add the unicode character in + // the place of all spaces. + V.sanitizeText = function(text) { + + return (text || '').replace(/ /g, '\u00A0'); + }; + + V.isUndefined = function(value) { + + return typeof value === 'undefined'; + }; + + V.isString = function(value) { + + return typeof value === 'string'; + }; + + V.isObject = function(value) { + + return value && (typeof value === 'object'); + }; + + V.isArray = Array.isArray; + + V.parseXML = function(data, opt) { + + opt = opt || {}; + + var xml; + + try { + var parser = new DOMParser(); + + if (!V.isUndefined(opt.async)) { + parser.async = opt.async; + } + + xml = parser.parseFromString(data, 'text/xml'); + } catch (error) { + xml = undefined; + } + + if (!xml || xml.getElementsByTagName('parsererror').length) { + throw new Error('Invalid XML: ' + data); + } + + return xml; + }; + + /** + * @param {string} name + * @returns {{ns: string|null, local: string}} namespace and attribute name + */ + V.qualifyAttr = function(name) { + + if (name.indexOf(':') !== -1) { + var combinedKey = name.split(':'); + return { + ns: ns[combinedKey[0]], + local: combinedKey[1] + }; + } + + return { + ns: null, + local: name + }; + }; + + V.transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi; + V.transformSeparatorRegex = /[ ,]+/; + V.transformationListRegex = /^(\w+)\((.*)\)/; + + V.transformStringToMatrix = function(transform) { + + var transformationMatrix = V.createSVGMatrix(); + var matches = transform && transform.match(V.transformRegex); + if (!matches) { + return transformationMatrix; + } + + for (var i = 0, n = matches.length; i < n; i++) { + var transformationString = matches[i]; + + var transformationMatch = transformationString.match(V.transformationListRegex); + if (transformationMatch) { + var sx, sy, tx, ty, angle; + var ctm = V.createSVGMatrix(); + var args = transformationMatch[2].split(V.transformSeparatorRegex); + switch (transformationMatch[1].toLowerCase()) { + case 'scale': + sx = parseFloat(args[0]); + sy = (args[1] === undefined) ? sx : parseFloat(args[1]); + ctm = ctm.scaleNonUniform(sx, sy); + break; + case 'translate': + tx = parseFloat(args[0]); + ty = parseFloat(args[1]); + ctm = ctm.translate(tx, ty); + break; + case 'rotate': + angle = parseFloat(args[0]); + tx = parseFloat(args[1]) || 0; + ty = parseFloat(args[2]) || 0; + if (tx !== 0 || ty !== 0) { + ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty); + } else { + ctm = ctm.rotate(angle); + } + break; + case 'skewx': + angle = parseFloat(args[0]); + ctm = ctm.skewX(angle); + break; + case 'skewy': + angle = parseFloat(args[0]); + ctm = ctm.skewY(angle); + break; + case 'matrix': + ctm.a = parseFloat(args[0]); + ctm.b = parseFloat(args[1]); + ctm.c = parseFloat(args[2]); + ctm.d = parseFloat(args[3]); + ctm.e = parseFloat(args[4]); + ctm.f = parseFloat(args[5]); + break; + default: + continue; + } + + transformationMatrix = transformationMatrix.multiply(ctm); + } + + } + return transformationMatrix; + }; + + V.matrixToTransformString = function(matrix) { + matrix || (matrix = true); + + return 'matrix(' + + (matrix.a !== undefined ? matrix.a : 1) + ',' + + (matrix.b !== undefined ? matrix.b : 0) + ',' + + (matrix.c !== undefined ? matrix.c : 0) + ',' + + (matrix.d !== undefined ? matrix.d : 1) + ',' + + (matrix.e !== undefined ? matrix.e : 0) + ',' + + (matrix.f !== undefined ? matrix.f : 0) + + ')'; + }; + + V.parseTransformString = function(transform) { + + var translate, rotate, scale; + + if (transform) { + + var separator = V.transformSeparatorRegex; + + // Allow reading transform string with a single matrix + if (transform.trim().indexOf('matrix') >= 0) { + + var matrix = V.transformStringToMatrix(transform); + var decomposedMatrix = V.decomposeMatrix(matrix); + + translate = [decomposedMatrix.translateX, decomposedMatrix.translateY]; + scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]; + rotate = [decomposedMatrix.rotation]; + + var transformations = []; + if (translate[0] !== 0 || translate[0] !== 0) { + transformations.push('translate(' + translate + ')'); + } + if (scale[0] !== 1 || scale[1] !== 1) { + transformations.push('scale(' + scale + ')'); + } + if (rotate[0] !== 0) { + transformations.push('rotate(' + rotate + ')'); + } + transform = transformations.join(' '); + + } else { + + var translateMatch = transform.match(/translate\((.*?)\)/); + if (translateMatch) { + translate = translateMatch[1].split(separator); + } + var rotateMatch = transform.match(/rotate\((.*?)\)/); + if (rotateMatch) { + rotate = rotateMatch[1].split(separator); + } + var scaleMatch = transform.match(/scale\((.*?)\)/); + if (scaleMatch) { + scale = scaleMatch[1].split(separator); + } + } + } + + var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; + + return { + value: transform, + translate: { + tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, + ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 + }, + rotate: { + angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, + cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, + cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined + }, + scale: { + sx: sx, + sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx + } + }; + }; + + V.deltaTransformPoint = function(matrix, point) { + + var dx = point.x * matrix.a + point.y * matrix.c + 0; + var dy = point.x * matrix.b + point.y * matrix.d + 0; + return { x: dx, y: dy }; + }; + + V.decomposeMatrix = function(matrix) { + + // @see https://gist.github.com/2052247 + + // calculate delta transform point + var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); + var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); + + // calculate skew + var skewX = ((180 / PI) * atan2(px.y, px.x) - 90); + var skewY = ((180 / PI) * atan2(py.y, py.x)); + + return { + + translateX: matrix.e, + translateY: matrix.f, + scaleX: sqrt(matrix.a * matrix.a + matrix.b * matrix.b), + scaleY: sqrt(matrix.c * matrix.c + matrix.d * matrix.d), + skewX: skewX, + skewY: skewY, + rotation: skewX // rotation is the same as skew x + }; + }; + + // Return the `scale` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToScale = function(matrix) { + + var a,b,c,d; + if (matrix) { + a = V.isUndefined(matrix.a) ? 1 : matrix.a; + d = V.isUndefined(matrix.d) ? 1 : matrix.d; + b = matrix.b; + c = matrix.c; + } else { + a = d = 1; + } + return { + sx: b ? sqrt(a * a + b * b) : a, + sy: c ? sqrt(c * c + d * d) : d + }; + }, + + // Return the `rotate` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToRotate = function(matrix) { + + var p = { x: 0, y: 1 }; + if (matrix) { + p = V.deltaTransformPoint(matrix, p); + } + + return { + angle: g.normalizeAngle(g.toDeg(atan2(p.y, p.x)) - 90) + }; + }, + + // Return the `translate` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToTranslate = function(matrix) { + + return { + tx: (matrix && matrix.e) || 0, + ty: (matrix && matrix.f) || 0 + }; + }, + + V.isV = function(object) { + + return object instanceof V; + }; + + // For backwards compatibility: + V.isVElement = V.isV; + + var svgDocument = V('svg').node; + + V.createSVGMatrix = function(matrix) { + + var svgMatrix = svgDocument.createSVGMatrix(); + for (var component in matrix) { + svgMatrix[component] = matrix[component]; + } + + return svgMatrix; + }; + + V.createSVGTransform = function(matrix) { + + if (!V.isUndefined(matrix)) { + + if (!(matrix instanceof SVGMatrix)) { + matrix = V.createSVGMatrix(matrix); + } + + return svgDocument.createSVGTransformFromMatrix(matrix); + } + + return svgDocument.createSVGTransform(); + }; + + V.createSVGPoint = function(x, y) { + + var p = svgDocument.createSVGPoint(); + p.x = x; + p.y = y; + return p; + }; + + V.transformRect = function(r, matrix) { + + var p = svgDocument.createSVGPoint(); + + p.x = r.x; + p.y = r.y; + var corner1 = p.matrixTransform(matrix); + + p.x = r.x + r.width; + p.y = r.y; + var corner2 = p.matrixTransform(matrix); + + p.x = r.x + r.width; + p.y = r.y + r.height; + var corner3 = p.matrixTransform(matrix); + + p.x = r.x; + p.y = r.y + r.height; + var corner4 = p.matrixTransform(matrix); + + var minX = min(corner1.x, corner2.x, corner3.x, corner4.x); + var maxX = max(corner1.x, corner2.x, corner3.x, corner4.x); + var minY = min(corner1.y, corner2.y, corner3.y, corner4.y); + var maxY = max(corner1.y, corner2.y, corner3.y, corner4.y); + + return new g.Rect(minX, minY, maxX - minX, maxY - minY); + }; + + V.transformPoint = function(p, matrix) { + + return new g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); + }; + + V.transformLine = function(l, matrix) { + + return new g.Line( + V.transformPoint(l.start, matrix), + V.transformPoint(l.end, matrix) + ); + }; + + V.transformPolyline = function(p, matrix) { + + var inPoints = (p instanceof g.Polyline) ? p.points : p; + if (!V.isArray(inPoints)) inPoints = []; + var outPoints = []; + for (var i = 0, n = inPoints.length; i < n; i++) outPoints[i] = V.transformPoint(inPoints[i], matrix); + return new g.Polyline(outPoints); + }, + + // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to + // an object (`{ fill: 'blue', stroke: 'red' }`). + V.styleToObject = function(styleString) { + var ret = {}; + var styles = styleString.split(';'); + for (var i = 0; i < styles.length; i++) { + var style = styles[i]; + var pair = style.split('='); + ret[pair[0].trim()] = pair[1].trim(); + } + return ret; + }; + + // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js + V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { + + var svgArcMax = 2 * PI - 1e-6; + var r0 = innerRadius; + var r1 = outerRadius; + var a0 = startAngle; + var a1 = endAngle; + var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); + var df = da < PI ? '0' : '1'; + var c0 = cos(a0); + var s0 = sin(a0); + var c1 = cos(a1); + var s1 = sin(a1); + + return (da >= svgArcMax) + ? (r0 + ? 'M0,' + r1 + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + + 'M0,' + r0 + + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) + + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 + + 'Z' + : 'M0,' + r1 + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + + 'Z') + : (r0 + ? 'M' + r1 * c0 + ',' + r1 * s0 + + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + + 'L' + r0 * c1 + ',' + r0 * s1 + + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 + + 'Z' + : 'M' + r1 * c0 + ',' + r1 * s0 + + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + + 'L0,0' + + 'Z'); + }; + + // Merge attributes from object `b` with attributes in object `a`. + // Note that this modifies the object `a`. + // Also important to note that attributes are merged but CSS classes are concatenated. + V.mergeAttrs = function(a, b) { + + for (var attr in b) { + + if (attr === 'class') { + // Concatenate classes. + a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; + } else if (attr === 'style') { + // `style` attribute can be an object. + if (V.isObject(a[attr]) && V.isObject(b[attr])) { + // `style` stored in `a` is an object. + a[attr] = V.mergeAttrs(a[attr], b[attr]); + } else if (V.isObject(a[attr])) { + // `style` in `a` is an object but it's a string in `b`. + // Convert the style represented as a string to an object in `b`. + a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); + } else if (V.isObject(b[attr])) { + // `style` in `a` is a string, in `b` it's an object. + a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); + } else { + // Both styles are strings. + a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); + } + } else { + a[attr] = b[attr]; + } + } + + return a; + }; + + V.annotateString = function(t, annotations, opt) { + + annotations = annotations || []; + opt = opt || {}; + + var offset = opt.offset || 0; + var compacted = []; + var batch; + var ret = []; + var item; + var prev; + + for (var i = 0; i < t.length; i++) { + + item = ret[i] = t[i]; + + for (var j = 0; j < annotations.length; j++) { + + var annotation = annotations[j]; + var start = annotation.start + offset; + var end = annotation.end + offset; + + if (i >= start && i < end) { + // Annotation applies. + if (V.isObject(item)) { + // There is more than one annotation to be applied => Merge attributes. + item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); + } else { + item = ret[i] = { t: t[i], attrs: annotation.attrs }; + } + if (opt.includeAnnotationIndices) { + (item.annotations || (item.annotations = [])).push(j); + } + } + } + + prev = ret[i - 1]; + + if (!prev) { + + batch = item; + + } else if (V.isObject(item) && V.isObject(prev)) { + // Both previous item and the current one are annotations. If the attributes + // didn't change, merge the text. + if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { + batch.t += item.t; + } else { + compacted.push(batch); + batch = item; + } + + } else if (V.isObject(item)) { + // Previous item was a string, current item is an annotation. + compacted.push(batch); + batch = item; + + } else if (V.isObject(prev)) { + // Previous item was an annotation, current item is a string. + compacted.push(batch); + batch = item; + + } else { + // Both previous and current item are strings. + batch = (batch || '') + item; + } + } + + if (batch) { + compacted.push(batch); + } + + return compacted; + }; + + V.findAnnotationsAtIndex = function(annotations, index) { + + var found = []; + + if (annotations) { + + annotations.forEach(function(annotation) { + + if (annotation.start < index && index <= annotation.end) { + found.push(annotation); + } + }); + } + + return found; + }; + + V.findAnnotationsBetweenIndexes = function(annotations, start, end) { + + var found = []; + + if (annotations) { + + annotations.forEach(function(annotation) { + + if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { + found.push(annotation); + } + }); + } + + return found; + }; + + // Shift all the text annotations after character `index` by `offset` positions. + V.shiftAnnotations = function(annotations, index, offset) { + + if (annotations) { + + annotations.forEach(function(annotation) { + + if (annotation.start < index && annotation.end >= index) { + annotation.end += offset; + } else if (annotation.start >= index) { + annotation.start += offset; + annotation.end += offset; + } + }); + } + + return annotations; + }; + + V.convertLineToPathData = function(line) { + + line = V(line); + var d = [ + 'M', line.attr('x1'), line.attr('y1'), + 'L', line.attr('x2'), line.attr('y2') + ].join(' '); + return d; + }; + + V.convertPolygonToPathData = function(polygon) { + + var points = V.getPointsFromSvgNode(polygon); + if (points.length === 0) return null; + + return V.svgPointsToPath(points) + ' Z'; + }; + + V.convertPolylineToPathData = function(polyline) { + + var points = V.getPointsFromSvgNode(polyline); + if (points.length === 0) return null; + + return V.svgPointsToPath(points); + }; + + V.svgPointsToPath = function(points) { + + for (var i = 0, n = points.length; i < n; i++) { + points[i] = points[i].x + ' ' + points[i].y; + } + + return 'M ' + points.join(' L'); + }; + + V.getPointsFromSvgNode = function(node) { + + node = V.toNode(node); + var points = []; + var nodePoints = node.points; + if (nodePoints) { + for (var i = 0, n = nodePoints.numberOfItems; i < n; i++) { + points.push(nodePoints.getItem(i)); + } + } + + return points; + }; + + V.KAPPA = 0.551784; + + V.convertCircleToPathData = function(circle) { + + circle = V(circle); + var cx = parseFloat(circle.attr('cx')) || 0; + var cy = parseFloat(circle.attr('cy')) || 0; + var r = parseFloat(circle.attr('r')); + var cd = r * V.KAPPA; // Control distance. + + var d = [ + 'M', cx, cy - r, // Move to the first point. + 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. + 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. + 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. + 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. + 'Z' + ].join(' '); + return d; + }; + + V.convertEllipseToPathData = function(ellipse) { + + ellipse = V(ellipse); + var cx = parseFloat(ellipse.attr('cx')) || 0; + var cy = parseFloat(ellipse.attr('cy')) || 0; + var rx = parseFloat(ellipse.attr('rx')); + var ry = parseFloat(ellipse.attr('ry')) || rx; + var cdx = rx * V.KAPPA; // Control distance x. + var cdy = ry * V.KAPPA; // Control distance y. + + var d = [ + 'M', cx, cy - ry, // Move to the first point. + 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. + 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. + 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. + 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. + 'Z' + ].join(' '); + return d; + }; + + V.convertRectToPathData = function(rect) { + + rect = V(rect); + + return V.rectToPath({ + x: parseFloat(rect.attr('x')) || 0, + y: parseFloat(rect.attr('y')) || 0, + width: parseFloat(rect.attr('width')) || 0, + height: parseFloat(rect.attr('height')) || 0, + rx: parseFloat(rect.attr('rx')) || 0, + ry: parseFloat(rect.attr('ry')) || 0 + }); + }; + + // Convert a rectangle to SVG path commands. `r` is an object of the form: + // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, + // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for + // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle + // that has only `rx` and `ry` attributes). + V.rectToPath = function(r) { + + var d; + var x = r.x; + var y = r.y; + var width = r.width; + var height = r.height; + var topRx = min(r.rx || r['top-rx'] || 0, width / 2); + var bottomRx = min(r.rx || r['bottom-rx'] || 0, width / 2); + var topRy = min(r.ry || r['top-ry'] || 0, height / 2); + var bottomRy = min(r.ry || r['bottom-ry'] || 0, height / 2); + + if (topRx || bottomRx || topRy || bottomRy) { + d = [ + 'M', x, y + topRy, + 'v', height - topRy - bottomRy, + 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, + 'h', width - 2 * bottomRx, + 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, + 'v', -(height - bottomRy - topRy), + 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, + 'h', -(width - 2 * topRx), + 'a', topRx, topRy, 0, 0, 0, -topRx, topRy, + 'Z' + ]; + } else { + d = [ + 'M', x, y, + 'H', x + width, + 'V', y + height, + 'H', x, + 'V', y, + 'Z' + ]; + } + + return d.join(' '); + }; + + // Take a path data string + // Return a normalized path data string + // If data cannot be parsed, return 'M 0 0' + // Adapted from Rappid normalizePath polyfill + // Highly inspired by Raphael Library (www.raphael.com) + V.normalizePathData = (function() { + + var spaces = '\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029'; + var pathCommand = new RegExp('([a-z])[' + spaces + ',]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?[' + spaces + ']*,?[' + spaces + ']*)+)', 'ig'); + var pathValues = new RegExp('(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)[' + spaces + ']*,?[' + spaces + ']*', 'ig'); + + var math = Math; + var PI = math.PI; + var sin = math.sin; + var cos = math.cos; + var tan = math.tan; + var asin = math.asin; + var sqrt = math.sqrt; + var abs = math.abs; + + function q2c(x1, y1, ax, ay, x2, y2) { + + var _13 = 1 / 3; + var _23 = 2 / 3; + return [(_13 * x1) + (_23 * ax), (_13 * y1) + (_23 * ay), (_13 * x2) + (_23 * ax), (_13 * y2) + (_23 * ay), x2, y2]; + } + + function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { + // for more information of where this math came from visit: + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + + var _120 = (PI * 120) / 180; + var rad = (PI / 180) * (+angle || 0); + var res = []; + var xy; + + var rotate = function(x, y, rad) { + + var X = (x * cos(rad)) - (y * sin(rad)); + var Y = (x * sin(rad)) + (y * cos(rad)); + return { x: X, y: Y }; + }; + + if (!recursive) { + xy = rotate(x1, y1, -rad); + x1 = xy.x; + y1 = xy.y; + + xy = rotate(x2, y2, -rad); + x2 = xy.x; + y2 = xy.y; + + var x = (x1 - x2) / 2; + var y = (y1 - y2) / 2; + var h = ((x * x) / (rx * rx)) + ((y * y) / (ry * ry)); + + if (h > 1) { + h = sqrt(h); + rx = h * rx; + ry = h * ry; + } + + var rx2 = rx * rx; + var ry2 = ry * ry; + + var k = ((large_arc_flag == sweep_flag) ? -1 : 1) * sqrt(abs(((rx2 * ry2) - (rx2 * y * y) - (ry2 * x * x)) / ((rx2 * y * y) + (ry2 * x * x)))); + + var cx = ((k * rx * y) / ry) + ((x1 + x2) / 2); + var cy = ((k * -ry * x) / rx) + ((y1 + y2) / 2); + + var f1 = asin(((y1 - cy) / ry).toFixed(9)); + var f2 = asin(((y2 - cy) / ry).toFixed(9)); + + f1 = ((x1 < cx) ? (PI - f1) : f1); + f2 = ((x2 < cx) ? (PI - f2) : f2); + + if (f1 < 0) f1 = (PI * 2) + f1; + if (f2 < 0) f2 = (PI * 2) + f2; + + if ((sweep_flag && f1) > f2) f1 = f1 - (PI * 2); + if ((!sweep_flag && f2) > f1) f2 = f2 - (PI * 2); + + } else { + f1 = recursive[0]; + f2 = recursive[1]; + cx = recursive[2]; + cy = recursive[3]; + } + + var df = f2 - f1; + + if (abs(df) > _120) { + var f2old = f2; + var x2old = x2; + var y2old = y2; + + f2 = f1 + (_120 * (((sweep_flag && f2) > f1) ? 1 : -1)); + x2 = cx + (rx * cos(f2)); + y2 = cy + (ry * sin(f2)); + + res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); + } + + df = f2 - f1; + + var c1 = cos(f1); + var s1 = sin(f1); + var c2 = cos(f2); + var s2 = sin(f2); + + var t = tan(df / 4); + + var hx = (4 / 3) * (rx * t); + var hy = (4 / 3) * (ry * t); + + var m1 = [x1, y1]; + var m2 = [x1 + (hx * s1), y1 - (hy * c1)]; + var m3 = [x2 + (hx * s2), y2 - (hy * c2)]; + var m4 = [x2, y2]; + + m2[0] = (2 * m1[0]) - m2[0]; + m2[1] = (2 * m1[1]) - m2[1]; + + if (recursive) { + return [m2, m3, m4].concat(res); + + } else { + res = [m2, m3, m4].concat(res).join().split(','); + + var newres = []; + var ii = res.length; + for (var i = 0; i < ii; i++) { + newres[i] = (i % 2) ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; + } + + return newres; + } + } + + function parsePathString(pathString) { + + if (!pathString) return null; + + var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 }; + var data = []; + + String(pathString).replace(pathCommand, function(a, b, c) { + + var params = []; + var name = b.toLowerCase(); + c.replace(pathValues, function(a, b) { + if (b) params.push(+b); + }); + + if ((name === 'm') && (params.length > 2)) { + data.push([b].concat(params.splice(0, 2))); + name = 'l'; + b = ((b === 'm') ? 'l' : 'L'); + } + + while (params.length >= paramCounts[name]) { + data.push([b].concat(params.splice(0, paramCounts[name]))); + if (!paramCounts[name]) break; + } + }); + + return data; + } + + function pathToAbsolute(pathArray) { + + if (!Array.isArray(pathArray) || !Array.isArray(pathArray && pathArray[0])) { // rough assumption + pathArray = parsePathString(pathArray); + } + + // if invalid string, return 'M 0 0' + if (!pathArray || !pathArray.length) return [['M', 0, 0]]; + + var res = []; + var x = 0; + var y = 0; + var mx = 0; + var my = 0; + var start = 0; + var pa0; + + var ii = pathArray.length; + for (var i = start; i < ii; i++) { + + var r = []; + res.push(r); + + var pa = pathArray[i]; + pa0 = pa[0]; + + if (pa0 != pa0.toUpperCase()) { + r[0] = pa0.toUpperCase(); + + var jj; + var j; + switch (r[0]) { + case 'A': + r[1] = pa[1]; + r[2] = pa[2]; + r[3] = pa[3]; + r[4] = pa[4]; + r[5] = pa[5]; + r[6] = +pa[6] + x; + r[7] = +pa[7] + y; + break; + + case 'V': + r[1] = +pa[1] + y; + break; + + case 'H': + r[1] = +pa[1] + x; + break; + + case 'M': + mx = +pa[1] + x; + my = +pa[2] + y; + + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + + default: + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + } + } else { + var kk = pa.length; + for (var k = 0; k < kk; k++) { + r[k] = pa[k]; + } + } + + switch (r[0]) { + case 'Z': + x = +mx; + y = +my; + break; + + case 'H': + x = r[1]; + break; + + case 'V': + y = r[1]; + break; + + case 'M': + mx = r[r.length - 2]; + my = r[r.length - 1]; + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + + default: + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + } + } + + return res; + } + + function normalize(path) { + + var p = pathToAbsolute(path); + var attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null }; + + function processPath(path, d, pcom) { + + var nx, ny; + + if (!path) return ['C', d.x, d.y, d.x, d.y, d.x, d.y]; + + if (!(path[0] in { T: 1, Q: 1 })) { + d.qx = null; + d.qy = null; + } + + switch (path[0]) { + case 'M': + d.X = path[1]; + d.Y = path[2]; + break; + + case 'A': + path = ['C'].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1)))); + break; + + case 'S': + if (pcom === 'C' || pcom === 'S') { // In 'S' case we have to take into account, if the previous command is C/S. + nx = (d.x * 2) - d.bx; // And reflect the previous + ny = (d.y * 2) - d.by; // command's control point relative to the current point. + } + else { // or some else or nothing + nx = d.x; + ny = d.y; + } + path = ['C', nx, ny].concat(path.slice(1)); + break; + + case 'T': + if (pcom === 'Q' || pcom === 'T') { // In 'T' case we have to take into account, if the previous command is Q/T. + d.qx = (d.x * 2) - d.qx; // And make a reflection similar + d.qy = (d.y * 2) - d.qy; // to case 'S'. + } + else { // or something else or nothing + d.qx = d.x; + d.qy = d.y; + } + path = ['C'].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); + break; + + case 'Q': + d.qx = path[1]; + d.qy = path[2]; + path = ['C'].concat(q2c(d.x, d.y, path[1], path[2], path[3], path[4])); + break; + + case 'H': + path = ['L'].concat(path[1], d.y); + break; + + case 'V': + path = ['L'].concat(d.x, path[1]); + break; + + // leave 'L' & 'Z' commands as they were: + + case 'L': + break; + + case 'Z': + break; + } + + return path; + } + + function fixArc(pp, i) { + + if (pp[i].length > 7) { + + pp[i].shift(); + var pi = pp[i]; + + while (pi.length) { + pcoms[i] = 'A'; // if created multiple 'C's, their original seg is saved + pp.splice(i++, 0, ['C'].concat(pi.splice(0, 6))); + } + + pp.splice(i, 1); + ii = p.length; + } + } + + var pcoms = []; // path commands of original path p + var pfirst = ''; // temporary holder for original path command + var pcom = ''; // holder for previous path command of original path + + var ii = p.length; + for (var i = 0; i < ii; i++) { + if (p[i]) pfirst = p[i][0]; // save current path command + + if (pfirst !== 'C') { // C is not saved yet, because it may be result of conversion + pcoms[i] = pfirst; // Save current path command + if (i > 0) pcom = pcoms[i - 1]; // Get previous path command pcom + } + + p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath + + if (pcoms[i] !== 'A' && pfirst === 'C') pcoms[i] = 'C'; // 'A' is the only command + // which may produce multiple 'C's + // so we have to make sure that 'C' is also 'C' in original path + + fixArc(p, i); // fixArc adds also the right amount of 'A's to pcoms + + var seg = p[i]; + var seglen = seg.length; + + attrs.x = seg[seglen - 2]; + attrs.y = seg[seglen - 1]; + + attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x; + attrs.by = parseFloat(seg[seglen - 3]) || attrs.y; + } + + // make sure normalized path data string starts with an M segment + if (!p[0][0] || p[0][0] !== 'M') { + p.unshift(['M', 0, 0]); + } + + return p; + } + + return function(pathData) { + return normalize(pathData).join(',').split(',').join(' '); + }; + })(); + + V.namespace = ns; + + return V; + +})(); + +// Global namespace. + +var joint = { + + version: '2.1.0', + + config: { + // The class name prefix config is for advanced use only. + // Be aware that if you change the prefix, the JointJS CSS will no longer function properly. + classNamePrefix: 'joint-', + defaultTheme: 'default' + }, + + // `joint.dia` namespace. + dia: {}, + + // `joint.ui` namespace. + ui: {}, + + // `joint.layout` namespace. + layout: {}, + + // `joint.shapes` namespace. + shapes: {}, + + // `joint.format` namespace. + format: {}, + + // `joint.connectors` namespace. + connectors: {}, + + // `joint.highlighters` namespace. + highlighters: {}, + + // `joint.routers` namespace. + routers: {}, + + // `joint.anchors` namespace. + anchors: {}, + + // `joint.connectionPoints` namespace. + connectionPoints: {}, + + // `joint.connectionStrategies` namespace. + connectionStrategies: {}, + + // `joint.linkTools` namespace. + linkTools: {}, + + // `joint.mvc` namespace. + mvc: { + views: {} + }, + + setTheme: function(theme, opt) { + + opt = opt || {}; + + joint.util.invoke(joint.mvc.views, 'setTheme', theme, opt); + + // Update the default theme on the view prototype. + joint.mvc.View.prototype.defaultTheme = theme; + }, + + // `joint.env` namespace. + env: { + + _results: {}, + + _tests: { + + svgforeignobject: function() { + return !!document.createElementNS && + /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); + } + }, + + addTest: function(name, fn) { + + return joint.env._tests[name] = fn; + }, + + test: function(name) { + + var fn = joint.env._tests[name]; + + if (!fn) { + throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); + } + + var result = joint.env._results[name]; + + if (typeof result !== 'undefined') { + return result; + } + + try { + result = fn(); + } catch (error) { + result = false; + } + + // Cache the test result. + joint.env._results[name] = result; + + return result; + } + }, + + util: { + + // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. + hashCode: function(str) { + + var hash = 0; + if (str.length == 0) return hash; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + hash = ((hash << 5) - hash) + c; + hash = hash & hash; // Convert to 32bit integer + } + return hash; + }, + + getByPath: function(obj, path, delim) { + + var keys = Array.isArray(path) ? path.slice() : path.split(delim || '/'); + var key; + + while (keys.length) { + key = keys.shift(); + if (Object(obj) === obj && key in obj) { + obj = obj[key]; + } else { + return undefined; + } + } + return obj; + }, + + setByPath: function(obj, path, value, delim) { + + var keys = Array.isArray(path) ? path : path.split(delim || '/'); + + var diver = obj; + var i = 0; + + for (var len = keys.length; i < len - 1; i++) { + // diver creates an empty object if there is no nested object under such a key. + // This means that one can populate an empty nested object with setByPath(). + diver = diver[keys[i]] || (diver[keys[i]] = {}); + } + diver[keys[len - 1]] = value; + + return obj; + }, + + unsetByPath: function(obj, path, delim) { + + delim = delim || '/'; + + var pathArray = Array.isArray(path) ? path.slice() : path.split(delim); + + var propertyToRemove = pathArray.pop(); + if (pathArray.length > 0) { + + // unsetting a nested attribute + var parent = joint.util.getByPath(obj, pathArray, delim); + + if (parent) { + delete parent[propertyToRemove]; + } + + } else { + + // unsetting a primitive attribute + delete obj[propertyToRemove]; + } + + return obj; + }, + + flattenObject: function(obj, delim, stop) { + + delim = delim || '/'; + var ret = {}; + + for (var key in obj) { + + if (!obj.hasOwnProperty(key)) continue; + + var shouldGoDeeper = typeof obj[key] === 'object'; + if (shouldGoDeeper && stop && stop(obj[key])) { + shouldGoDeeper = false; + } + + if (shouldGoDeeper) { + + var flatObject = this.flattenObject(obj[key], delim, stop); + + for (var flatKey in flatObject) { + if (!flatObject.hasOwnProperty(flatKey)) continue; + ret[key + delim + flatKey] = flatObject[flatKey]; + } + + } else { + + ret[key] = obj[key]; + } + } + + return ret; + }, + + uuid: function() { + + // credit: http://stackoverflow.com/posts/2117523/revisions + + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16|0; + var v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, + + // Generate global unique id for obj and store it as a property of the object. + guid: function(obj) { + + this.guid.id = this.guid.id || 1; + obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); + return obj.id; + }, + + toKebabCase: function(string) { + + return string.replace(/[A-Z]/g, '-$&').toLowerCase(); + }, + + // Copy all the properties to the first argument from the following arguments. + // All the properties will be overwritten by the properties from the following + // arguments. Inherited properties are ignored. + mixin: _.assign, + + // Copy all properties to the first argument from the following + // arguments only in case if they don't exists in the first argument. + // All the function propererties in the first argument will get + // additional property base pointing to the extenders same named + // property function's call method. + supplement: _.defaults, + + // Same as `mixin()` but deep version. + deepMixin: _.mixin, + + // Same as `supplement()` but deep version. + deepSupplement: _.defaultsDeep, + + normalizeEvent: function(evt) { + + var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; + if (touchEvt) { + for (var property in evt) { + // copy all the properties from the input event that are not + // defined on the touch event (functions included). + if (touchEvt[property] === undefined) { + touchEvt[property] = evt[property]; + } + } + return touchEvt; + } + + return evt; + }, + + nextFrame: (function() { + + var raf; + + if (typeof window !== 'undefined') { + + raf = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame; + } + + if (!raf) { + + var lastTime = 0; + + raf = function(callback) { + + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + + lastTime = currTime + timeToCall; + + return id; + }; + } + + return function(callback, context) { + return context + ? raf(callback.bind(context)) + : raf(callback); + }; + + })(), + + cancelFrame: (function() { + + var caf; + var client = typeof window != 'undefined'; + + if (client) { + + caf = window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.webkitCancelRequestAnimationFrame || + window.msCancelAnimationFrame || + window.msCancelRequestAnimationFrame || + window.oCancelAnimationFrame || + window.oCancelRequestAnimationFrame || + window.mozCancelAnimationFrame || + window.mozCancelRequestAnimationFrame; + } + + caf = caf || clearTimeout; + + return client ? caf.bind(window) : caf; + + })(), + + // ** Deprecated ** + shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { + + var bbox; + var spot; + + if (!magnet) { + + // There is no magnet, try to make the best guess what is the + // wrapping SVG element. This is because we want this "smart" + // connection points to work out of the box without the + // programmer to put magnet marks to any of the subelements. + // For example, we want the functoin to work on basic.Path elements + // without any special treatment of such elements. + // The code below guesses the wrapping element based on + // one simple assumption. The wrapping elemnet is the + // first child of the scalable group if such a group exists + // or the first child of the rotatable group if not. + // This makese sense because usually the wrapping element + // is below any other sub element in the shapes. + var scalable = view.$('.scalable')[0]; + var rotatable = view.$('.rotatable')[0]; + + if (scalable && scalable.firstChild) { + + magnet = scalable.firstChild; + + } else if (rotatable && rotatable.firstChild) { + + magnet = rotatable.firstChild; + } + } + + if (magnet) { + + spot = V(magnet).findIntersection(reference, linkView.paper.viewport); + if (!spot) { + bbox = V(magnet).getBBox({ target: linkView.paper.viewport }); + } + + } else { + + bbox = view.model.getBBox(); + spot = bbox.intersectionWithLineFromCenterToPoint(reference); + } + return spot || bbox.center(); + }, + + isPercentage: function(val) { + + return joint.util.isString(val) && val.slice(-1) === '%'; + }, + + parseCssNumeric: function(strValue, restrictUnits) { + + restrictUnits = restrictUnits || []; + var cssNumeric = { value: parseFloat(strValue) }; + + if (Number.isNaN(cssNumeric.value)) { + return null; + } + + var validUnitsExp = restrictUnits.join('|'); + + if (joint.util.isString(strValue)) { + var matches = new RegExp('(\\d+)(' + validUnitsExp + ')$').exec(strValue); + if (!matches) { + return null; + } + if (matches[2]) { + cssNumeric.unit = matches[2]; + } + } + return cssNumeric; + }, + + breakText: function(text, size, styles, opt) { + + opt = opt || {}; + styles = styles || {}; + + var width = size.width; + var height = size.height; + + var svgDocument = opt.svgDocument || V('svg').node; + var textSpan = V('tspan').node; + var textElement = V('text').attr(styles).append(textSpan).node; + var textNode = document.createTextNode(''); + + // Prevent flickering + textElement.style.opacity = 0; + // Prevent FF from throwing an uncaught exception when `getBBox()` + // called on element that is not in the render tree (is not measurable). + // .getComputedTextLength() returns always 0 in this case. + // Note that the `textElement` resp. `textSpan` can become hidden + // when it's appended to the DOM and a `display: none` CSS stylesheet + // rule gets applied. + textElement.style.display = 'block'; + textSpan.style.display = 'block'; + + textSpan.appendChild(textNode); + svgDocument.appendChild(textElement); + + if (!opt.svgDocument) { + + document.body.appendChild(svgDocument); + } + + var separator = opt.separator || ' '; + var eol = opt.eol || '\n'; + + var words = text.split(separator); + var full = []; + var lines = []; + var p; + var lineHeight; + + for (var i = 0, l = 0, len = words.length; i < len; i++) { + + var word = words[i]; + + if (!word) continue; + + if (eol && word.indexOf(eol) >= 0) { + // word cotains end-of-line character + if (word.length > 1) { + // separate word and continue cycle + var eolWords = word.split(eol); + for (var j = 0, jl = eolWords.length - 1; j < jl; j++) { + eolWords.splice(2 * j + 1, 0, eol); + } + Array.prototype.splice.apply(words, [i, 1].concat(eolWords)); + i--; + len += eolWords.length - 1; + } else { + // creates new line + l++; + } + continue; + } + + + textNode.data = lines[l] ? lines[l] + ' ' + word : word; + + if (textSpan.getComputedTextLength() <= width) { + + // the current line fits + lines[l] = textNode.data; + + if (p) { + // We were partitioning. Put rest of the word onto next line + full[l++] = true; + + // cancel partitioning + p = 0; + } + + } else { + + if (!lines[l] || p) { + + var partition = !!p; + + p = word.length - 1; + + if (partition || !p) { + + // word has only one character. + if (!p) { + + if (!lines[l]) { + + // we won't fit this text within our rect + lines = []; + + break; + } + + // partitioning didn't help on the non-empty line + // try again, but this time start with a new line + + // cancel partitions created + words.splice(i, 2, word + words[i + 1]); + + // adjust word length + len--; + + full[l++] = true; + i--; + + continue; + } + + // move last letter to the beginning of the next word + words[i] = word.substring(0, p); + words[i + 1] = word.substring(p) + words[i + 1]; + + } else { + + // We initiate partitioning + // split the long word into two words + words.splice(i, 1, word.substring(0, p), word.substring(p)); + + // adjust words length + len++; + + if (l && !full[l - 1]) { + // if the previous line is not full, try to fit max part of + // the current word there + l--; + } + } + + i--; + + continue; + } + + l++; + i--; + } + + // if size.height is defined we have to check whether the height of the entire + // text exceeds the rect height + if (height !== undefined) { + + if (lineHeight === undefined) { + + var heightValue; + + // use the same defaults as in V.prototype.text + if (styles.lineHeight === 'auto') { + heightValue = { value: 1.5, unit: 'em' }; + } else { + heightValue = joint.util.parseCssNumeric(styles.lineHeight, ['em']) || { value: 1, unit: 'em' }; + } + + lineHeight = heightValue.value; + if (heightValue.unit === 'em' ) { + lineHeight *= textElement.getBBox().height; + } + } + + if (lineHeight * lines.length > height) { + + // remove overflowing lines + lines.splice(Math.floor(height / lineHeight)); + + break; + } + } + } + + if (opt.svgDocument) { + + // svg document was provided, remove the text element only + svgDocument.removeChild(textElement); + + } else { + + // clean svg document + document.body.removeChild(svgDocument); + } + + return lines.join(eol); + }, + + // Sanitize HTML + // Based on https://gist.github.com/ufologist/5a0da51b2b9ef1b861c30254172ac3c9 + // Parses a string into an array of DOM nodes. + // Then outputs it back as a string. + sanitizeHTML: function(html) { + + // Ignores tags that are invalid inside a
tag (e.g. , ) + + // If documentContext (second parameter) is not specified or given as `null` or `undefined`, a new document is used. + // Inline events will not execute when the HTML is parsed; this includes, for example, sending GET requests for images. + + // If keepScripts (last parameter) is `false`, scripts are not executed. + var output = $($.parseHTML('
' + html + '
', null, false)); + + output.find('*').each(function() { // for all nodes + var currentNode = this; + + $.each(currentNode.attributes, function() { // for all attributes in each node + var currentAttribute = this; + + var attrName = currentAttribute.name; + var attrValue = currentAttribute.value; + + // Remove attribute names that start with "on" (e.g. onload, onerror...). + // Remove attribute values that start with "javascript:" pseudo protocol (e.g. `href="javascript:alert(1)"`). + if (attrName.indexOf('on') === 0 || attrValue.indexOf('javascript:') === 0) { + $(currentNode).removeAttr(attrName); + } + }); + }); + + return output.html(); + }, + + // Download `blob` as file with `fileName`. + // Does not work in IE9. + downloadBlob: function(blob, fileName) { + + if (window.navigator.msSaveBlob) { // requires IE 10+ + // pulls up a save dialog + window.navigator.msSaveBlob(blob, fileName); + + } else { // other browsers + // downloads directly in Chrome and Safari + + // presents a save/open dialog in Firefox + // Firefox bug: `from` field in save dialog always shows `from:blob:` + // https://bugzilla.mozilla.org/show_bug.cgi?id=1053327 + + var url = window.URL.createObjectURL(blob); + var link = document.createElement('a'); + + link.href = url; + link.download = fileName; + document.body.appendChild(link); + + link.click(); + + document.body.removeChild(link); + window.URL.revokeObjectURL(url); // mark the url for garbage collection + } + }, + + // Download `dataUri` as file with `fileName`. + // Does not work in IE9. + downloadDataUri: function(dataUri, fileName) { + + var blob = joint.util.dataUriToBlob(dataUri); + joint.util.downloadBlob(blob, fileName); + }, + + // Convert an uri-encoded data component (possibly also base64-encoded) to a blob. + dataUriToBlob: function(dataUri) { + + // first, make sure there are no newlines in the data uri + dataUri = dataUri.replace(/\s/g, ''); + dataUri = decodeURIComponent(dataUri); + + var firstCommaIndex = dataUri.indexOf(','); // split dataUri as `dataTypeString`,`data` + + var dataTypeString = dataUri.slice(0, firstCommaIndex); // e.g. 'data:image/jpeg;base64' + var mimeString = dataTypeString.split(':')[1].split(';')[0]; // e.g. 'image/jpeg' + + var data = dataUri.slice(firstCommaIndex + 1); + var decodedString; + if (dataTypeString.indexOf('base64') >= 0) { // data may be encoded in base64 + decodedString = atob(data); // decode data + } else { + // convert the decoded string to UTF-8 + decodedString = unescape(encodeURIComponent(data)); + } + // write the bytes of the string to a typed array + var ia = new window.Uint8Array(decodedString.length); + for (var i = 0; i < decodedString.length; i++) { + ia[i] = decodedString.charCodeAt(i); + } + + return new Blob([ia], { type: mimeString }); // return the typed array as Blob + }, + + // Read an image at `url` and return it as base64-encoded data uri. + // The mime type of the image is inferred from the `url` file extension. + // If data uri is provided as `url`, it is returned back unchanged. + // `callback` is a method with `err` as first argument and `dataUri` as second argument. + // Works with IE9. + imageToDataUri: function(url, callback) { + + if (!url || url.substr(0, 'data:'.length) === 'data:') { + // No need to convert to data uri if it is already in data uri. + + // This not only convenient but desired. For example, + // IE throws a security error if data:image/svg+xml is used to render + // an image to the canvas and an attempt is made to read out data uri. + // Now if our image is already in data uri, there is no need to render it to the canvas + // and so we can bypass this error. + + // Keep the async nature of the function. + return setTimeout(function() { + callback(null, url); + }, 0); + } + + // chrome, IE10+ + var modernHandler = function(xhr, callback) { + + if (xhr.status === 200) { + + var reader = new FileReader(); + + reader.onload = function(evt) { + var dataUri = evt.target.result; + callback(null, dataUri); + }; + + reader.onerror = function() { + callback(new Error('Failed to load image ' + url)); + }; + + reader.readAsDataURL(xhr.response); + } else { + callback(new Error('Failed to load image ' + url)); + } + }; + + var legacyHandler = function(xhr, callback) { + + var Uint8ToString = function(u8a) { + var CHUNK_SZ = 0x8000; + var c = []; + for (var i = 0; i < u8a.length; i += CHUNK_SZ) { + c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); + } + return c.join(''); + }; + + if (xhr.status === 200) { + + var bytes = new Uint8Array(xhr.response); + + var suffix = (url.split('.').pop()) || 'png'; + var map = { + 'svg': 'svg+xml' + }; + var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,'; + var b64encoded = meta + btoa(Uint8ToString(bytes)); + callback(null, b64encoded); + } else { + callback(new Error('Failed to load image ' + url)); + } + }; + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url, true); + xhr.addEventListener('error', function() { + callback(new Error('Failed to load image ' + url)); + }); + + xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer'; + + xhr.addEventListener('load', function() { + if (window.FileReader) { + modernHandler(xhr, callback); + } else { + legacyHandler(xhr, callback); + } + }); + + xhr.send(); + }, + + getElementBBox: function(el) { + + var $el = $(el); + if ($el.length === 0) { + throw new Error('Element not found') + } + + var element = $el[0]; + var doc = element.ownerDocument; + var clientBBox = element.getBoundingClientRect(); + + var strokeWidthX = 0; + var strokeWidthY = 0; + + // Firefox correction + if (element.ownerSVGElement) { + + var vel = V(element); + var bbox = vel.getBBox({ target: vel.svg() }); + + // if FF getBoundingClientRect includes stroke-width, getBBox doesn't. + // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value. + strokeWidthX = (clientBBox.width - bbox.width); + strokeWidthY = (clientBBox.height - bbox.height); + } + + return { + x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2, + y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2, + width: clientBBox.width - strokeWidthX, + height: clientBBox.height - strokeWidthY + }; + }, + + + // Highly inspired by the jquery.sortElements plugin by Padolsey. + // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. + sortElements: function(elements, comparator) { + + var $elements = $(elements); + var placements = $elements.map(function() { + + var sortElement = this; + var parentNode = sortElement.parentNode; + // Since the element itself will change position, we have + // to have some way of storing it's original position in + // the DOM. The easiest way is to have a 'flag' node: + var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); + + return function() { + + if (parentNode === this) { + throw new Error('You can\'t sort elements if any one is a descendant of another.'); + } + + // Insert before flag: + parentNode.insertBefore(this, nextSibling); + // Remove flag: + parentNode.removeChild(nextSibling); + }; + }); + + return Array.prototype.sort.call($elements, comparator).each(function(i) { + placements[i].call(this); + }); + }, + + // Sets attributes on the given element and its descendants based on the selector. + // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} + setAttributesBySelector: function(element, attrs) { + + var $element = $(element); + + joint.util.forIn(attrs, function(attrs, selector) { + var $elements = $element.find(selector).addBack().filter(selector); + // Make a special case for setting classes. + // We do not want to overwrite any existing class. + if (joint.util.has(attrs, 'class')) { + $elements.addClass(attrs['class']); + attrs = joint.util.omit(attrs, 'class'); + } + $elements.attr(attrs); + }); + }, + + // Return a new object with all four sides (top, right, bottom, left) in it. + // Value of each side is taken from the given argument (either number or object). + // Default value for a side is 0. + // Examples: + // joint.util.normalizeSides(5) --> { top: 5, right: 5, bottom: 5, left: 5 } + // joint.util.normalizeSides({ horizontal: 5 }) --> { top: 0, right: 5, bottom: 0, left: 5 } + // joint.util.normalizeSides({ left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 } + // joint.util.normalizeSides({ horizontal: 10, left: 5 }) --> { top: 0, right: 10, bottom: 0, left: 5 } + // joint.util.normalizeSides({ horizontal: 0, left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 } + normalizeSides: function(box) { + + if (Object(box) !== box) { // `box` is not an object + var val = 0; // `val` left as 0 if `box` cannot be understood as finite number + if (isFinite(box)) val = +box; // actually also accepts string numbers (e.g. '100') + + return { top: val, right: val, bottom: val, left: val }; + } + + // `box` is an object + var top, right, bottom, left; + top = right = bottom = left = 0; + + if (isFinite(box.vertical)) top = bottom = +box.vertical; + if (isFinite(box.horizontal)) right = left = +box.horizontal; + + if (isFinite(box.top)) top = +box.top; // overwrite vertical + if (isFinite(box.right)) right = +box.right; // overwrite horizontal + if (isFinite(box.bottom)) bottom = +box.bottom; // overwrite vertical + if (isFinite(box.left)) left = +box.left; // overwrite horizontal + + return { top: top, right: right, bottom: bottom, left: left }; + }, + + timing: { + + linear: function(t) { + return t; + }, + + quad: function(t) { + return t * t; + }, + + cubic: function(t) { + return t * t * t; + }, + + inout: function(t) { + if (t <= 0) return 0; + if (t >= 1) return 1; + var t2 = t * t; + var t3 = t2 * t; + return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); + }, + + exponential: function(t) { + return Math.pow(2, 10 * (t - 1)); + }, + + bounce: function(t) { + for (var a = 0, b = 1; 1; a += b, b /= 2) { + if (t >= (7 - 4 * a) / 11) { + var q = (11 - 6 * a - 11 * t) / 4; + return -q * q + b * b; + } + } + }, + + reverse: function(f) { + return function(t) { + return 1 - f(1 - t); + }; + }, + + reflect: function(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); + }; + }, + + clamp: function(f, n, x) { + n = n || 0; + x = x || 1; + return function(t) { + var r = f(t); + return r < n ? n : r > x ? x : r; + }; + }, + + back: function(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + }, + + elastic: function(x) { + if (!x) x = 1.5; + return function(t) { + return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); + }; + } + }, + + interpolate: { + + number: function(a, b) { + var d = b - a; + return function(t) { return a + d * t; }; + }, + + object: function(a, b) { + var s = Object.keys(a); + return function(t) { + var i, p; + var r = {}; + for (i = s.length - 1; i != -1; i--) { + p = s[i]; + r[p] = a[p] + (b[p] - a[p]) * t; + } + return r; + }; + }, + + hexColor: function(a, b) { + + var ca = parseInt(a.slice(1), 16); + var cb = parseInt(b.slice(1), 16); + var ra = ca & 0x0000ff; + var rd = (cb & 0x0000ff) - ra; + var ga = ca & 0x00ff00; + var gd = (cb & 0x00ff00) - ga; + var ba = ca & 0xff0000; + var bd = (cb & 0xff0000) - ba; + + return function(t) { + + var r = (ra + rd * t) & 0x000000ff; + var g = (ga + gd * t) & 0x0000ff00; + var b = (ba + bd * t) & 0x00ff0000; + + return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); + }; + }, + + unit: function(a, b) { + + var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; + var ma = r.exec(a); + var mb = r.exec(b); + var p = mb[1].indexOf('.'); + var f = p > 0 ? mb[1].length - p - 1 : 0; + a = +ma[1]; + var d = +mb[1] - a; + var u = ma[2]; + + return function(t) { + return (a + d * t).toFixed(f) + u; + }; + } + }, + + // SVG filters. + filter: { + + // `color` ... outline color + // `width`... outline width + // `opacity` ... outline opacity + // `margin` ... gap between outline and the element + outline: function(args) { + + var tpl = ''; + + var margin = Number.isFinite(args.margin) ? args.margin : 2; + var width = Number.isFinite(args.width) ? args.width : 1; + + return joint.util.template(tpl)({ + color: args.color || 'blue', + opacity: Number.isFinite(args.opacity) ? args.opacity : 1, + outerRadius: margin + width, + innerRadius: margin + }); + }, + + // `color` ... color + // `width`... width + // `blur` ... blur + // `opacity` ... opacity + highlight: function(args) { + + var tpl = ''; + + return joint.util.template(tpl)({ + color: args.color || 'red', + width: Number.isFinite(args.width) ? args.width : 1, + blur: Number.isFinite(args.blur) ? args.blur : 0, + opacity: Number.isFinite(args.opacity) ? args.opacity : 1 + }); + }, + + // `x` ... horizontal blur + // `y` ... vertical blur (optional) + blur: function(args) { + + var x = Number.isFinite(args.x) ? args.x : 2; + + return joint.util.template('')({ + stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x + }); + }, + + // `dx` ... horizontal shift + // `dy` ... vertical shift + // `blur` ... blur + // `color` ... color + // `opacity` ... opacity + dropShadow: function(args) { + + var tpl = 'SVGFEDropShadowElement' in window + ? '' + : ''; + + return joint.util.template(tpl)({ + dx: args.dx || 0, + dy: args.dy || 0, + opacity: Number.isFinite(args.opacity) ? args.opacity : 1, + color: args.color || 'black', + blur: Number.isFinite(args.blur) ? args.blur : 4 + }); + }, + + // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. + grayscale: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + a: 0.2126 + 0.7874 * (1 - amount), + b: 0.7152 - 0.7152 * (1 - amount), + c: 0.0722 - 0.0722 * (1 - amount), + d: 0.2126 - 0.2126 * (1 - amount), + e: 0.7152 + 0.2848 * (1 - amount), + f: 0.0722 - 0.0722 * (1 - amount), + g: 0.2126 - 0.2126 * (1 - amount), + h: 0.0722 + 0.9278 * (1 - amount) + }); + }, + + // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. + sepia: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + a: 0.393 + 0.607 * (1 - amount), + b: 0.769 - 0.769 * (1 - amount), + c: 0.189 - 0.189 * (1 - amount), + d: 0.349 - 0.349 * (1 - amount), + e: 0.686 + 0.314 * (1 - amount), + f: 0.168 - 0.168 * (1 - amount), + g: 0.272 - 0.272 * (1 - amount), + h: 0.534 - 0.534 * (1 - amount), + i: 0.131 + 0.869 * (1 - amount) + }); + }, + + // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + saturate: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + amount: 1 - amount + }); + }, + + // `angle` ... the number of degrees around the color circle the input samples will be adjusted. + hueRotate: function(args) { + + return joint.util.template('')({ + angle: args.angle || 0 + }); + }, + + // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. + invert: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + amount: amount, + amount2: 1 - amount + }); + }, + + // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + brightness: function(args) { + + return joint.util.template('')({ + amount: Number.isFinite(args.amount) ? args.amount : 1 + }); + }, + + // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + contrast: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + amount: amount, + amount2: .5 - amount / 2 + }); + } + }, + + format: { + + // Formatting numbers via the Python Format Specification Mini-language. + // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. + // Heavilly inspired by the D3.js library implementation. + number: function(specifier, value, locale) { + + locale = locale || { + + currency: ['$', ''], + decimal: '.', + thousands: ',', + grouping: [3] + }; + + // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. + // [[fill]align][sign][symbol][0][width][,][.precision][type] + var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + + var match = re.exec(specifier); + var fill = match[1] || ' '; + var align = match[2] || '>'; + var sign = match[3] || ''; + var symbol = match[4] || ''; + var zfill = match[5]; + var width = +match[6]; + var comma = match[7]; + var precision = match[8]; + var type = match[9]; + var scale = 1; + var prefix = ''; + var suffix = ''; + var integer = false; + + if (precision) precision = +precision.substring(1); + + if (zfill || fill === '0' && align === '=') { + zfill = fill = '0'; + align = '='; + if (comma) width -= Math.floor((width - 1) / 4); + } + + switch (type) { + case 'n': + comma = true; type = 'g'; + break; + case '%': + scale = 100; suffix = '%'; type = 'f'; + break; + case 'p': + scale = 100; suffix = '%'; type = 'r'; + break; + case 'b': + case 'o': + case 'x': + case 'X': + if (symbol === '#') prefix = '0' + type.toLowerCase(); + break; + case 'c': + case 'd': + integer = true; precision = 0; + break; + case 's': + scale = -1; type = 'r'; + break; + } + + if (symbol === '$') { + prefix = locale.currency[0]; + suffix = locale.currency[1]; + } + + // If no precision is specified for `'r'`, fallback to general notation. + if (type == 'r' && !precision) type = 'g'; + + // Ensure that the requested precision is in the supported range. + if (precision != null) { + if (type == 'g') precision = Math.max(1, Math.min(21, precision)); + else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); + } + + var zcomma = zfill && comma; + + // Return the empty string for floats formatted as ints. + if (integer && (value % 1)) return ''; + + // Convert negative to positive, and record the sign prefix. + var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; + + var fullSuffix = suffix; + + // Apply the scale, computing it from the value's exponent for si format. + // Preserve the existing suffix, if any, such as the currency symbol. + if (scale < 0) { + var unit = this.prefix(value, precision); + value = unit.scale(value); + fullSuffix = unit.symbol + suffix; + } else { + value *= scale; + } + + // Convert to the desired precision. + value = this.convert(type, value, precision); + + // Break the value into the integer part (before) and decimal part (after). + var i = value.lastIndexOf('.'); + var before = i < 0 ? value : value.substring(0, i); + var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); + + function formatGroup(value) { + + var i = value.length; + var t = []; + var j = 0; + var g = locale.grouping[0]; + while (i > 0 && g > 0) { + t.push(value.substring(i -= g, i + g)); + g = locale.grouping[j = (j + 1) % locale.grouping.length]; + } + return t.reverse().join(locale.thousands); + } + + // If the fill character is not `'0'`, grouping is applied before padding. + if (!zfill && comma && locale.grouping) { + + before = formatGroup(before); + } + + var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); + var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; + + // If the fill character is `'0'`, grouping is applied after padding. + if (zcomma) before = formatGroup(padding + before); + + // Apply prefix. + negative += prefix; + + // Rejoin integer and decimal parts. + value = before + after; + + return (align === '<' ? negative + value + padding + : align === '>' ? padding + negative + value + : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) + : negative + (zcomma ? value : padding + value)) + fullSuffix; + }, + + // Formatting string via the Python Format string. + // See https://docs.python.org/2/library/string.html#format-string-syntax) + string: function(formatString, value) { + + var fieldDelimiterIndex; + var fieldDelimiter = '{'; + var endPlaceholder = false; + var formattedStringArray = []; + + while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { + + var pieceFormatedString, formatSpec, fieldName; + + pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); + + if (endPlaceholder) { + formatSpec = pieceFormatedString.split(':'); + fieldName = formatSpec.shift().split('.'); + pieceFormatedString = value; + + for (var i = 0; i < fieldName.length; i++) + pieceFormatedString = pieceFormatedString[fieldName[i]]; + + if (formatSpec.length) + pieceFormatedString = this.number(formatSpec, pieceFormatedString); + } + + formattedStringArray.push(pieceFormatedString); + + formatString = formatString.slice(fieldDelimiterIndex + 1); + fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; + } + formattedStringArray.push(formatString); + + return formattedStringArray.join(''); + }, + + convert: function(type, value, precision) { + + switch (type) { + case 'b': return value.toString(2); + case 'c': return String.fromCharCode(value); + case 'o': return value.toString(8); + case 'x': return value.toString(16); + case 'X': return value.toString(16).toUpperCase(); + case 'g': return value.toPrecision(precision); + case 'e': return value.toExponential(precision); + case 'f': return value.toFixed(precision); + case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); + default: return value + ''; + } + }, + + round: function(value, precision) { + + return precision + ? Math.round(value * (precision = Math.pow(10, precision))) / precision + : Math.round(value); + }, + + precision: function(value, precision) { + + return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); + }, + + prefix: function(value, precision) { + + var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) { + var k = Math.pow(10, Math.abs(8 - i) * 3); + return { + scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, + symbol: d + }; + }); + + var i = 0; + if (value) { + if (value < 0) value *= -1; + if (precision) value = this.round(value, this.precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); + } + return prefixes[8 + i / 3]; + } + }, + + /* + Pre-compile the HTML to be used as a template. + */ + template: function(html) { + + /* + Must support the variation in templating syntax found here: + https://lodash.com/docs#template + */ + var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; + + return function(data) { + + data = data || {}; + + return html.replace(regex, function(match) { + + var args = Array.from(arguments); + var attr = args.slice(1, 4).find(function(_attr) { + return !!_attr; + }); + + var attrArray = attr.split('.'); + var value = data[attrArray.shift()]; + + while (value !== undefined && attrArray.length) { + value = value[attrArray.shift()]; + } + + return value !== undefined ? value : ''; + }); + }; + }, + + /** + * @param {Element=} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default. + */ + toggleFullScreen: function(el) { + + var topDocument = window.top.document; + el = el || topDocument.body; + + function prefixedResult(el, prop) { + + var prefixes = ['webkit', 'moz', 'ms', 'o', '']; + for (var i = 0; i < prefixes.length; i++) { + var prefix = prefixes[i]; + var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); + if (el[propName] !== undefined) { + return joint.util.isFunction(el[propName]) ? el[propName]() : el[propName]; + } + } + } + + if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) { + prefixedResult(topDocument, 'ExitFullscreen') || // Spec. + prefixedResult(topDocument, 'CancelFullScreen'); // Firefox + } else { + prefixedResult(el, 'RequestFullscreen') || // Spec. + prefixedResult(el, 'RequestFullScreen'); // Firefox + } + }, + + addClassNamePrefix: function(className) { + + if (!className) return className; + + return className.toString().split(' ').map(function(_className) { + + if (_className.substr(0, joint.config.classNamePrefix.length) !== joint.config.classNamePrefix) { + _className = joint.config.classNamePrefix + _className; + } + + return _className; + + }).join(' '); + }, + + removeClassNamePrefix: function(className) { + + if (!className) return className; + + return className.toString().split(' ').map(function(_className) { + + if (_className.substr(0, joint.config.classNamePrefix.length) === joint.config.classNamePrefix) { + _className = _className.substr(joint.config.classNamePrefix.length); + } + + return _className; + + }).join(' '); + }, + + wrapWith: function(object, methods, wrapper) { + + if (joint.util.isString(wrapper)) { + + if (!joint.util.wrappers[wrapper]) { + throw new Error('Unknown wrapper: "' + wrapper + '"'); + } + + wrapper = joint.util.wrappers[wrapper]; + } + + if (!joint.util.isFunction(wrapper)) { + throw new Error('Wrapper must be a function.'); + } + + this.toArray(methods).forEach(function(method) { + object[method] = wrapper(object[method]); + }); + }, + + wrappers: { + + /* + Prepares a function with the following usage: + + fn([cell, cell, cell], opt); + fn([cell, cell, cell]); + fn(cell, cell, cell, opt); + fn(cell, cell, cell); + fn(cell); + */ + cells: function(fn) { + + return function() { + + var args = Array.from(arguments); + var n = args.length; + var cells = n > 0 && args[0] || []; + var opt = n > 1 && args[n - 1] || {}; + + if (!Array.isArray(cells)) { + + if (opt instanceof joint.dia.Cell) { + cells = args; + } else if (cells instanceof joint.dia.Cell) { + if (args.length > 1) { + args.pop(); + } + cells = args; + } + } + + if (opt instanceof joint.dia.Cell) { + opt = {}; + } + + return fn.call(this, cells, opt); + }; + } + }, + + parseDOMJSON: function(json, namespace) { + + var selectors = {}; + var svgNamespace = V.namespace.xmlns; + var ns = namespace || svgNamespace; + var fragment = document.createDocumentFragment(); + var queue = [json, fragment, ns]; + while (queue.length > 0) { + ns = queue.pop(); + var parentNode = queue.pop(); + var siblingsDef = queue.pop(); + for (var i = 0, n = siblingsDef.length; i < n; i++) { + var nodeDef = siblingsDef[i]; + // TagName + if (!nodeDef.hasOwnProperty('tagName')) throw new Error('json-dom-parser: missing tagName'); + var tagName = nodeDef.tagName; + // Namespace URI + if (nodeDef.hasOwnProperty('namespaceURI')) ns = nodeDef.namespaceURI; + var node = document.createElementNS(ns, tagName); + var svg = (ns === svgNamespace); + var wrapper = (svg) ? V : $; + // Attributes + var attributes = nodeDef.attributes; + if (attributes) wrapper(node).attr(attributes); + // Style + var style = nodeDef.style; + if (style) $(node).css(style); + // ClassName + if (nodeDef.hasOwnProperty('className')) { + var className = nodeDef.className; + if (svg) { + node.className.baseVal = className; + } else { + node.className = className; + } + } + // Selector + if (nodeDef.hasOwnProperty('selector')) { + var nodeSelector = nodeDef.selector; + if (selectors[nodeSelector]) throw new Error('json-dom-parser: selector must be unique'); + selectors[nodeSelector] = node; + wrapper(node).attr('joint-selector', nodeSelector); + } + parentNode.appendChild(node); + // Children + var childrenDef = nodeDef.children; + if (Array.isArray(childrenDef)) queue.push(childrenDef, node, ns); + } + } + return { + fragment: fragment, + selectors: selectors + } + }, + + // lodash 3 vs 4 incompatible + sortedIndex: _.sortedIndexBy || _.sortedIndex, + uniq: _.uniqBy || _.uniq, + uniqueId: _.uniqueId, + sortBy: _.sortBy, + isFunction: _.isFunction, + result: _.result, + union: _.union, + invoke: _.invokeMap || _.invoke, + difference: _.difference, + intersection: _.intersection, + omit: _.omit, + pick: _.pick, + has: _.has, + bindAll: _.bindAll, + assign: _.assign, + defaults: _.defaults, + defaultsDeep: _.defaultsDeep, + isPlainObject: _.isPlainObject, + isEmpty: _.isEmpty, + isEqual: _.isEqual, + noop: function() {}, + cloneDeep: _.cloneDeep, + toArray: _.toArray, + flattenDeep: _.flattenDeep, + camelCase: _.camelCase, + groupBy: _.groupBy, + forIn: _.forIn, + without: _.without, + debounce: _.debounce, + clone: _.clone, + + isBoolean: function(value) { + var toString = Object.prototype.toString; + return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]'); + }, + + isObject: function(value) { + return !!value && (typeof value === 'object' || typeof value === 'function'); + }, + + isNumber: function(value) { + var toString = Object.prototype.toString; + return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]'); + }, + + isString: function(value) { + var toString = Object.prototype.toString; + return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]'); + }, + + merge: function() { + if (_.mergeWith) { + var args = Array.from(arguments); + var last = args[args.length - 1]; + + var customizer = this.isFunction(last) ? last : this.noop; + args.push(function(a,b) { + var customResult = customizer(a, b); + if (customResult !== undefined) { + return customResult; + } + + if (Array.isArray(a) && !Array.isArray(b)) { + return b; + } + }); + + return _.mergeWith.apply(this, args) + } + return _.merge.apply(this, arguments); + } + } +}; + + +joint.mvc.View = Backbone.View.extend({ + + options: {}, + theme: null, + themeClassNamePrefix: joint.util.addClassNamePrefix('theme-'), + requireSetThemeOverride: false, + defaultTheme: joint.config.defaultTheme, + children: null, + childNodes: null, + + constructor: function(options) { + + this.requireSetThemeOverride = options && !!options.theme; + this.options = joint.util.assign({}, this.options, options); + + Backbone.View.call(this, options); + }, + + initialize: function(options) { + + joint.util.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); + + joint.mvc.views[this.cid] = this; + + this.setTheme(this.options.theme || this.defaultTheme); + this.init(); + }, + + renderChildren: function(children) { + children || (children = this.children); + if (children) { + var namespace = V.namespace[this.svgElement ? 'xmlns' : 'xhtml']; + var doc = joint.util.parseDOMJSON(children, namespace); + this.vel.empty().append(doc.fragment); + this.childNodes = doc.selectors; + } + return this; + }, + + // Override the Backbone `_ensureElement()` method in order to create an + // svg element (e.g., ``) node that wraps all the nodes of the Cell view. + // Expose class name setter as a separate method. + _ensureElement: function() { + if (!this.el) { + var tagName = joint.util.result(this, 'tagName'); + var attrs = joint.util.assign({}, joint.util.result(this, 'attributes')); + if (this.id) attrs.id = joint.util.result(this, 'id'); + this.setElement(this._createElement(tagName)); + this._setAttributes(attrs); + } else { + this.setElement(joint.util.result(this, 'el')); + } + this._ensureElClassName(); + }, + + _setAttributes: function(attrs) { + if (this.svgElement) { + this.vel.attr(attrs); + } else { + this.$el.attr(attrs); + } + }, + + _createElement: function(tagName) { + if (this.svgElement) { + return document.createElementNS(V.namespace.xmlns, tagName); + } else { + return document.createElement(tagName); + } + }, + + // Utilize an alternative DOM manipulation API by + // adding an element reference wrapped in Vectorizer. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + if (this.svgElement) this.vel = V(this.el); + }, + + _ensureElClassName: function() { + var className = joint.util.result(this, 'className'); + var prefixedClassName = joint.util.addClassNamePrefix(className); + // Note: className removal here kept for backwards compatibility only + if (this.svgElement) { + this.vel.removeClass(className).addClass(prefixedClassName); + } else { + this.$el.removeClass(className).addClass(prefixedClassName); + } + }, + + init: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, + + onRender: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, + + setTheme: function(theme, opt) { + + opt = opt || {}; + + // Theme is already set, override is required, and override has not been set. + // Don't set the theme. + if (this.theme && this.requireSetThemeOverride && !opt.override) { + return this; + } + + this.removeThemeClassName(); + this.addThemeClassName(theme); + this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); + this.theme = theme; + + return this; + }, + + addThemeClassName: function(theme) { + + theme = theme || this.theme; + + var className = this.themeClassNamePrefix + theme; + + if (this.svgElement) { + this.vel.addClass(className); + } else { + this.$el.addClass(className); + } + + return this; + }, + + removeThemeClassName: function(theme) { + + theme = theme || this.theme; + + var className = this.themeClassNamePrefix + theme; + + if (this.svgElement) { + this.vel.removeClass(className); + } else { + this.$el.removeClass(className); + } + + return this; + }, + + onSetTheme: function(oldTheme, newTheme) { + // Intentionally empty. + // This method is meant to be overriden. + }, + + remove: function() { + + this.onRemove(); + this.undelegateDocumentEvents(); + + joint.mvc.views[this.cid] = null; + + Backbone.View.prototype.remove.apply(this, arguments); + + return this; + }, + + onRemove: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, + + getEventNamespace: function() { + // Returns a per-session unique namespace + return '.joint-event-ns-' + this.cid; + }, + + delegateElementEvents: function(element, events, data) { + if (!events) return this; + data || (data = {}); + var eventNS = this.getEventNamespace(); + for (var eventName in events) { + var method = events[eventName]; + if (typeof method !== 'function') method = this[method]; + if (!method) continue; + $(element).on(eventName + eventNS, data, method.bind(this)); + } + return this; + }, + + undelegateElementEvents: function(element) { + $(element).off(this.getEventNamespace()); + return this; + }, + + delegateDocumentEvents: function(events, data) { + events || (events = joint.util.result(this, 'documentEvents')); + return this.delegateElementEvents(document, events, data); + }, + + undelegateDocumentEvents: function() { + return this.undelegateElementEvents(document); + }, + + eventData: function(evt, data) { + if (!evt) throw new Error('eventData(): event object required.'); + var currentData = evt.data; + var key = '__' + this.cid + '__'; + if (data === undefined) { + if (!currentData) return {}; + return currentData[key] || {}; + } + currentData || (currentData = evt.data = {}); + currentData[key] || (currentData[key] = {}); + joint.util.assign(currentData[key], data); + return this; + } + +}, { + + extend: function() { + + var args = Array.from(arguments); + + // Deep clone the prototype and static properties objects. + // This prevents unexpected behavior where some properties are overwritten outside of this function. + var protoProps = args[0] && joint.util.assign({}, args[0]) || {}; + var staticProps = args[1] && joint.util.assign({}, args[1]) || {}; + + // Need the real render method so that we can wrap it and call it later. + var renderFn = protoProps.render || (this.prototype && this.prototype.render) || null; + + /* + Wrap the real render method so that: + .. `onRender` is always called. + .. `this` is always returned. + */ + protoProps.render = function() { + + if (renderFn) { + // Call the original render method. + renderFn.apply(this, arguments); + } + + // Should always call onRender() method. + this.onRender(); + + // Should always return itself. + return this; + }; + + return Backbone.View.extend.call(this, protoProps, staticProps); + } +}); + + + +joint.dia.GraphCells = Backbone.Collection.extend({ + + cellNamespace: joint.shapes, + + initialize: function(models, opt) { + + // Set the optional namespace where all model classes are defined. + if (opt.cellNamespace) { + this.cellNamespace = opt.cellNamespace; + } + + this.graph = opt.graph; + }, + + model: function(attrs, opt) { + + var collection = opt.collection; + var namespace = collection.cellNamespace; + + // Find the model class in the namespace or use the default one. + var ModelClass = (attrs.type === 'link') + ? joint.dia.Link + : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; + + var cell = new ModelClass(attrs, opt); + // Add a reference to the graph. It is necessary to do this here because this is the earliest place + // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. + if (!opt.dry) { + cell.graph = collection.graph; + } + + return cell; + }, + + // `comparator` makes it easy to sort cells based on their `z` index. + comparator: function(model) { + + return model.get('z') || 0; + } +}); + + +joint.dia.Graph = Backbone.Model.extend({ + + _batches: {}, + + initialize: function(attrs, opt) { + + opt = opt || {}; + + // Passing `cellModel` function in the options object to graph allows for + // setting models based on attribute objects. This is especially handy + // when processing JSON graphs that are in a different than JointJS format. + var cells = new joint.dia.GraphCells([], { + model: opt.cellModel, + cellNamespace: opt.cellNamespace, + graph: this + }); + Backbone.Model.prototype.set.call(this, 'cells', cells); + + // Make all the events fired in the `cells` collection available. + // to the outside world. + cells.on('all', this.trigger, this); + + // Backbone automatically doesn't trigger re-sort if models attributes are changed later when + // they're already in the collection. Therefore, we're triggering sort manually here. + this.on('change:z', this._sortOnChangeZ, this); + + // `joint.dia.Graph` keeps an internal data structure (an adjacency list) + // for fast graph queries. All changes that affect the structure of the graph + // must be reflected in the `al` object. This object provides fast answers to + // questions such as "what are the neighbours of this node" or "what + // are the sibling links of this link". + + // Outgoing edges per node. Note that we use a hash-table for the list + // of outgoing edges for a faster lookup. + // [node ID] -> Object [edge] -> true + this._out = {}; + // Ingoing edges per node. + // [node ID] -> Object [edge] -> true + this._in = {}; + // `_nodes` is useful for quick lookup of all the elements in the graph, without + // having to go through the whole cells array. + // [node ID] -> true + this._nodes = {}; + // `_edges` is useful for quick lookup of all the links in the graph, without + // having to go through the whole cells array. + // [edge ID] -> true + this._edges = {}; + + cells.on('add', this._restructureOnAdd, this); + cells.on('remove', this._restructureOnRemove, this); + cells.on('reset', this._restructureOnReset, this); + cells.on('change:source', this._restructureOnChangeSource, this); + cells.on('change:target', this._restructureOnChangeTarget, this); + cells.on('remove', this._removeCell, this); + }, + + _sortOnChangeZ: function() { + + this.get('cells').sort(); + }, + + _restructureOnAdd: function(cell) { + + if (cell.isLink()) { + this._edges[cell.id] = true; + var source = cell.get('source'); + var target = cell.get('target'); + if (source.id) { + (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; + } + if (target.id) { + (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; + } + } else { + this._nodes[cell.id] = true; + } + }, + + _restructureOnRemove: function(cell) { + + if (cell.isLink()) { + delete this._edges[cell.id]; + var source = cell.get('source'); + var target = cell.get('target'); + if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { + delete this._out[source.id][cell.id]; + } + if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { + delete this._in[target.id][cell.id]; + } + } else { + delete this._nodes[cell.id]; + } + }, + + _restructureOnReset: function(cells) { + + // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. + cells = cells.models; + + this._out = {}; + this._in = {}; + this._nodes = {}; + this._edges = {}; + + cells.forEach(this._restructureOnAdd, this); + }, + + _restructureOnChangeSource: function(link) { + + var prevSource = link.previous('source'); + if (prevSource.id && this._out[prevSource.id]) { + delete this._out[prevSource.id][link.id]; + } + var source = link.get('source'); + if (source.id) { + (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; + } + }, + + _restructureOnChangeTarget: function(link) { + + var prevTarget = link.previous('target'); + if (prevTarget.id && this._in[prevTarget.id]) { + delete this._in[prevTarget.id][link.id]; + } + var target = link.get('target'); + if (target.id) { + (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; + } + }, + + // Return all outbound edges for the node. Return value is an object + // of the form: [edge] -> true + getOutboundEdges: function(node) { + + return (this._out && this._out[node]) || {}; + }, + + // Return all inbound edges for the node. Return value is an object + // of the form: [edge] -> true + getInboundEdges: function(node) { + + return (this._in && this._in[node]) || {}; + }, + + toJSON: function() { + + // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. + // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. + var json = Backbone.Model.prototype.toJSON.apply(this, arguments); + json.cells = this.get('cells').toJSON(); + return json; + }, + + fromJSON: function(json, opt) { + + if (!json.cells) { + + throw new Error('Graph JSON must contain cells array.'); + } + + return this.set(json, opt); + }, + + set: function(key, val, opt) { + + var attrs; + + // Handle both `key`, value and {key: value} style arguments. + if (typeof key === 'object') { + attrs = key; + opt = val; + } else { + (attrs = {})[key] = val; + } + + // Make sure that `cells` attribute is handled separately via resetCells(). + if (attrs.hasOwnProperty('cells')) { + this.resetCells(attrs.cells, opt); + attrs = joint.util.omit(attrs, 'cells'); + } + + // The rest of the attributes are applied via original set method. + return Backbone.Model.prototype.set.call(this, attrs, opt); + }, + + clear: function(opt) { + + opt = joint.util.assign({}, opt, { clear: true }); + + var collection = this.get('cells'); + + if (collection.length === 0) return this; + + this.startBatch('clear', opt); + + // The elements come after the links. + var cells = collection.sortBy(function(cell) { + return cell.isLink() ? 1 : 2; + }); + + do { + + // Remove all the cells one by one. + // Note that all the links are removed first, so it's + // safe to remove the elements without removing the connected + // links first. + cells.shift().remove(opt); + + } while (cells.length > 0); + + this.stopBatch('clear'); + + return this; + }, + + _prepareCell: function(cell, opt) { + + var attrs; + if (cell instanceof Backbone.Model) { + attrs = cell.attributes; + if (!cell.graph && (!opt || !opt.dry)) { + // An element can not be member of more than one graph. + // A cell stops being the member of the graph after it's explicitely removed. + cell.graph = this; + } + } else { + // In case we're dealing with a plain JS object, we have to set the reference + // to the `graph` right after the actual model is created. This happens in the `model()` function + // of `joint.dia.GraphCells`. + attrs = cell; + } + + if (!joint.util.isString(attrs.type)) { + throw new TypeError('dia.Graph: cell type must be a string.'); + } + + return cell; + }, + + minZIndex: function() { + + var firstCell = this.get('cells').first(); + return firstCell ? (firstCell.get('z') || 0) : 0; + }, + + maxZIndex: function() { + + var lastCell = this.get('cells').last(); + return lastCell ? (lastCell.get('z') || 0) : 0; + }, + + addCell: function(cell, opt) { + + if (Array.isArray(cell)) { + + return this.addCells(cell, opt); + } + + if (cell instanceof Backbone.Model) { + + if (!cell.has('z')) { + cell.set('z', this.maxZIndex() + 1); + } + + } else if (cell.z === undefined) { + + cell.z = this.maxZIndex() + 1; + } + + this.get('cells').add(this._prepareCell(cell, opt), opt || {}); + + return this; + }, + + addCells: function(cells, opt) { + + if (cells.length) { + + cells = joint.util.flattenDeep(cells); + opt.position = cells.length; + + this.startBatch('add'); + cells.forEach(function(cell) { + opt.position--; + this.addCell(cell, opt); + }, this); + this.stopBatch('add'); + } + + return this; + }, + + // When adding a lot of cells, it is much more efficient to + // reset the entire cells collection in one go. + // Useful for bulk operations and optimizations. + resetCells: function(cells, opt) { + + var preparedCells = joint.util.toArray(cells).map(function(cell) { + return this._prepareCell(cell, opt); + }, this); + this.get('cells').reset(preparedCells, opt); + + return this; + }, + + removeCells: function(cells, opt) { + + if (cells.length) { + + this.startBatch('remove'); + joint.util.invoke(cells, 'remove', opt); + this.stopBatch('remove'); + } + + return this; + }, + + _removeCell: function(cell, collection, options) { + + options = options || {}; + + if (!options.clear) { + // Applications might provide a `disconnectLinks` option set to `true` in order to + // disconnect links when a cell is removed rather then removing them. The default + // is to remove all the associated links. + if (options.disconnectLinks) { + + this.disconnectLinks(cell, options); + + } else { + + this.removeLinks(cell, options); + } + } + // Silently remove the cell from the cells collection. Silently, because + // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is + // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events + // would be triggered on the graph model. + this.get('cells').remove(cell, { silent: true }); + + if (cell.graph === this) { + // Remove the element graph reference only if the cell is the member of this graph. + cell.graph = null; + } + }, + + // Get a cell by `id`. + getCell: function(id) { + + return this.get('cells').get(id); + }, + + getCells: function() { + + return this.get('cells').toArray(); + }, + + getElements: function() { + return Object.keys(this._nodes).map(this.getCell, this); + }, + + getLinks: function() { + return Object.keys(this._edges).map(this.getCell, this); + }, + + getFirstCell: function() { + + return this.get('cells').first(); + }, + + getLastCell: function() { + + return this.get('cells').last(); + }, + + // Get all inbound and outbound links connected to the cell `model`. + getConnectedLinks: function(model, opt) { + + opt = opt || {}; + + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } + + // The final array of connected link models. + var links = []; + // Connected edges. This hash table ([edge] -> true) serves only + // for a quick lookup to check if we already added a link. + var edges = {}; + + if (outbound) { + joint.util.forIn(this.getOutboundEdges(model.id), function(exists, edge) { + if (!edges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + if (inbound) { + joint.util.forIn(this.getInboundEdges(model.id), function(exists, edge) { + // Skip links that were already added. Those must be self-loop links + // because they are both inbound and outbond edges of the same element. + if (!edges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + + // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells + // and are not descendents themselves. + if (opt.deep) { + + var embeddedCells = model.getEmbeddedCells({ deep: true }); + // In the first round, we collect all the embedded edges so that we can exclude + // them from the final result. + var embeddedEdges = {}; + embeddedCells.forEach(function(cell) { + if (cell.isLink()) { + embeddedEdges[cell.id] = true; + } + }); + embeddedCells.forEach(function(cell) { + if (cell.isLink()) return; + if (outbound) { + joint.util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { + if (!edges[edge] && !embeddedEdges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + if (inbound) { + joint.util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { + if (!edges[edge] && !embeddedEdges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + }, this); + } + + return links; + }, + + getNeighbors: function(model, opt) { + + opt = opt || {}; + + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } + + var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { + + var source = link.get('source'); + var target = link.get('target'); + var loop = link.hasLoop(opt); + + // Discard if it is a point, or if the neighbor was already added. + if (inbound && joint.util.has(source, 'id') && !res[source.id]) { + + var sourceElement = this.getCell(source.id); + + if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { + res[source.id] = sourceElement; + } + } + + // Discard if it is a point, or if the neighbor was already added. + if (outbound && joint.util.has(target, 'id') && !res[target.id]) { + + var targetElement = this.getCell(target.id); + + if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { + res[target.id] = targetElement; + } + } + + return res; + }.bind(this), {}); + + return joint.util.toArray(neighbors); + }, + + getCommonAncestor: function(/* cells */) { + + var cellsAncestors = Array.from(arguments).map(function(cell) { + + var ancestors = []; + var parentId = cell.get('parent'); + + while (parentId) { + + ancestors.push(parentId); + parentId = this.getCell(parentId).get('parent'); + } + + return ancestors; + + }, this); + + cellsAncestors = cellsAncestors.sort(function(a, b) { + return a.length - b.length; + }); + + var commonAncestor = joint.util.toArray(cellsAncestors.shift()).find(function(ancestor) { + return cellsAncestors.every(function(cellAncestors) { + return cellAncestors.includes(ancestor); + }); + }); + + return this.getCell(commonAncestor); + }, + + // Find the whole branch starting at `element`. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. + getSuccessors: function(element, opt) { + + opt = opt || {}; + var res = []; + // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. + this.search(element, function(el) { + if (el !== element) { + res.push(el); + } + }, joint.util.assign({}, opt, { outbound: true })); + return res; + }, + + // Clone `cells` returning an object that maps the original cell ID to the clone. The number + // of clones is exactly the same as the `cells.length`. + // This function simply clones all the `cells`. However, it also reconstructs + // all the `source/target` and `parent/embed` references within the `cells`. + // This is the main difference from the `cell.clone()` method. The + // `cell.clone()` method works on one single cell only. + // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` + // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. + // the source and target of the link `L2` is changed to point to `A2` and `B2`. + cloneCells: function(cells) { + + cells = joint.util.uniq(cells); + + // A map of the form [original cell ID] -> [clone] helping + // us to reconstruct references for source/target and parent/embeds. + // This is also the returned value. + var cloneMap = joint.util.toArray(cells).reduce(function(map, cell) { + map[cell.id] = cell.clone(); + return map; + }, {}); + + joint.util.toArray(cells).forEach(function(cell) { + + var clone = cloneMap[cell.id]; + // assert(clone exists) + + if (clone.isLink()) { + var source = clone.get('source'); + var target = clone.get('target'); + if (source.id && cloneMap[source.id]) { + // Source points to an element and the element is among the clones. + // => Update the source of the cloned link. + clone.prop('source/id', cloneMap[source.id].id); + } + if (target.id && cloneMap[target.id]) { + // Target points to an element and the element is among the clones. + // => Update the target of the cloned link. + clone.prop('target/id', cloneMap[target.id].id); + } + } + + // Find the parent of the original cell + var parent = cell.get('parent'); + if (parent && cloneMap[parent]) { + clone.set('parent', cloneMap[parent].id); + } + + // Find the embeds of the original cell + var embeds = joint.util.toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) { + // Embedded cells that are not being cloned can not be carried + // over with other embedded cells. + if (cloneMap[embed]) { + newEmbeds.push(cloneMap[embed].id); + } + return newEmbeds; + }, []); + + if (!joint.util.isEmpty(embeds)) { + clone.set('embeds', embeds); + } + }); + + return cloneMap; + }, + + // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). + // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. + // Return a map of the form: [original cell ID] -> [clone]. + cloneSubgraph: function(cells, opt) { + + var subgraph = this.getSubgraph(cells, opt); + return this.cloneCells(subgraph); + }, + + // Return `cells` and all the connected links that connect cells in the `cells` array. + // If `opt.deep` is `true`, return all the cells including all their embedded cells + // and all the links that connect any of the returned cells. + // For example, for a single shallow element, the result is that very same element. + // For two elements connected with a link: `A --- L ---> B`, the result for + // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. + getSubgraph: function(cells, opt) { + + opt = opt || {}; + + var subgraph = []; + // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. + var cellMap = {}; + var elements = []; + var links = []; joint.util.toArray(cells).forEach(function(cell) { + if (!cellMap[cell.id]) { + subgraph.push(cell); + cellMap[cell.id] = cell; + if (cell.isLink()) { + links.push(cell); + } else { + elements.push(cell); + } + } + + if (opt.deep) { + var embeds = cell.getEmbeddedCells({ deep: true }); + embeds.forEach(function(embed) { + if (!cellMap[embed.id]) { + subgraph.push(embed); + cellMap[embed.id] = embed; + if (embed.isLink()) { + links.push(embed); + } else { + elements.push(embed); + } + } + }); + } + }); + + links.forEach(function(link) { + // For links, return their source & target (if they are elements - not points). + var source = link.get('source'); + var target = link.get('target'); + if (source.id && !cellMap[source.id]) { + var sourceElement = this.getCell(source.id); + subgraph.push(sourceElement); + cellMap[sourceElement.id] = sourceElement; + elements.push(sourceElement); + } + if (target.id && !cellMap[target.id]) { + var targetElement = this.getCell(target.id); + subgraph.push(this.getCell(target.id)); + cellMap[targetElement.id] = targetElement; + elements.push(targetElement); + } + }, this); + + elements.forEach(function(element) { + // For elements, include their connected links if their source/target is in the subgraph; + var links = this.getConnectedLinks(element, opt); + links.forEach(function(link) { + var source = link.get('source'); + var target = link.get('target'); + if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { + subgraph.push(link); + cellMap[link.id] = link; + } + }); + }, this); + + return subgraph; + }, + + // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. + getPredecessors: function(element, opt) { + + opt = opt || {}; + var res = []; + // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. + this.search(element, function(el) { + if (el !== element) { + res.push(el); + } + }, joint.util.assign({}, opt, { inbound: true })); + return res; + }, + + // Perform search on the graph. + // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. + // By setting `opt.inbound` to `true`, you can reverse the direction of the search. + // If `opt.deep` is `true`, take into account embedded elements too. + // `iteratee` is a function of the form `function(element) {}`. + // If `iteratee` explicitely returns `false`, the searching stops. + search: function(element, iteratee, opt) { + + opt = opt || {}; + if (opt.breadthFirst) { + this.bfs(element, iteratee, opt); + } else { + this.dfs(element, iteratee, opt); + } + }, + + // Breadth-first search. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). + // `iteratee` is a function of the form `function(element, distance) {}`. + // where `element` is the currently visited element and `distance` is the distance of that element + // from the root `element` passed the `bfs()`, i.e. the element we started the search from. + // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels + // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. + // If `iteratee` explicitely returns `false`, the searching stops. + bfs: function(element, iteratee, opt) { + + opt = opt || {}; + var visited = {}; + var distance = {}; + var queue = []; + + queue.push(element); + distance[element.id] = 0; + + while (queue.length > 0) { + var next = queue.shift(); + if (!visited[next.id]) { + visited[next.id] = true; + if (iteratee(next, distance[next.id]) === false) return; + this.getNeighbors(next, opt).forEach(function(neighbor) { + distance[neighbor.id] = distance[next.id] + 1; + queue.push(neighbor); + }); + } + } + }, + + // Depth-first search. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). + // `iteratee` is a function of the form `function(element, distance) {}`. + // If `iteratee` explicitely returns `false`, the search stops. + dfs: function(element, iteratee, opt, _visited, _distance) { + + opt = opt || {}; + var visited = _visited || {}; + var distance = _distance || 0; + if (iteratee(element, distance) === false) return; + visited[element.id] = true; + + this.getNeighbors(element, opt).forEach(function(neighbor) { + if (!visited[neighbor.id]) { + this.dfs(neighbor, iteratee, opt, visited, distance + 1); + } + }, this); + }, + + // Get all the roots of the graph. Time complexity: O(|V|). + getSources: function() { + + var sources = []; + joint.util.forIn(this._nodes, function(exists, node) { + if (!this._in[node] || joint.util.isEmpty(this._in[node])) { + sources.push(this.getCell(node)); + } + }.bind(this)); + return sources; + }, + + // Get all the leafs of the graph. Time complexity: O(|V|). + getSinks: function() { + + var sinks = []; + joint.util.forIn(this._nodes, function(exists, node) { + if (!this._out[node] || joint.util.isEmpty(this._out[node])) { + sinks.push(this.getCell(node)); + } + }.bind(this)); + return sinks; + }, + + // Return `true` if `element` is a root. Time complexity: O(1). + isSource: function(element) { + + return !this._in[element.id] || joint.util.isEmpty(this._in[element.id]); + }, + + // Return `true` if `element` is a leaf. Time complexity: O(1). + isSink: function(element) { + + return !this._out[element.id] || joint.util.isEmpty(this._out[element.id]); + }, + + // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. + isSuccessor: function(elementA, elementB) { + + var isSuccessor = false; + this.search(elementA, function(element) { + if (element === elementB && element !== elementA) { + isSuccessor = true; + return false; + } + }, { outbound: true }); + return isSuccessor; + }, + + // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. + isPredecessor: function(elementA, elementB) { + + var isPredecessor = false; + this.search(elementA, function(element) { + if (element === elementB && element !== elementA) { + isPredecessor = true; + return false; + } + }, { inbound: true }); + return isPredecessor; + }, + + // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. + // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` + // for more details. + // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. + // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. + isNeighbor: function(elementA, elementB, opt) { + + opt = opt || {}; + + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } + + var isNeighbor = false; + + this.getConnectedLinks(elementA, opt).forEach(function(link) { + + var source = link.get('source'); + var target = link.get('target'); + + // Discard if it is a point. + if (inbound && joint.util.has(source, 'id') && source.id === elementB.id) { + isNeighbor = true; + return false; + } + + // Discard if it is a point, or if the neighbor was already added. + if (outbound && joint.util.has(target, 'id') && target.id === elementB.id) { + isNeighbor = true; + return false; + } + }); + + return isNeighbor; + }, + + // Disconnect links connected to the cell `model`. + disconnectLinks: function(model, opt) { + + this.getConnectedLinks(model).forEach(function(link) { + + link.set(link.get('source').id === model.id ? 'source' : 'target', { x: 0, y: 0 }, opt); + }); + }, + + // Remove links connected to the cell `model` completely. + removeLinks: function(model, opt) { + + joint.util.invoke(this.getConnectedLinks(model), 'remove', opt); + }, + + // Find all elements at given point + findModelsFromPoint: function(p) { + + return this.getElements().filter(function(el) { + return el.getBBox().containsPoint(p); + }); + }, + + // Find all elements in given area + findModelsInArea: function(rect, opt) { + + rect = g.rect(rect); + opt = joint.util.defaults(opt || {}, { strict: false }); + + var method = opt.strict ? 'containsRect' : 'intersect'; + + return this.getElements().filter(function(el) { + return rect[method](el.getBBox()); + }); + }, + + // Find all elements under the given element. + findModelsUnderElement: function(element, opt) { + + opt = joint.util.defaults(opt || {}, { searchBy: 'bbox' }); + + var bbox = element.getBBox(); + var elements = (opt.searchBy === 'bbox') + ? this.findModelsInArea(bbox) + : this.findModelsFromPoint(bbox[opt.searchBy]()); + + // don't account element itself or any of its descendents + return elements.filter(function(el) { + return element.id !== el.id && !el.isEmbeddedIn(element); + }); + }, + + + // Return bounding box of all elements. + getBBox: function(cells, opt) { + + return this.getCellsBBox(cells || this.getElements(), opt); + }, + + // Return the bounding box of all cells in array provided. + // Links are being ignored. + getCellsBBox: function(cells, opt) { + + return joint.util.toArray(cells).reduce(function(memo, cell) { + if (cell.isLink()) return memo; + if (memo) { + return memo.union(cell.getBBox(opt)); + } else { + return cell.getBBox(opt); + } + }, null); + }, + + translate: function(dx, dy, opt) { + + // Don't translate cells that are embedded in any other cell. + var cells = this.getCells().filter(function(cell) { + return !cell.isEmbedded(); + }); + + joint.util.invoke(cells, 'translate', dx, dy, opt); + + return this; + }, + + resize: function(width, height, opt) { + + return this.resizeCells(width, height, this.getCells(), opt); + }, + + resizeCells: function(width, height, cells, opt) { + + // `getBBox` method returns `null` if no elements provided. + // i.e. cells can be an array of links + var bbox = this.getCellsBBox(cells); + if (bbox) { + var sx = Math.max(width / bbox.width, 0); + var sy = Math.max(height / bbox.height, 0); + joint.util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); + } + + return this; + }, + + startBatch: function(name, data) { + + data = data || {}; + this._batches[name] = (this._batches[name] || 0) + 1; + + return this.trigger('batch:start', joint.util.assign({}, data, { batchName: name })); + }, + + stopBatch: function(name, data) { + + data = data || {}; + this._batches[name] = (this._batches[name] || 0) - 1; + + return this.trigger('batch:stop', joint.util.assign({}, data, { batchName: name })); + }, + + hasActiveBatch: function(name) { + if (arguments.length === 0) { + return joint.util.toArray(this._batches).some(function(batches) { + return batches > 0; + }); + } + if (Array.isArray(name)) { + return name.some(function(name) { + return !!this._batches[name]; + }, this); + } + return !!this._batches[name]; + } + +}, { + + validations: { + + multiLinks: function(graph, link) { + + // Do not allow multiple links to have the same source and target. + var source = link.get('source'); + var target = link.get('target'); + + if (source.id && target.id) { + + var sourceModel = link.getSourceElement(); + if (sourceModel) { + + var connectedLinks = graph.getConnectedLinks(sourceModel, { outbound: true }); + var sameLinks = connectedLinks.filter(function(_link) { + + var _source = _link.get('source'); + var _target = _link.get('target'); + + return _source && _source.id === source.id && + (!_source.port || (_source.port === source.port)) && + _target && _target.id === target.id && + (!_target.port || (_target.port === target.port)); + + }); + + if (sameLinks.length > 1) { + return false; + } + } + } + + return true; + }, + + linkPinning: function(graph, link) { + return link.source().id && link.target().id; + } + } + +}); + +joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); + +(function(joint, V, g, $, util) { + + function setWrapper(attrName, dimension) { + return function(value, refBBox) { + var isValuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (isValuePercentage) { + value /= 100; + } + + var attrs = {}; + if (isFinite(value)) { + var attrValue = (isValuePercentage || value >= 0 && value <= 1) + ? value * refBBox[dimension] + : Math.max(value + refBBox[dimension], 0); + attrs[attrName] = attrValue; + } + + return attrs; + }; + } + + function positionWrapper(axis, dimension, origin) { + return function(value, refBBox) { + var valuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (valuePercentage) { + value /= 100; + } + + var delta; + if (isFinite(value)) { + var refOrigin = refBBox[origin](); + if (valuePercentage || value > 0 && value < 1) { + delta = refOrigin[axis] + refBBox[dimension] * value; + } else { + delta = refOrigin[axis] + value; + } + } + + var point = g.Point(); + point[axis] = delta || 0; + return point; + }; + } + + function offsetWrapper(axis, dimension, corner) { + return function(value, nodeBBox) { + var delta; + if (value === 'middle') { + delta = nodeBBox[dimension] / 2; + } else if (value === corner) { + delta = nodeBBox[dimension]; + } else if (isFinite(value)) { + // TODO: or not to do a breaking change? + delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value; + } else if (util.isPercentage(value)) { + delta = nodeBBox[dimension] * parseFloat(value) / 100; + } else { + delta = 0; + } + + var point = g.Point(); + point[axis] = -(nodeBBox[axis] + delta); + return point; + }; + } + + function shapeWrapper(shapeConstructor, opt) { + var cacheName = 'joint-shape'; + var resetOffset = opt && opt.resetOffset; + return function(value, refBBox, node) { + var $node = $(node); + var cache = $node.data(cacheName); + if (!cache || cache.value !== value) { + // only recalculate if value has changed + var cachedShape = shapeConstructor(value); + cache = { + value: value, + shape: cachedShape, + shapeBBox: cachedShape.bbox() + }; + $node.data(cacheName, cache); + } + + var shape = cache.shape.clone(); + var shapeBBox = cache.shapeBBox.clone(); + var shapeOrigin = shapeBBox.origin(); + var refOrigin = refBBox.origin(); + + shapeBBox.x = refOrigin.x; + shapeBBox.y = refOrigin.y; + + var fitScale = refBBox.maxRectScaleToFit(shapeBBox, refOrigin); + // `maxRectScaleToFit` can give Infinity if width or height is 0 + var sx = (shapeBBox.width === 0 || refBBox.width === 0) ? 1 : fitScale.sx; + var sy = (shapeBBox.height === 0 || refBBox.height === 0) ? 1 : fitScale.sy; + + shape.scale(sx, sy, shapeOrigin); + if (resetOffset) { + shape.translate(-shapeOrigin.x, -shapeOrigin.y); + } + + return shape; + }; + } + + // `d` attribute for SVGPaths + function dWrapper(opt) { + function pathConstructor(value) { + return new g.Path(V.normalizePathData(value)); + } + var shape = shapeWrapper(pathConstructor, opt); + return function(value, refBBox, node) { + var path = shape(value, refBBox, node); + return { + d: path.serialize() + }; + }; + } + + // `points` attribute for SVGPolylines and SVGPolygons + function pointsWrapper(opt) { + var shape = shapeWrapper(g.Polyline, opt); + return function(value, refBBox, node) { + var polyline = shape(value, refBBox, node); + return { + points: polyline.serialize() + }; + }; + } + + function atConnectionWrapper(method, opt) { + var zeroVector = new g.Point(1, 0); + return function(value) { + var p, angle; + var tangent = this[method](value); + if (tangent) { + angle = (opt.rotate) ? tangent.vector().vectorAngle(zeroVector) : 0; + p = tangent.start; + } else { + p = this.path.start; + angle = 0; + } + if (angle === 0) return { transform: 'translate(' + p.x + ',' + p.y + ')' }; + return { transform: 'translate(' + p.x + ',' + p.y + ') rotate(' + angle + ')' }; + } + } + + function isTextInUse(lineHeight, node, attrs) { + return (attrs.text !== undefined); + } + + function isLinkView() { + return this instanceof joint.dia.LinkView; + } + + function contextMarker(context) { + var marker = {}; + // Stroke + // The context 'fill' is disregared here. The usual case is to use the marker with a connection + // (for which 'fill' attribute is set to 'none'). + var stroke = context.stroke; + if (typeof stroke === 'string') { + marker['stroke'] = stroke; + marker['fill'] = stroke; + } + // Opacity + // Again the context 'fill-opacity' is ignored. + var strokeOpacity = context.strokeOpacity; + if (strokeOpacity === undefined) strokeOpacity = context['stroke-opacity']; + if (strokeOpacity === undefined) strokeOpacity = context.opacity + if (strokeOpacity !== undefined) { + marker['stroke-opacity'] = strokeOpacity; + marker['fill-opacity'] = strokeOpacity; + } + return marker; + } + + var attributesNS = joint.dia.attributes = { + + xlinkHref: { + set: 'xlink:href' + }, + + xlinkShow: { + set: 'xlink:show' + }, + + xlinkRole: { + set: 'xlink:role' + }, + + xlinkType: { + set: 'xlink:type' + }, + + xlinkArcrole: { + set: 'xlink:arcrole' + }, + + xlinkTitle: { + set: 'xlink:title' + }, + + xlinkActuate: { + set: 'xlink:actuate' + }, + + xmlSpace: { + set: 'xml:space' + }, + + xmlBase: { + set: 'xml:base' + }, + + xmlLang: { + set: 'xml:lang' + }, + + preserveAspectRatio: { + set: 'preserveAspectRatio' + }, + + requiredExtension: { + set: 'requiredExtension' + }, + + requiredFeatures: { + set: 'requiredFeatures' + }, + + systemLanguage: { + set: 'systemLanguage' + }, + + externalResourcesRequired: { + set: 'externalResourceRequired' + }, + + filter: { + qualify: util.isPlainObject, + set: function(filter) { + return 'url(#' + this.paper.defineFilter(filter) + ')'; + } + }, + + fill: { + qualify: util.isPlainObject, + set: function(fill) { + return 'url(#' + this.paper.defineGradient(fill) + ')'; + } + }, + + stroke: { + qualify: util.isPlainObject, + set: function(stroke) { + return 'url(#' + this.paper.defineGradient(stroke) + ')'; + } + }, + + sourceMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), marker); + return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, + + targetMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), { 'transform': 'rotate(180)' }, marker); + return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, + + vertexMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), marker); + return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, + + text: { + qualify: function(text, node, attrs) { + return !attrs.textWrap || !util.isPlainObject(attrs.textWrap); + }, + set: function(text, refBBox, node, attrs) { + var $node = $(node); + var cacheName = 'joint-text'; + var cache = $node.data(cacheName); + var textAttrs = joint.util.pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'textVerticalAnchor', 'eol'); + var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize']; + var textHash = JSON.stringify([text, textAttrs]); + // Update the text only if there was a change in the string + // or any of its attributes. + if (cache === undefined || cache !== textHash) { + // Chrome bug: + // Tspans positions defined as `em` are not updated + // when container `font-size` change. + if (fontSize) node.setAttribute('font-size', fontSize); + // Text Along Path Selector + var textPath = textAttrs.textPath; + if (util.isObject(textPath)) { + var pathSelector = textPath.selector; + if (typeof pathSelector === 'string') { + var pathNode = this.findBySelector(pathSelector)[0]; + if (pathNode instanceof SVGPathElement) { + textAttrs.textPath = util.assign({ 'xlink:href': '#' + pathNode.id }, textPath); + } + } + } + V(node).text('' + text, textAttrs); + $node.data(cacheName, textHash); + } + } + }, + + textWrap: { + qualify: util.isPlainObject, + set: function(value, refBBox, node, attrs) { + // option `width` + var width = value.width || 0; + if (util.isPercentage(width)) { + refBBox.width *= parseFloat(width) / 100; + } else if (width <= 0) { + refBBox.width += width; + } else { + refBBox.width = width; + } + // option `height` + var height = value.height || 0; + if (util.isPercentage(height)) { + refBBox.height *= parseFloat(height) / 100; + } else if (height <= 0) { + refBBox.height += height; + } else { + refBBox.height = height; + } + // option `text` + var text = value.text; + if (text === undefined) text = attr.text; + if (text !== undefined) { + var wrappedText = joint.util.breakText('' + text, refBBox, { + 'font-weight': attrs['font-weight'] || attrs.fontWeight, + 'font-size': attrs['font-size'] || attrs.fontSize, + 'font-family': attrs['font-family'] || attrs.fontFamily, + 'lineHeight': attrs.lineHeight + }, { + // Provide an existing SVG Document here + // instead of creating a temporary one over again. + svgDocument: this.paper.svg + }); + } + joint.dia.attributes.text.set.call(this, wrappedText, refBBox, node, attrs); + } + }, + + title: { + qualify: function(title, node) { + // HTMLElement title is specified via an attribute (i.e. not an element) + return node instanceof SVGElement; + }, + set: function(title, refBBox, node) { + var $node = $(node); + var cacheName = 'joint-title'; + var cache = $node.data(cacheName); + if (cache === undefined || cache !== title) { + $node.data(cacheName, title); + // Generally element should be the first child element of its parent. + var firstChild = node.firstChild; + if (firstChild && firstChild.tagName.toUpperCase() === 'TITLE') { + // Update an existing title + firstChild.textContent = title; + } else { + // Create a new title + var titleNode = document.createElementNS(node.namespaceURI, 'title'); + titleNode.textContent = title; + node.insertBefore(titleNode, firstChild); + } + } + } + }, + + lineHeight: { + qualify: isTextInUse + }, + + textVerticalAnchor: { + qualify: isTextInUse + }, + + textPath: { + qualify: isTextInUse + }, + + annotations: { + qualify: isTextInUse + }, + + // `port` attribute contains the `id` of the port that the underlying magnet represents. + port: { + set: function(port) { + return (port === null || port.id === undefined) ? port : port.id; + } + }, + + // `style` attribute is special in the sense that it sets the CSS style of the subelement. + style: { + qualify: util.isPlainObject, + set: function(styles, refBBox, node) { + $(node).css(styles); + } + }, + + html: { + set: function(html, refBBox, node) { + $(node).html(html + ''); + } + }, + + ref: { + // We do not set `ref` attribute directly on an element. + // The attribute itself does not qualify for relative positioning. + }, + + // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width + // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box + // otherwise, `refX` is the left coordinate of the bounding box + + refX: { + position: positionWrapper('x', 'width', 'origin') + }, + + refY: { + position: positionWrapper('y', 'height', 'origin') + }, + + // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom + // coordinate of the reference element. + + refDx: { + position: positionWrapper('x', 'width', 'corner') + }, + + refDy: { + position: positionWrapper('y', 'height', 'corner') + }, + + // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to + // the reference element size + // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width + // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 + + refWidth: { + set: setWrapper('width', 'width') + }, + + refHeight: { + set: setWrapper('height', 'height') + }, + + refRx: { + set: setWrapper('rx', 'width') + }, + + refRy: { + set: setWrapper('ry', 'height') + }, + + refRInscribed: { + set: (function(attrName) { + var widthFn = setWrapper(attrName, 'width'); + var heightFn = setWrapper(attrName, 'height'); + return function(value, refBBox) { + var fn = (refBBox.height > refBBox.width) ? widthFn : heightFn; + return fn(value, refBBox); + } + })('r') + }, + + refRCircumscribed: { + set: function(value, refBBox) { + var isValuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (isValuePercentage) { + value /= 100; + } + + var diagonalLength = Math.sqrt((refBBox.height * refBBox.height) + (refBBox.width * refBBox.width)); + + var rValue; + if (isFinite(value)) { + if (isValuePercentage || value >= 0 && value <= 1) rValue = value * diagonalLength; + else rValue = Math.max(value + diagonalLength, 0); + } + + return { r: rValue }; + } + }, + + refCx: { + set: setWrapper('cx', 'width') + }, + + refCy: { + set: setWrapper('cy', 'height') + }, + + // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. + // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. + + xAlignment: { + offset: offsetWrapper('x', 'width', 'right') + }, + + // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. + // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. + + yAlignment: { + offset: offsetWrapper('y', 'height', 'bottom') + }, + + resetOffset: { + offset: function(val, nodeBBox) { + return (val) + ? { x: -nodeBBox.x, y: -nodeBBox.y } + : { x: 0, y: 0 }; + } + + }, + + refDResetOffset: { + set: dWrapper({ resetOffset: true }) + }, + + refDKeepOffset: { + set: dWrapper({ resetOffset: false }) + }, + + refPointsResetOffset: { + set: pointsWrapper({ resetOffset: true }) + }, + + refPointsKeepOffset: { + set: pointsWrapper({ resetOffset: false }) + }, + + // LinkView Attributes + + connection: { + qualify: isLinkView, + set: function() { + return { d: this.getSerializedConnection() }; + } + }, + + atConnectionLengthKeepGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtLength', { rotate: true }) + }, + + atConnectionLengthIgnoreGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtLength', { rotate: false }) + }, + + atConnectionRatioKeepGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtRatio', { rotate: true }) + }, + + atConnectionRatioIgnoreGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtRatio', { rotate: false }) + } + }; + + // Aliases + attributesNS.refR = attributesNS.refRInscribed; + attributesNS.refD = attributesNS.refDResetOffset; + attributesNS.refPoints = attributesNS.refPointsResetOffset; + attributesNS.atConnectionLength = attributesNS.atConnectionLengthKeepGradient; + attributesNS.atConnectionRatio = attributesNS.atConnectionRatioKeepGradient; + + // This allows to combine both absolute and relative positioning + // refX: 50%, refX2: 20 + attributesNS.refX2 = attributesNS.refX; + attributesNS.refY2 = attributesNS.refY; + + // Aliases for backwards compatibility + attributesNS['ref-x'] = attributesNS.refX; + attributesNS['ref-y'] = attributesNS.refY; + attributesNS['ref-dy'] = attributesNS.refDy; + attributesNS['ref-dx'] = attributesNS.refDx; + attributesNS['ref-width'] = attributesNS.refWidth; + attributesNS['ref-height'] = attributesNS.refHeight; + attributesNS['x-alignment'] = attributesNS.xAlignment; + attributesNS['y-alignment'] = attributesNS.yAlignment; + +})(joint, V, g, $, joint.util); + +(function(joint, util) { + + var ToolView = joint.mvc.View.extend({ + name: null, + tagName: 'g', + className: 'tool', + svgElement: true, + _visible: true, + + init: function() { + var name = this.name; + if (name) this.vel.attr('data-tool-name', name); + }, + + configure: function(view, toolsView) { + this.relatedView = view; + this.paper = view.paper; + this.parentView = toolsView; + this.simulateRelatedView(this.el); + return this; + }, + + simulateRelatedView: function(el) { + if (el) el.setAttribute('model-id', this.relatedView.model.id); + }, + + getName: function() { + return this.name; + }, + + show: function() { + this.el.style.display = ''; + this._visible = true; + }, + + hide: function() { + this.el.style.display = 'none'; + this._visible = false; + }, + + isVisible: function() { + return !!this._visible; + }, + + focus: function() { + var opacity = this.options.focusOpacity; + if (isFinite(opacity)) this.el.style.opacity = opacity; + this.parentView.focusTool(this); + }, + + blur: function() { + this.el.style.opacity = ''; + this.parentView.blurTool(this); + }, + + update: function() { + // to be overriden + } + }); + + var ToolsView = joint.mvc.View.extend({ + tagName: 'g', + className: 'tools', + svgElement: true, + tools: null, + options: { + tools: null, + relatedView: null, + name: null, + component: false + }, + + configure: function(options) { + options = util.assign(this.options, options); + var tools = options.tools; + if (!Array.isArray(tools)) return this; + var relatedView = options.relatedView; + if (!(relatedView instanceof joint.dia.CellView)) return this; + var views = this.tools = []; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (!(tool instanceof ToolView)) continue; + tool.configure(relatedView, this); + tool.render(); + this.vel.append(tool.el); + views.push(tool); + } + return this; + }, + + getName: function() { + return this.options.name; + }, + + update: function(opt) { + + opt || (opt = {}); + var tools = this.tools; + if (!tools) return; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (opt.tool !== tool.cid && tool.isVisible()) { + tool.update(); + } + } + return this; + }, + + focusTool: function(focusedTool) { + + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (focusedTool === tool) { + tool.show(); + } else { + tool.hide(); + } + } + return this; + }, + + blurTool: function(blurredTool) { + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (tool !== blurredTool && !tool.isVisible()) { + tool.show(); + tool.update(); + } + } + return this; + }, + + hide: function() { + return this.focusTool(null); + }, + + show: function() { + return this.blurTool(null); + }, + + onRemove: function() { + + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + tools[i].remove(); + } + this.tools = null; + }, + + mount: function() { + var options = this.options; + var relatedView = options.relatedView; + if (relatedView) { + var container = (options.component) ? relatedView.el : relatedView.paper.tools; + container.appendChild(this.el); + } + return this; + } + + }); + + joint.dia.ToolsView = ToolsView; + joint.dia.ToolView = ToolView; + +})(joint, joint.util); + + +// joint.dia.Cell base model. +// -------------------------- + +joint.dia.Cell = Backbone.Model.extend({ + + // This is the same as Backbone.Model with the only difference that is uses joint.util.merge + // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. + constructor: function(attributes, options) { + + var defaults; + var attrs = attributes || {}; + this.cid = joint.util.uniqueId('c'); + this.attributes = {}; + if (options && options.collection) this.collection = options.collection; + if (options && options.parse) attrs = this.parse(attrs, options) || {}; + if ((defaults = joint.util.result(this, 'defaults'))) { + //<custom code> + // Replaced the call to _.defaults with joint.util.merge. + attrs = joint.util.merge({}, defaults, attrs); + //</custom code> + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }, + + translate: function(dx, dy, opt) { + + throw new Error('Must define a translate() method.'); + }, + + toJSON: function() { + + var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; + var attrs = this.attributes.attrs; + var finalAttrs = {}; + + // Loop through all the attributes and + // omit the default attributes as they are implicitly reconstructable by the cell 'type'. + joint.util.forIn(attrs, function(attr, selector) { + + var defaultAttr = defaultAttrs[selector]; + + joint.util.forIn(attr, function(value, name) { + + // attr is mainly flat though it might have one more level (consider the `style` attribute). + // Check if the `value` is object and if yes, go one level deep. + if (joint.util.isObject(value) && !Array.isArray(value)) { + + joint.util.forIn(value, function(value2, name2) { + + if (!defaultAttr || !defaultAttr[name] || !joint.util.isEqual(defaultAttr[name][name2], value2)) { + + finalAttrs[selector] = finalAttrs[selector] || {}; + (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; + } + }); + + } else if (!defaultAttr || !joint.util.isEqual(defaultAttr[name], value)) { + // `value` is not an object, default attribute for such a selector does not exist + // or it is different than the attribute value set on the model. + + finalAttrs[selector] = finalAttrs[selector] || {}; + finalAttrs[selector][name] = value; + } + }); + }); + + var attributes = joint.util.cloneDeep(joint.util.omit(this.attributes, 'attrs')); + //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); + attributes.attrs = finalAttrs; + + return attributes; + }, + + initialize: function(options) { + + if (!options || !options.id) { + + this.set('id', joint.util.uuid(), { silent: true }); + } + + this._transitionIds = {}; + + // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. + this.processPorts(); + this.on('change:attrs', this.processPorts, this); + }, + + /** + * @deprecated + */ + processPorts: function() { + + // Whenever `attrs` changes, we extract ports from the `attrs` object and store it + // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` + // set to that port, we remove those links as well (to follow the same behaviour as + // with a removed element). + + var previousPorts = this.ports; + + // Collect ports from the `attrs` object. + var ports = {}; + joint.util.forIn(this.get('attrs'), function(attrs, selector) { + + if (attrs && attrs.port) { + + // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). + if (attrs.port.id !== undefined) { + ports[attrs.port.id] = attrs.port; + } else { + ports[attrs.port] = { id: attrs.port }; + } + } + }); + + // Collect ports that have been removed (compared to the previous ports) - if any. + // Use hash table for quick lookup. + var removedPorts = {}; + joint.util.forIn(previousPorts, function(port, id) { + + if (!ports[id]) removedPorts[id] = true; + }); + + // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. + if (this.graph && !joint.util.isEmpty(removedPorts)) { + + var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); + inboundLinks.forEach(function(link) { + + if (removedPorts[link.get('target').port]) link.remove(); + }); + + var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); + outboundLinks.forEach(function(link) { + + if (removedPorts[link.get('source').port]) link.remove(); + }); + } + + // Update the `ports` object. + this.ports = ports; + }, - var clone = cloneMap[cell.id]; - // assert(clone exists) + remove: function(opt) { - if (clone.isLink()) { - var source = clone.get('source'); - var target = clone.get('target'); - if (source.id && cloneMap[source.id]) { - // Source points to an element and the element is among the clones. - // => Update the source of the cloned link. - clone.prop('source/id', cloneMap[source.id].id); - } - if (target.id && cloneMap[target.id]) { - // Target points to an element and the element is among the clones. - // => Update the target of the cloned link. - clone.prop('target/id', cloneMap[target.id].id); - } + opt = opt || {}; + + // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. + var graph = this.graph; + if (graph) { + graph.startBatch('remove'); + } + + // First, unembed this cell from its parent cell if there is one. + var parentCell = this.getParentCell(); + if (parentCell) parentCell.unembed(this); + + joint.util.invoke(this.getEmbeddedCells(), 'remove', opt); + + this.trigger('remove', this, this.collection, opt); + + if (graph) { + graph.stopBatch('remove'); + } + + return this; + }, + + toFront: function(opt) { + + var graph = this.graph; + if (graph) { + + opt = opt || {}; + + var z = graph.maxZIndex(); + + var cells; + + if (opt.deep) { + cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + cells.unshift(this); + } else { + cells = [this]; } - // Find the parent of the original cell - var parent = cell.get('parent'); - if (parent && cloneMap[parent]) { - clone.set('parent', cloneMap[parent].id); + z = z - cells.length + 1; + + var collection = graph.get('cells'); + var shouldUpdate = (collection.indexOf(this) !== (collection.length - cells.length)); + if (!shouldUpdate) { + shouldUpdate = cells.some(function(cell, index) { + return cell.get('z') !== z + index; + }); } - // Find the embeds of the original cell - var embeds = joint.util.toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) { - // Embedded cells that are not being cloned can not be carried - // over with other embedded cells. - if (cloneMap[embed]) { - newEmbeds.push(cloneMap[embed].id); + if (shouldUpdate) { + this.startBatch('to-front'); + + z = z + cells.length; + + cells.forEach(function(cell, index) { + cell.set('z', z + index, opt); + }); + + this.stopBatch('to-front'); + } + } + + return this; + }, + + toBack: function(opt) { + + var graph = this.graph; + if (graph) { + + opt = opt || {}; + + var z = graph.minZIndex(); + + var cells; + + if (opt.deep) { + cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + cells.unshift(this); + } else { + cells = [this]; + } + + var collection = graph.get('cells'); + var shouldUpdate = (collection.indexOf(this) !== 0); + if (!shouldUpdate) { + shouldUpdate = cells.some(function(cell, index) { + return cell.get('z') !== z + index; + }); + } + + if (shouldUpdate) { + this.startBatch('to-back'); + + z -= cells.length; + + cells.forEach(function(cell, index) { + cell.set('z', z + index, opt); + }); + + this.stopBatch('to-back'); + } + } + + return this; + }, + + parent: function(parent, opt) { + + // getter + if (parent === undefined) return this.get('parent'); + // setter + return this.set('parent', parent, opt); + }, + + embed: function(cell, opt) { + + if (this === cell || this.isEmbeddedIn(cell)) { + + throw new Error('Recursive embedding not allowed.'); + + } else { + + this.startBatch('embed'); + + var embeds = joint.util.assign([], this.get('embeds')); + + // We keep all element ids after link ids. + embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); + + cell.parent(this.id, opt); + this.set('embeds', joint.util.uniq(embeds), opt); + + this.stopBatch('embed'); + } + + return this; + }, + + unembed: function(cell, opt) { + + this.startBatch('unembed'); + + cell.unset('parent', opt); + this.set('embeds', joint.util.without(this.get('embeds'), cell.id), opt); + + this.stopBatch('unembed'); + + return this; + }, + + getParentCell: function() { + + // unlike link.source/target, cell.parent stores id directly as a string + var parentId = this.parent(); + var graph = this.graph; + + return (parentId && graph && graph.getCell(parentId)) || null; + }, + + // Return an array of ancestor cells. + // The array is ordered from the parent of the cell + // to the most distant ancestor. + getAncestors: function() { + + var ancestors = []; + + if (!this.graph) { + return ancestors; + } + + var parentCell = this.getParentCell(); + while (parentCell) { + ancestors.push(parentCell); + parentCell = parentCell.getParentCell(); + } + + return ancestors; + }, + + getEmbeddedCells: function(opt) { + + opt = opt || {}; + + // Cell models can only be retrieved when this element is part of a collection. + // There is no way this element knows about other cells otherwise. + // This also means that calling e.g. `translate()` on an element with embeds before + // adding it to a graph does not translate its embeds. + if (this.graph) { + + var cells; + + if (opt.deep) { + + if (opt.breadthFirst) { + + // breadthFirst algorithm + cells = []; + var queue = this.getEmbeddedCells(); + + while (queue.length > 0) { + + var parent = queue.shift(); + cells.push(parent); + queue.push.apply(queue, parent.getEmbeddedCells()); + } + + } else { + + // depthFirst algorithm + cells = this.getEmbeddedCells(); + cells.forEach(function(cell) { + cells.push.apply(cells, cell.getEmbeddedCells(opt)); + }); } - return newEmbeds; - }, []); - if (!joint.util.isEmpty(embeds)) { - clone.set('embeds', embeds); - } - }); + } else { + + cells = joint.util.toArray(this.get('embeds')).map(this.graph.getCell, this.graph); + } + + return cells; + } + return []; + }, + + isEmbeddedIn: function(cell, opt) { + + var cellId = joint.util.isString(cell) ? cell : cell.id; + var parentId = this.parent(); + + opt = joint.util.defaults({ deep: true }, opt); + + // See getEmbeddedCells(). + if (this.graph && opt.deep) { + + while (parentId) { + if (parentId === cellId) { + return true; + } + parentId = this.graph.getCell(parentId).parent(); + } + + return false; + + } else { + + // When this cell is not part of a collection check + // at least whether it's a direct child of given cell. + return parentId === cellId; + } + }, + + // Whether or not the cell is embedded in any other cell. + isEmbedded: function() { + + return !!this.parent(); + }, + + // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). + // Shallow cloning simply clones the cell and returns a new cell with different ID. + // Deep cloning clones the cell and all its embedded cells recursively. + clone: function(opt) { + + opt = opt || {}; + + if (!opt.deep) { + // Shallow cloning. + + var clone = Backbone.Model.prototype.clone.apply(this, arguments); + // We don't want the clone to have the same ID as the original. + clone.set('id', joint.util.uuid()); + // A shallow cloned element does not carry over the original embeds. + clone.unset('embeds'); + // And can not be embedded in any cell + // as the clone is not part of the graph. + clone.unset('parent'); + + return clone; - return cloneMap; + } else { + // Deep cloning. + + // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. + return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); + } }, - // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). - // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. - // Return a map of the form: [original cell ID] -> [clone]. - cloneSubgraph: function(cells, opt) { + // A convenient way to set nested properties. + // This method merges the properties you'd like to set with the ones + // stored in the cell and makes sure change events are properly triggered. + // You can either set a nested property with one object + // or use a property path. + // The most simple use case is: + // `cell.prop('name/first', 'John')` or + // `cell.prop({ name: { first: 'John' } })`. + // Nested arrays are supported too: + // `cell.prop('series/0/data/0/degree', 50)` or + // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. + prop: function(props, value, opt) { - var subgraph = this.getSubgraph(cells, opt); - return this.cloneCells(subgraph); - }, + var delim = '/'; + var isString = joint.util.isString(props); - // Return `cells` and all the connected links that connect cells in the `cells` array. - // If `opt.deep` is `true`, return all the cells including all their embedded cells - // and all the links that connect any of the returned cells. - // For example, for a single shallow element, the result is that very same element. - // For two elements connected with a link: `A --- L ---> B`, the result for - // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. - getSubgraph: function(cells, opt) { + if (isString || Array.isArray(props)) { + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. - opt = opt || {}; + if (arguments.length > 1) { - var subgraph = []; - // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. - var cellMap = {}; - var elements = []; - var links = []; + var path; + var pathArray; - joint.util.toArray(cells).forEach(function(cell) { - if (!cellMap[cell.id]) { - subgraph.push(cell); - cellMap[cell.id] = cell; - if (cell.isLink()) { - links.push(cell); + if (isString) { + path = props; + pathArray = path.split('/') } else { - elements.push(cell); + path = props.join(delim); + pathArray = props.slice(); } - } - if (opt.deep) { - var embeds = cell.getEmbeddedCells({ deep: true }); - embeds.forEach(function(embed) { - if (!cellMap[embed.id]) { - subgraph.push(embed); - cellMap[embed.id] = embed; - if (embed.isLink()) { - links.push(embed); - } else { - elements.push(embed); - } - } - }); - } - }); + var property = pathArray[0]; + var pathArrayLength = pathArray.length; - links.forEach(function(link) { - // For links, return their source & target (if they are elements - not points). - var source = link.get('source'); - var target = link.get('target'); - if (source.id && !cellMap[source.id]) { - var sourceElement = this.getCell(source.id); - subgraph.push(sourceElement); - cellMap[sourceElement.id] = sourceElement; - elements.push(sourceElement); - } - if (target.id && !cellMap[target.id]) { - var targetElement = this.getCell(target.id); - subgraph.push(this.getCell(target.id)); - cellMap[targetElement.id] = targetElement; - elements.push(targetElement); - } - }, this); + opt = opt || {}; + opt.propertyPath = path; + opt.propertyValue = value; + opt.propertyPathArray = pathArray; - elements.forEach(function(element) { - // For elements, include their connected links if their source/target is in the subgraph; - var links = this.getConnectedLinks(element, opt); - links.forEach(function(link) { - var source = link.get('source'); - var target = link.get('target'); - if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { - subgraph.push(link); - cellMap[link.id] = link; + if (pathArrayLength === 1) { + // Property is not nested. We can simply use `set()`. + return this.set(property, value, opt); } - }); - }, this); - return subgraph; - }, + var update = {}; + // Initialize the nested object. Subobjects are either arrays or objects. + // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. + // Note that this imposes a limitation on object keys one can use with Inspector. + // Pure integer keys will cause issues and are therefore not allowed. + var initializer = update; + var prevProperty = property; - // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. - getPredecessors: function(element, opt) { + for (var i = 1; i < pathArrayLength; i++) { + var pathItem = pathArray[i]; + var isArrayIndex = Number.isFinite(isString ? Number(pathItem) : pathItem); + initializer = initializer[prevProperty] = isArrayIndex ? [] : {}; + prevProperty = pathItem; + } - opt = opt || {}; - var res = []; - // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. - this.search(element, function(el) { - if (el !== element) { - res.push(el); - } - }, joint.util.assign({}, opt, { inbound: true })); - return res; - }, + // Fill update with the `value` on `path`. + update = joint.util.setByPath(update, pathArray, value, '/'); - // Perform search on the graph. - // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. - // By setting `opt.inbound` to `true`, you can reverse the direction of the search. - // If `opt.deep` is `true`, take into account embedded elements too. - // `iteratee` is a function of the form `function(element) {}`. - // If `iteratee` explicitely returns `false`, the searching stops. - search: function(element, iteratee, opt) { + var baseAttributes = joint.util.merge({}, this.attributes); + // if rewrite mode enabled, we replace value referenced by path with + // the new one (we don't merge). + opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); - opt = opt || {}; - if (opt.breadthFirst) { - this.bfs(element, iteratee, opt); - } else { - this.dfs(element, iteratee, opt); + // Merge update with the model attributes. + var attributes = joint.util.merge(baseAttributes, update); + // Finally, set the property to the updated attributes. + return this.set(property, attributes[property], opt); + + } else { + + return joint.util.getByPath(this.attributes, props, delim); + } } + + return this.set(joint.util.merge({}, this.attributes, props), value); }, - // Breadth-first search. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). - // `iteratee` is a function of the form `function(element, distance) {}`. - // where `element` is the currently visited element and `distance` is the distance of that element - // from the root `element` passed the `bfs()`, i.e. the element we started the search from. - // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels - // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. - // If `iteratee` explicitely returns `false`, the searching stops. - bfs: function(element, iteratee, opt) { + // A convient way to unset nested properties + removeProp: function(path, opt) { + // Once a property is removed from the `attrs` attribute + // the cellView will recognize a `dirty` flag and rerender itself + // in order to remove the attribute from SVG element. opt = opt || {}; - var visited = {}; - var distance = {}; - var queue = []; + opt.dirty = true; - queue.push(element); - distance[element.id] = 0; + var pathArray = Array.isArray(path) ? path : path.split('/'); - while (queue.length > 0) { - var next = queue.shift(); - if (!visited[next.id]) { - visited[next.id] = true; - if (iteratee(next, distance[next.id]) === false) return; - this.getNeighbors(next, opt).forEach(function(neighbor) { - distance[neighbor.id] = distance[next.id] + 1; - queue.push(neighbor); - }); - } + if (pathArray.length === 1) { + // A top level property + return this.unset(path, opt); } - }, - // Depth-first search. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). - // `iteratee` is a function of the form `function(element, distance) {}`. - // If `iteratee` explicitely returns `false`, the search stops. - dfs: function(element, iteratee, opt, _visited, _distance) { + // A nested property + var property = pathArray[0]; + var nestedPath = pathArray.slice(1); + var propertyValue = joint.util.cloneDeep(this.get(property)); - opt = opt || {}; - var visited = _visited || {}; - var distance = _distance || 0; - if (iteratee(element, distance) === false) return; - visited[element.id] = true; + joint.util.unsetByPath(propertyValue, nestedPath, '/'); - this.getNeighbors(element, opt).forEach(function(neighbor) { - if (!visited[neighbor.id]) { - this.dfs(neighbor, iteratee, opt, visited, distance + 1); - } - }, this); + return this.set(property, propertyValue, opt); }, - // Get all the roots of the graph. Time complexity: O(|V|). - getSources: function() { + // A convenient way to set nested attributes. + attr: function(attrs, value, opt) { - var sources = []; - joint.util.forIn(this._nodes, function(exists, node) { - if (!this._in[node] || joint.util.isEmpty(this._in[node])) { - sources.push(this.getCell(node)); - } - }.bind(this)); - return sources; - }, + var args = Array.from(arguments); + if (args.length === 0) { + return this.get('attrs'); + } + + if (Array.isArray(attrs)) { + args[0] = ['attrs'].concat(attrs); + } else if (joint.util.isString(attrs)) { + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. + args[0] = 'attrs/' + attrs; - // Get all the leafs of the graph. Time complexity: O(|V|). - getSinks: function() { + } else { - var sinks = []; - joint.util.forIn(this._nodes, function(exists, node) { - if (!this._out[node] || joint.util.isEmpty(this._out[node])) { - sinks.push(this.getCell(node)); - } - }.bind(this)); - return sinks; + args[0] = { 'attrs' : attrs }; + } + + return this.prop.apply(this, args); }, - // Return `true` if `element` is a root. Time complexity: O(1). - isSource: function(element) { + // A convenient way to unset nested attributes + removeAttr: function(path, opt) { - return !this._in[element.id] || joint.util.isEmpty(this._in[element.id]); - }, + if (Array.isArray(path)) { - // Return `true` if `element` is a leaf. Time complexity: O(1). - isSink: function(element) { + return this.removeProp(['attrs'].concat(path)); + } - return !this._out[element.id] || joint.util.isEmpty(this._out[element.id]); + return this.removeProp('attrs/' + path, opt); }, - // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. - isSuccessor: function(elementA, elementB) { + transition: function(path, value, opt, delim) { - var isSuccessor = false; - this.search(elementA, function(element) { - if (element === elementB && element !== elementA) { - isSuccessor = true; - return false; - } - }, { outbound: true }); - return isSuccessor; - }, + delim = delim || '/'; - // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. - isPredecessor: function(elementA, elementB) { + var defaults = { + duration: 100, + delay: 10, + timingFunction: joint.util.timing.linear, + valueFunction: joint.util.interpolate.number + }; - var isPredecessor = false; - this.search(elementA, function(element) { - if (element === elementB && element !== elementA) { - isPredecessor = true; - return false; + opt = joint.util.assign(defaults, opt); + + var firstFrameTime = 0; + var interpolatingFunction; + + var setter = function(runtime) { + + var id, progress, propertyValue; + + firstFrameTime = firstFrameTime || runtime; + runtime -= firstFrameTime; + progress = runtime / opt.duration; + + if (progress < 1) { + this._transitionIds[path] = id = joint.util.nextFrame(setter); + } else { + progress = 1; + delete this._transitionIds[path]; } - }, { inbound: true }); - return isPredecessor; - }, - // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. - // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` - // for more details. - // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. - // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. - isNeighbor: function(elementA, elementB, opt) { + propertyValue = interpolatingFunction(opt.timingFunction(progress)); - opt = opt || {}; + opt.transitionId = id; - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; - } + this.prop(path, propertyValue, opt); - var isNeighbor = false; + if (!id) this.trigger('transition:end', this, path); - this.getConnectedLinks(elementA, opt).forEach(function(link) { + }.bind(this); - var source = link.get('source'); - var target = link.get('target'); + var initiator = function(callback) { - // Discard if it is a point. - if (inbound && joint.util.has(source, 'id') && source.id === elementB.id) { - isNeighbor = true; - return false; - } + this.stopTransitions(path); - // Discard if it is a point, or if the neighbor was already added. - if (outbound && joint.util.has(target, 'id') && target.id === elementB.id) { - isNeighbor = true; - return false; - } - }); + interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); - return isNeighbor; - }, + this._transitionIds[path] = joint.util.nextFrame(callback); - // Disconnect links connected to the cell `model`. - disconnectLinks: function(model, opt) { + this.trigger('transition:start', this, path); - this.getConnectedLinks(model).forEach(function(link) { + }.bind(this); - link.set(link.get('source').id === model.id ? 'source' : 'target', { x: 0, y: 0 }, opt); - }); + return setTimeout(initiator, opt.delay, setter); }, - // Remove links connected to the cell `model` completely. - removeLinks: function(model, opt) { + getTransitions: function() { - joint.util.invoke(this.getConnectedLinks(model), 'remove', opt); + return Object.keys(this._transitionIds); }, - // Find all elements at given point - findModelsFromPoint: function(p) { + stopTransitions: function(path, delim) { - return this.getElements().filter(function(el) { - return el.getBBox().containsPoint(p); - }); - }, + delim = delim || '/'; - // Find all elements in given area - findModelsInArea: function(rect, opt) { + var pathArray = path && path.split(delim); - rect = g.rect(rect); - opt = joint.util.defaults(opt || {}, { strict: false }); + Object.keys(this._transitionIds).filter(pathArray && function(key) { - var method = opt.strict ? 'containsRect' : 'intersect'; + return joint.util.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); - return this.getElements().filter(function(el) { - return rect[method](el.getBBox()); - }); - }, + }).forEach(function(key) { - // Find all elements under the given element. - findModelsUnderElement: function(element, opt) { + joint.util.cancelFrame(this._transitionIds[key]); - opt = joint.util.defaults(opt || {}, { searchBy: 'bbox' }); + delete this._transitionIds[key]; - var bbox = element.getBBox(); - var elements = (opt.searchBy === 'bbox') - ? this.findModelsInArea(bbox) - : this.findModelsFromPoint(bbox[opt.searchBy]()); + this.trigger('transition:end', this, key); - // don't account element itself or any of its descendents - return elements.filter(function(el) { - return element.id !== el.id && !el.isEmbeddedIn(element); - }); + }, this); + + return this; }, + // A shorcut making it easy to create constructs like the following: + // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. + addTo: function(graph, opt) { - // Return bounding box of all elements. - getBBox: function(cells, opt) { + graph.addCell(this, opt); + return this; + }, - return this.getCellsBBox(cells || this.getElements(), opt); + // A shortcut for an equivalent call: `paper.findViewByModel(cell)` + // making it easy to create constructs like the following: + // `cell.findView(paper).highlight()` + findView: function(paper) { + + return paper.findViewByModel(this); }, - // Return the bounding box of all cells in array provided. - // Links are being ignored. - getCellsBBox: function(cells, opt) { + isElement: function() { - return joint.util.toArray(cells).reduce(function(memo, cell) { - if (cell.isLink()) return memo; - if (memo) { - return memo.union(cell.getBBox(opt)); - } else { - return cell.getBBox(opt); - } - }, null); + return false; }, - translate: function(dx, dy, opt) { + isLink: function() { - // Don't translate cells that are embedded in any other cell. - var cells = this.getCells().filter(function(cell) { - return !cell.isEmbedded(); - }); + return false; + }, - joint.util.invoke(cells, 'translate', dx, dy, opt); + startBatch: function(name, opt) { + if (this.graph) { this.graph.startBatch(name, joint.util.assign({}, opt, { cell: this })); } return this; }, - resize: function(width, height, opt) { + stopBatch: function(name, opt) { - return this.resizeCells(width, height, this.getCells(), opt); - }, + if (this.graph) { this.graph.stopBatch(name, joint.util.assign({}, opt, { cell: this })); } + return this; + } - resizeCells: function(width, height, cells, opt) { +}, { - // `getBBox` method returns `null` if no elements provided. - // i.e. cells can be an array of links - var bbox = this.getCellsBBox(cells); - if (bbox) { - var sx = Math.max(width / bbox.width, 0); - var sy = Math.max(height / bbox.height, 0); - joint.util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); - } + getAttributeDefinition: function(attrName) { - return this; + var defNS = this.attributes; + var globalDefNS = joint.dia.attributes; + return (defNS && defNS[attrName]) || globalDefNS[attrName]; }, - startBatch: function(name, data) { + define: function(type, defaults, protoProps, staticProps) { - data = data || {}; - this._batches[name] = (this._batches[name] || 0) + 1; + protoProps = joint.util.assign({ + defaults: joint.util.defaultsDeep({ type: type }, defaults, this.prototype.defaults) + }, protoProps); - return this.trigger('batch:start', joint.util.assign({}, data, { batchName: name })); - }, + var Cell = this.extend(protoProps, staticProps); + joint.util.setByPath(joint.shapes, type, Cell, '.'); + return Cell; + } +}); - stopBatch: function(name, data) { +// joint.dia.CellView base view and controller. +// -------------------------------------------- - data = data || {}; - this._batches[name] = (this._batches[name] || 0) - 1; +// This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. + +joint.dia.CellView = joint.mvc.View.extend({ + + tagName: 'g', + + svgElement: true, + + selector: 'root', + + className: function() { + + var classNames = ['cell']; + var type = this.model.get('type'); - return this.trigger('batch:stop', joint.util.assign({}, data, { batchName: name })); - }, + if (type) { - hasActiveBatch: function(name) { - if (name) { - return !!this._batches[name]; - } else { - return joint.util.toArray(this._batches).some(function(batches) { - return batches > 0; + type.toLowerCase().split('.').forEach(function(value, index, list) { + classNames.push('type-' + list.slice(0, index + 1).join('-')); }); } - } -}); -joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); - -(function(joint, _, g, $, util) { - - function isPercentage(val) { - return util.isString(val) && val.slice(-1) === '%'; - } + return classNames.join(' '); + }, - function setWrapper(attrName, dimension) { - return function(value, refBBox) { - var isValuePercentage = isPercentage(value); - value = parseFloat(value); - if (isValuePercentage) { - value /= 100; - } + attributes: function() { - var attrs = {}; - if (isFinite(value)) { - var attrValue = (isValuePercentage || value >= 0 && value <= 1) - ? value * refBBox[dimension] - : Math.max(value + refBBox[dimension], 0); - attrs[attrName] = attrValue; - } + return { 'model-id': this.model.id }; + }, - return attrs; - }; - } + constructor: function(options) { - function positionWrapper(axis, dimension, origin) { - return function(value, refBBox) { - var valuePercentage = isPercentage(value); - value = parseFloat(value); - if (valuePercentage) { - value /= 100; - } + // Make sure a global unique id is assigned to this view. Store this id also to the properties object. + // The global unique id makes sure that the same view can be rendered on e.g. different machines and + // still be associated to the same object among all those clients. This is necessary for real-time + // collaboration mechanism. + options.id = options.id || joint.util.guid(this); - var delta; - if (isFinite(value)) { - var refOrigin = refBBox[origin](); - if (valuePercentage || value > 0 && value < 1) { - delta = refOrigin[axis] + refBBox[dimension] * value; - } else { - delta = refOrigin[axis] + value; - } - } + joint.mvc.View.call(this, options); + }, - var point = g.Point(); - point[axis] = delta || 0; - return point; - }; - } + init: function() { - function offsetWrapper(axis, dimension, corner) { - return function(value, nodeBBox) { - var delta; - if (value === 'middle') { - delta = nodeBBox[dimension] / 2; - } else if (value === corner) { - delta = nodeBBox[dimension]; - } else if (isFinite(value)) { - // TODO: or not to do a breaking change? - delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value; - } else if (isPercentage(value)) { - delta = nodeBBox[dimension] * parseFloat(value) / 100; - } else { - delta = 0; - } + joint.util.bindAll(this, 'remove', 'update'); - var point = g.Point(); - point[axis] = -(nodeBBox[axis] + delta); - return point; - }; - } + // Store reference to this to the <g> DOM element so that the view is accessible through the DOM tree. + this.$el.data('view', this); - var attributesNS = joint.dia.attributes = { + // Add the cell's type to the view's element as a data attribute. + this.$el.attr('data-type', this.model.get('type')); - xlinkHref: { - set: 'xlink:href' - }, + this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); + }, - xlinkShow: { - set: 'xlink:show' - }, + onChangeAttrs: function(cell, attrs, opt) { - xlinkRole: { - set: 'xlink:role' - }, + if (opt.dirty) { - xlinkType: { - set: 'xlink:type' - }, + // dirty flag could be set when a model attribute was removed and it needs to be cleared + // also from the DOM element. See cell.removeAttr(). + return this.render(); + } - xlinkArcrole: { - set: 'xlink:arcrole' - }, + return this.update(cell, attrs, opt); + }, - xlinkTitle: { - set: 'xlink:title' - }, + // Return `true` if cell link is allowed to perform a certain UI `feature`. + // Example: `can('vertexMove')`, `can('labelMove')`. + can: function(feature) { - xlinkActuate: { - set: 'xlink:actuate' - }, + var interactive = joint.util.isFunction(this.options.interactive) + ? this.options.interactive(this) + : this.options.interactive; - xmlSpace: { - set: 'xml:space' - }, + return (joint.util.isObject(interactive) && interactive[feature] !== false) || + (joint.util.isBoolean(interactive) && interactive !== false); + }, - xmlBase: { - set: 'xml:base' - }, + findBySelector: function(selector, root, selectors) { - xmlLang: { - set: 'xml:lang' - }, + root || (root = this.el); + selectors || (selectors = this.selectors); - preserveAspectRatio: { - set: 'preserveAspectRatio' - }, + // These are either descendants of `this.$el` of `this.$el` itself. + // `.` is a special selector used to select the wrapping `<g>` element. + if (!selector || selector === '.') return [root]; + if (selectors && selectors[selector]) return [selectors[selector]]; + // Maintaining backwards compatibility + // e.g. `circle:first` would fail with querySelector() call + return $(root).find(selector).toArray(); + }, - requiredExtension: { - set: 'requiredExtension' - }, + notify: function(eventName) { - requiredFeatures: { - set: 'requiredFeatures' - }, + if (this.paper) { - systemLanguage: { - set: 'systemLanguage' - }, + var args = Array.prototype.slice.call(arguments, 1); - externalResourcesRequired: { - set: 'externalResourceRequired' - }, + // Trigger the event on both the element itself and also on the paper. + this.trigger.apply(this, [eventName].concat(args)); - filter: { - qualify: util.isPlainObject, - set: function(filter) { - return 'url(#' + this.paper.defineFilter(filter) + ')'; - } - }, + // Paper event handlers receive the view object as the first argument. + this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); + } + }, - fill: { - qualify: util.isPlainObject, - set: function(fill) { - return 'url(#' + this.paper.defineGradient(fill) + ')'; - } - }, + // ** Deprecated ** + getStrokeBBox: function(el) { + // Return a bounding box rectangle that takes into account stroke. + // Note that this is a naive and ad-hoc implementation that does not + // works only in certain cases and should be replaced as soon as browsers will + // start supporting the getStrokeBBox() SVG method. + // @TODO any better solution is very welcome! - stroke: { - qualify: util.isPlainObject, - set: function(stroke) { - return 'url(#' + this.paper.defineGradient(stroke) + ')'; - } - }, + var isMagnet = !!el; - sourceMarker: { - qualify: util.isPlainObject, - set: function(marker) { - return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + el = el || this.el; + var bbox = V(el).getBBox({ target: this.paper.viewport }); + var strokeWidth; + if (isMagnet) { - targetMarker: { - qualify: util.isPlainObject, - set: function(marker) { - marker = util.assign({ transform: 'rotate(180)' }, marker); - return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + strokeWidth = V(el).attr('stroke-width'); - vertexMarker: { - qualify: util.isPlainObject, - set: function(marker) { - return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + } else { - text: { - set: function(text, refBBox, node, attrs) { - var $node = $(node); - var cacheName = 'joint-text'; - var cache = $node.data(cacheName); - var textAttrs = joint.util.pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'eol'); - var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize']; - var textHash = JSON.stringify([text, textAttrs]); - // Update the text only if there was a change in the string - // or any of its attributes. - if (cache === undefined || cache !== textHash) { - // Chrome bug: - // Tspans positions defined as `em` are not updated - // when container `font-size` change. - if (fontSize) { - node.setAttribute('font-size', fontSize); - } - V(node).text('' + text, textAttrs); - $node.data(cacheName, textHash); - } - } - }, + strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); + } - textWrap: { - qualify: util.isPlainObject, - set: function(value, refBBox, node, attrs) { - // option `width` - var width = value.width || 0; - if (isPercentage(width)) { - refBBox.width *= parseFloat(width) / 100; - } else if (width <= 0) { - refBBox.width += width; - } else { - refBBox.width = width; - } - // option `height` - var height = value.height || 0; - if (isPercentage(height)) { - refBBox.height *= parseFloat(height) / 100; - } else if (height <= 0) { - refBBox.height += height; - } else { - refBBox.height = height; - } - // option `text` - var wrappedText = joint.util.breakText('' + value.text, refBBox, { - 'font-weight': attrs['font-weight'] || attrs.fontWeight, - 'font-size': attrs['font-size'] || attrs.fontSize, - 'font-family': attrs['font-family'] || attrs.fontFamily - }, { - // Provide an existing SVG Document here - // instead of creating a temporary one over again. - svgDocument: this.paper.svg - }); + strokeWidth = parseFloat(strokeWidth) || 0; - V(node).text(wrappedText); - } - }, + return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); + }, - lineHeight: { - qualify: function(lineHeight, node, attrs) { - return (attrs.text !== undefined); - } - }, + getBBox: function() { - textPath: { - qualify: function(textPath, node, attrs) { - return (attrs.text !== undefined); - } - }, + return this.vel.getBBox({ target: this.paper.svg }); + }, - annotations: { - qualify: function(annotations, node, attrs) { - return (attrs.text !== undefined); - } - }, + highlight: function(el, opt) { - // `port` attribute contains the `id` of the port that the underlying magnet represents. - port: { - set: function(port) { - return (port === null || port.id === undefined) ? port : port.id; - } - }, + el = !el ? this.el : this.$(el)[0] || this.el; - // `style` attribute is special in the sense that it sets the CSS style of the subelement. - style: { - qualify: util.isPlainObject, - set: function(styles, refBBox, node) { - $(node).css(styles); - } - }, + // set partial flag if the highlighted element is not the entire view. + opt = opt || {}; + opt.partial = (el !== this.el); - html: { - set: function(html, refBBox, node) { - $(node).html(html + ''); - } - }, + this.notify('cell:highlight', el, opt); + return this; + }, - ref: { - // We do not set `ref` attribute directly on an element. - // The attribute itself does not qualify for relative positioning. - }, + unhighlight: function(el, opt) { - // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width - // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box - // otherwise, `refX` is the left coordinate of the bounding box + el = !el ? this.el : this.$(el)[0] || this.el; - refX: { - position: positionWrapper('x', 'width', 'origin') - }, + opt = opt || {}; + opt.partial = el != this.el; - refY: { - position: positionWrapper('y', 'height', 'origin') - }, + this.notify('cell:unhighlight', el, opt); + return this; + }, - // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom - // coordinate of the reference element. + // Find the closest element that has the `magnet` attribute set to `true`. If there was not such + // an element found, return the root element of the cell view. + findMagnet: function(el) { - refDx: { - position: positionWrapper('x', 'width', 'corner') - }, + var $el = this.$(el); + var $rootEl = this.$el; - refDy: { - position: positionWrapper('y', 'height', 'corner') - }, + if ($el.length === 0) { + $el = $rootEl; + } - // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to - // the reference element size - // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width - // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 + do { - refWidth: { - set: setWrapper('width', 'width') - }, + var magnet = $el.attr('magnet'); + if ((magnet || $el.is($rootEl)) && magnet !== 'false') { + return $el[0]; + } - refHeight: { - set: setWrapper('height', 'height') - }, + $el = $el.parent(); - refRx: { - set: setWrapper('rx', 'width') - }, + } while ($el.length > 0); - refRy: { - set: setWrapper('ry', 'height') - }, + // If the overall cell has set `magnet === false`, then return `undefined` to + // announce there is no magnet found for this cell. + // This is especially useful to set on cells that have 'ports'. In this case, + // only the ports have set `magnet === true` and the overall element has `magnet === false`. + return undefined; + }, - refCx: { - set: setWrapper('cx', 'width') - }, + // Construct a unique selector for the `el` element within this view. + // `prevSelector` is being collected through the recursive call. + // No value for `prevSelector` is expected when using this method. + getSelector: function(el, prevSelector) { - refCy: { - set: setWrapper('cy', 'height') - }, + if (el === this.el) { + return prevSelector; + } - // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. - // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. + var selector; - xAlignment: { - offset: offsetWrapper('x', 'width', 'right') - }, + if (el) { - // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. - // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. + var nthChild = V(el).index() + 1; + selector = el.tagName + ':nth-child(' + nthChild + ')'; - yAlignment: { - offset: offsetWrapper('y', 'height', 'bottom') - }, + if (prevSelector) { + selector += ' > ' + prevSelector; + } - resetOffset: { - offset: function(val, nodeBBox) { - return (val) - ? { x: -nodeBBox.x, y: -nodeBBox.y } - : { x: 0, y: 0 }; + selector = this.getSelector(el.parentNode, selector); + } + + return selector; + }, + + getLinkEnd: function(magnet, x, y, link, endType) { + + var model = this.model; + var id = model.id; + var port = this.findAttribute('port', magnet); + // Find a unique `selector` of the element under pointer that is a magnet. + var selector = magnet.getAttribute('joint-selector'); + + var end = { id: id }; + if (selector != null) end.magnet = selector; + if (port != null) { + end.port = port; + if (!model.hasPort(port) && !selector) { + // port created via the `port` attribute (not API) + end.selector = this.getSelector(magnet); } + } else if (selector == null && this.el !== magnet) { + end.selector = this.getSelector(magnet); + } + var paper = this.paper; + var connectionStrategy = paper.options.connectionStrategy; + if (typeof connectionStrategy === 'function') { + var strategy = connectionStrategy.call(paper, end, this, magnet, new g.Point(x, y), link, endType); + if (strategy) end = strategy; } - }; - // This allows to combine both absolute and relative positioning - // refX: 50%, refX2: 20 - attributesNS.refX2 = attributesNS.refX; - attributesNS.refY2 = attributesNS.refY; + return end; + }, - // Aliases for backwards compatibility - attributesNS['ref-x'] = attributesNS.refX; - attributesNS['ref-y'] = attributesNS.refY; - attributesNS['ref-dy'] = attributesNS.refDy; - attributesNS['ref-dx'] = attributesNS.refDx; - attributesNS['ref-width'] = attributesNS.refWidth; - attributesNS['ref-height'] = attributesNS.refHeight; - attributesNS['x-alignment'] = attributesNS.xAlignment; - attributesNS['y-alignment'] = attributesNS.yAlignment; + getMagnetFromLinkEnd: function(end) { -})(joint, _, g, $, joint.util); + var root = this.el; + var port = end.port; + var selector = end.magnet; + var magnet; + if (port != null && this.model.hasPort(port)) { + magnet = this.findPortNode(port, selector) || root; + } else { + magnet = this.findBySelector(selector || end.selector, root, this.selectors)[0]; + } + return magnet; + }, -// joint.dia.Cell base model. -// -------------------------- + findAttribute: function(attributeName, node) { -joint.dia.Cell = Backbone.Model.extend({ + if (!node) return null; - // This is the same as Backbone.Model with the only difference that is uses joint.util.merge - // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. - constructor: function(attributes, options) { + var attributeValue = node.getAttribute(attributeName); + if (attributeValue === null) { + if (node === this.el) return null; + var currentNode = node.parentNode; + while (currentNode && currentNode !== this.el && currentNode.nodeType === 1) { + attributeValue = currentNode.getAttribute(attributeName) + if (attributeValue !== null) break; + currentNode = currentNode.parentNode; + } + } + return attributeValue; + }, - var defaults; - var attrs = attributes || {}; - this.cid = joint.util.uniqueId('c'); - this.attributes = {}; - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attrs = this.parse(attrs, options) || {}; - if ((defaults = joint.util.result(this, 'defaults'))) { - //<custom code> - // Replaced the call to _.defaults with joint.util.merge. - attrs = joint.util.merge({}, defaults, attrs); - //</custom code> + getAttributeDefinition: function(attrName) { + + return this.model.constructor.getAttributeDefinition(attrName); + }, + + setNodeAttributes: function(node, attrs) { + + if (!joint.util.isEmpty(attrs)) { + if (node instanceof SVGElement) { + V(node).attr(attrs); + } else { + $(node).attr(attrs); + } } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); }, - translate: function(dx, dy, opt) { + processNodeAttributes: function(node, attrs) { - throw new Error('Must define a translate() method.'); + var attrName, attrVal, def, i, n; + var normalAttrs, setAttrs, positionAttrs, offsetAttrs; + var relatives = []; + // divide the attributes between normal and special + for (attrName in attrs) { + if (!attrs.hasOwnProperty(attrName)) continue; + attrVal = attrs[attrName]; + def = this.getAttributeDefinition(attrName); + if (def && (!joint.util.isFunction(def.qualify) || def.qualify.call(this, attrVal, node, attrs))) { + if (joint.util.isString(def.set)) { + normalAttrs || (normalAttrs = {}); + normalAttrs[def.set] = attrVal; + } + if (attrVal !== null) { + relatives.push(attrName, def); + } + } else { + normalAttrs || (normalAttrs = {}); + normalAttrs[joint.util.toKebabCase(attrName)] = attrVal; + } + } + + // handle the rest of attributes via related method + // from the special attributes namespace. + for (i = 0, n = relatives.length; i < n; i+=2) { + attrName = relatives[i]; + def = relatives[i+1]; + attrVal = attrs[attrName]; + if (joint.util.isFunction(def.set)) { + setAttrs || (setAttrs = {}); + setAttrs[attrName] = attrVal; + } + if (joint.util.isFunction(def.position)) { + positionAttrs || (positionAttrs = {}); + positionAttrs[attrName] = attrVal; + } + if (joint.util.isFunction(def.offset)) { + offsetAttrs || (offsetAttrs = {}); + offsetAttrs[attrName] = attrVal; + } + } + + return { + raw: attrs, + normal: normalAttrs, + set: setAttrs, + position: positionAttrs, + offset: offsetAttrs + }; }, - toJSON: function() { - - var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; - var attrs = this.attributes.attrs; - var finalAttrs = {}; + updateRelativeAttributes: function(node, attrs, refBBox, opt) { - // Loop through all the attributes and - // omit the default attributes as they are implicitly reconstructable by the cell 'type'. - joint.util.forIn(attrs, function(attr, selector) { + opt || (opt = {}); - var defaultAttr = defaultAttrs[selector]; + var attrName, attrVal, def; + var rawAttrs = attrs.raw || {}; + var nodeAttrs = attrs.normal || {}; + var setAttrs = attrs.set; + var positionAttrs = attrs.position; + var offsetAttrs = attrs.offset; - joint.util.forIn(attr, function(value, name) { + for (attrName in setAttrs) { + attrVal = setAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // SET - set function should return attributes to be set on the node, + // which will affect the node dimensions based on the reference bounding + // box. e.g. `width`, `height`, `d`, `rx`, `ry`, `points + var setResult = def.set.call(this, attrVal, refBBox.clone(), node, rawAttrs); + if (joint.util.isObject(setResult)) { + joint.util.assign(nodeAttrs, setResult); + } else if (setResult !== undefined) { + nodeAttrs[attrName] = setResult; + } + } - // attr is mainly flat though it might have one more level (consider the `style` attribute). - // Check if the `value` is object and if yes, go one level deep. - if (joint.util.isObject(value) && !Array.isArray(value)) { + if (node instanceof HTMLElement) { + // TODO: setting the `transform` attribute on HTMLElements + // via `node.style.transform = 'matrix(...)';` would introduce + // a breaking change (e.g. basic.TextBlock). + this.setNodeAttributes(node, nodeAttrs); + return; + } - joint.util.forIn(value, function(value2, name2) { + // The final translation of the subelement. + var nodeTransform = nodeAttrs.transform; + var nodeMatrix = V.transformStringToMatrix(nodeTransform); + var nodePosition = g.Point(nodeMatrix.e, nodeMatrix.f); + if (nodeTransform) { + nodeAttrs = joint.util.omit(nodeAttrs, 'transform'); + nodeMatrix.e = nodeMatrix.f = 0; + } - if (!defaultAttr || !defaultAttr[name] || !joint.util.isEqual(defaultAttr[name][name2], value2)) { + // Calculate node scale determined by the scalable group + // only if later needed. + var sx, sy, translation; + if (positionAttrs || offsetAttrs) { + var nodeScale = this.getNodeScale(node, opt.scalableNode); + sx = nodeScale.sx; + sy = nodeScale.sy; + } - finalAttrs[selector] = finalAttrs[selector] || {}; - (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; - } - }); + var positioned = false; + for (attrName in positionAttrs) { + attrVal = positionAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // POSITION - position function should return a point from the + // reference bounding box. The default position of the node is x:0, y:0 of + // the reference bounding box or could be further specify by some + // SVG attributes e.g. `x`, `y` + translation = def.position.call(this, attrVal, refBBox.clone(), node, rawAttrs); + if (translation) { + nodePosition.offset(g.Point(translation).scale(sx, sy)); + positioned || (positioned = true); + } + } - } else if (!defaultAttr || !joint.util.isEqual(defaultAttr[name], value)) { - // `value` is not an object, default attribute for such a selector does not exist - // or it is different than the attribute value set on the model. + // The node bounding box could depend on the `size` set from the previous loop. + // Here we know, that all the size attributes have been already set. + this.setNodeAttributes(node, nodeAttrs); - finalAttrs[selector] = finalAttrs[selector] || {}; - finalAttrs[selector][name] = value; + var offseted = false; + if (offsetAttrs) { + // Check if the node is visible + var nodeClientRect = node.getBoundingClientRect(); + if (nodeClientRect.width > 0 && nodeClientRect.height > 0) { + var nodeBBox = V.transformRect(node.getBBox(), nodeMatrix).scale(1 / sx, 1 / sy); + for (attrName in offsetAttrs) { + attrVal = offsetAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // OFFSET - offset function should return a point from the element + // bounding box. The default offset point is x:0, y:0 (origin) or could be further + // specify with some SVG attributes e.g. `text-anchor`, `cx`, `cy` + translation = def.offset.call(this, attrVal, nodeBBox, node, rawAttrs); + if (translation) { + nodePosition.offset(g.Point(translation).scale(sx, sy)); + offseted || (offseted = true); + } } - }); - }); - - var attributes = joint.util.cloneDeep(joint.util.omit(this.attributes, 'attrs')); - //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); - attributes.attrs = finalAttrs; + } + } - return attributes; + // Do not touch node's transform attribute if there is no transformation applied. + if (nodeTransform !== undefined || positioned || offseted) { + // Round the coordinates to 1 decimal point. + nodePosition.round(1); + nodeMatrix.e = nodePosition.x; + nodeMatrix.f = nodePosition.y; + node.setAttribute('transform', V.matrixToTransformString(nodeMatrix)); + // TODO: store nodeMatrix metrics? + } }, - initialize: function(options) { - - if (!options || !options.id) { + getNodeScale: function(node, scalableNode) { - this.set('id', joint.util.uuid(), { silent: true }); + // Check if the node is a descendant of the scalable group. + var sx, sy; + if (scalableNode && scalableNode.contains(node)) { + var scale = scalableNode.scale(); + sx = 1 / scale.sx; + sy = 1 / scale.sy; + } else { + sx = 1; + sy = 1; } - this._transitionIds = {}; - - // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. - this.processPorts(); - this.on('change:attrs', this.processPorts, this); + return { sx: sx, sy: sy }; }, - /** - * @deprecated - */ - processPorts: function() { + findNodesAttributes: function(attrs, root, selectorCache, selectors) { - // Whenever `attrs` changes, we extract ports from the `attrs` object and store it - // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` - // set to that port, we remove those links as well (to follow the same behaviour as - // with a removed element). + // TODO: merge attributes in order defined by `index` property - var previousPorts = this.ports; + // most browsers sort elements in attrs by order of addition + // which is useful but not required - // Collect ports from the `attrs` object. - var ports = {}; - joint.util.forIn(this.get('attrs'), function(attrs, selector) { + // link.updateLabels() relies on that assumption for merging label attrs over default label attrs - if (attrs && attrs.port) { + var nodesAttrs = {}; - // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). - if (attrs.port.id !== undefined) { - ports[attrs.port.id] = attrs.port; + for (var selector in attrs) { + if (!attrs.hasOwnProperty(selector)) continue; + var selected = selectorCache[selector] = this.findBySelector(selector, root, selectors); + for (var i = 0, n = selected.length; i < n; i++) { + var node = selected[i]; + var nodeId = V.ensureId(node); + var nodeAttrs = attrs[selector]; + var prevNodeAttrs = nodesAttrs[nodeId]; + if (prevNodeAttrs) { + if (!prevNodeAttrs.merged) { + prevNodeAttrs.merged = true; + // if prevNode attrs is `null`, replace with `{}` + prevNodeAttrs.attributes = joint.util.cloneDeep(prevNodeAttrs.attributes) || {}; + } + // if prevNode attrs not set (or `null` or`{}`), use node attrs + // if node attrs not set (or `null` or `{}`), use prevNode attrs + joint.util.merge(prevNodeAttrs.attributes, nodeAttrs); } else { - ports[attrs.port] = { id: attrs.port }; + nodesAttrs[nodeId] = { + attributes: nodeAttrs, + node: node, + merged: false + }; } } - }); - - // Collect ports that have been removed (compared to the previous ports) - if any. - // Use hash table for quick lookup. - var removedPorts = {}; - joint.util.forIn(previousPorts, function(port, id) { - - if (!ports[id]) removedPorts[id] = true; - }); - - // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. - if (this.graph && !joint.util.isEmpty(removedPorts)) { - - var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); - inboundLinks.forEach(function(link) { - - if (removedPorts[link.get('target').port]) link.remove(); - }); - - var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); - outboundLinks.forEach(function(link) { - - if (removedPorts[link.get('source').port]) link.remove(); - }); } - // Update the `ports` object. - this.ports = ports; + return nodesAttrs; }, - remove: function(opt) { - - opt = opt || {}; - - // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. - var graph = this.graph; - if (graph) { - graph.startBatch('remove'); - } - - // First, unembed this cell from its parent cell if there is one. - var parentCellId = this.get('parent'); - if (parentCellId) { - - var parentCell = graph && graph.getCell(parentCellId); - parentCell.unembed(this); - } - - joint.util.invoke(this.getEmbeddedCells(), 'remove', opt); + // Default is to process the `model.attributes.attrs` object and set attributes on subelements based on the selectors, + // unless `attrs` parameter was passed. + updateDOMSubtreeAttributes: function(rootNode, attrs, opt) { - this.trigger('remove', this, this.collection, opt); + opt || (opt = {}); + opt.rootBBox || (opt.rootBBox = g.Rect()); + opt.selectors || (opt.selectors = this.selectors); // selector collection to use - if (graph) { - graph.stopBatch('remove'); - } + // Cache table for query results and bounding box calculation. + // Note that `selectorCache` needs to be invalidated for all + // `updateAttributes` calls, as the selectors might pointing + // to nodes designated by an attribute or elements dynamically + // created. + var selectorCache = {}; + var bboxCache = {}; + var relativeItems = []; + var item, node, nodeAttrs, nodeData, processedAttrs; - return this; - }, + var roAttrs = opt.roAttributes; + var nodesAttrs = this.findNodesAttributes(roAttrs || attrs, rootNode, selectorCache, opt.selectors); + // `nodesAttrs` are different from all attributes, when + // rendering only attributes sent to this method. + var nodesAllAttrs = (roAttrs) + ? nodesAllAttrs = this.findNodesAttributes(attrs, rootNode, selectorCache, opt.selectors) + : nodesAttrs; - toFront: function(opt) { + for (var nodeId in nodesAttrs) { + nodeData = nodesAttrs[nodeId]; + nodeAttrs = nodeData.attributes; + node = nodeData.node; + processedAttrs = this.processNodeAttributes(node, nodeAttrs); - if (this.graph) { + if (!processedAttrs.set && !processedAttrs.position && !processedAttrs.offset) { + // Set all the normal attributes right on the SVG/HTML element. + this.setNodeAttributes(node, processedAttrs.normal); - opt = opt || {}; + } else { - var z = (this.graph.getLastCell().get('z') || 0) + 1; + var nodeAllAttrs = nodesAllAttrs[nodeId] && nodesAllAttrs[nodeId].attributes; + var refSelector = (nodeAllAttrs && (nodeAttrs.ref === undefined)) + ? nodeAllAttrs.ref + : nodeAttrs.ref; - this.startBatch('to-front').set('z', z, opt); + var refNode; + if (refSelector) { + refNode = (selectorCache[refSelector] || this.findBySelector(refSelector, rootNode, opt.selectors))[0]; + if (!refNode) { + throw new Error('dia.ElementView: "' + refSelector + '" reference does not exist.'); + } + } else { + refNode = null; + } - if (opt.deep) { + item = { + node: node, + refNode: refNode, + processedAttributes: processedAttrs, + allAttributes: nodeAllAttrs + }; - var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - cells.forEach(function(cell) { cell.set('z', ++z, opt); }); + // If an element in the list is positioned relative to this one, then + // we want to insert this one before it in the list. + var itemIndex = relativeItems.findIndex(function(item) { + return item.refNode === node; + }); + if (itemIndex > -1) { + relativeItems.splice(itemIndex, 0, item); + } else { + relativeItems.push(item); + } } - - this.stopBatch('to-front'); } - return this; - }, + for (var i = 0, n = relativeItems.length; i < n; i++) { + item = relativeItems[i]; + node = item.node; + refNode = item.refNode; - toBack: function(opt) { + // Find the reference element bounding box. If no reference was provided, we + // use the optional bounding box. + var refNodeId = refNode ? V.ensureId(refNode) : ''; + var refBBox = bboxCache[refNodeId]; + if (!refBBox) { + // Get the bounding box of the reference element relative to the `rotatable` `<g>` (without rotation) + // or to the root `<g>` element if no rotatable group present if reference node present. + // Uses the bounding box provided. + refBBox = bboxCache[refNodeId] = (refNode) + ? V(refNode).getBBox({ target: (opt.rotatableNode || rootNode) }) + : opt.rootBBox; + } - if (this.graph) { + if (roAttrs) { + // if there was a special attribute affecting the position amongst passed-in attributes + // we have to merge it with the rest of the element's attributes as they are necessary + // to update the position relatively (i.e `ref-x` && 'ref-dx') + processedAttrs = this.processNodeAttributes(node, item.allAttributes); + this.mergeProcessedAttributes(processedAttrs, item.processedAttributes); - opt = opt || {}; + } else { + processedAttrs = item.processedAttributes; + } - var z = (this.graph.getFirstCell().get('z') || 0) - 1; + this.updateRelativeAttributes(node, processedAttrs, refBBox, opt); + } + }, - this.startBatch('to-back'); + mergeProcessedAttributes: function(processedAttrs, roProcessedAttrs) { - if (opt.deep) { + processedAttrs.set || (processedAttrs.set = {}); + processedAttrs.position || (processedAttrs.position = {}); + processedAttrs.offset || (processedAttrs.offset = {}); - var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - cells.reverse().forEach(function(cell) { cell.set('z', z--, opt); }); - } + joint.util.assign(processedAttrs.set, roProcessedAttrs.set); + joint.util.assign(processedAttrs.position, roProcessedAttrs.position); + joint.util.assign(processedAttrs.offset, roProcessedAttrs.offset); - this.set('z', z, opt).stopBatch('to-back'); + // Handle also the special transform property. + var transform = processedAttrs.normal && processedAttrs.normal.transform; + if (transform !== undefined && roProcessedAttrs.normal) { + roProcessedAttrs.normal.transform = transform; } + processedAttrs.normal = roProcessedAttrs.normal; + }, - return this; + onRemove: function() { + this.removeTools(); }, - embed: function(cell, opt) { + _toolsView: null, - if (this === cell || this.isEmbeddedIn(cell)) { + hasTools: function(name) { + var toolsView = this._toolsView; + if (!toolsView) return false; + if (!name) return true; + return (toolsView.getName() === name); + }, - throw new Error('Recursive embedding not allowed.'); + addTools: function(toolsView) { - } else { + this.removeTools(); - this.startBatch('embed'); + if (toolsView instanceof joint.dia.ToolsView) { + this._toolsView = toolsView; + toolsView.configure({ relatedView: this }); + toolsView.listenTo(this.paper, 'tools:event', this.onToolEvent.bind(this)); + toolsView.mount(); + } + return this; + }, - var embeds = joint.util.assign([], this.get('embeds')); + updateTools: function(opt) { - // We keep all element ids after link ids. - embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); + var toolsView = this._toolsView; + if (toolsView) toolsView.update(opt); + return this; + }, - cell.set('parent', this.id, opt); - this.set('embeds', joint.util.uniq(embeds), opt); + removeTools: function() { - this.stopBatch('embed'); + var toolsView = this._toolsView; + if (toolsView) { + toolsView.remove(); + this._toolsView = null; } - return this; }, - unembed: function(cell, opt) { - - this.startBatch('unembed'); - - cell.unset('parent', opt); - this.set('embeds', joint.util.without(this.get('embeds'), cell.id), opt); - - this.stopBatch('unembed'); + hideTools: function() { + var toolsView = this._toolsView; + if (toolsView) toolsView.hide(); return this; }, - // Return an array of ancestor cells. - // The array is ordered from the parent of the cell - // to the most distant ancestor. - getAncestors: function() { + showTools: function() { - var ancestors = []; - var parentId = this.get('parent'); - - if (!this.graph) { - return ancestors; - } + var toolsView = this._toolsView; + if (toolsView) toolsView.show(); + return this; + }, - while (parentId !== undefined) { - var parent = this.graph.getCell(parentId); - if (parent !== undefined) { - ancestors.push(parent); - parentId = parent.get('parent'); - } else { + onToolEvent: function(event) { + switch (event) { + case 'remove': + this.removeTools(); + break; + case 'hide': + this.hideTools(); + break; + case 'show': + this.showTools(); break; - } } - - return ancestors; }, - getEmbeddedCells: function(opt) { - - opt = opt || {}; - - // Cell models can only be retrieved when this element is part of a collection. - // There is no way this element knows about other cells otherwise. - // This also means that calling e.g. `translate()` on an element with embeds before - // adding it to a graph does not translate its embeds. - if (this.graph) { - - var cells; - - if (opt.deep) { - - if (opt.breadthFirst) { + // Interaction. The controller part. + // --------------------------------- - // breadthFirst algorithm - cells = []; - var queue = this.getEmbeddedCells(); + // Interaction is handled by the paper and delegated to the view in interest. + // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. + // If necessary, real coordinates can be obtained from the `evt` event object. - while (queue.length > 0) { + // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, + // i.e. `joint.dia.Element` and `joint.dia.Link`. - var parent = queue.shift(); - cells.push(parent); - queue.push.apply(queue, parent.getEmbeddedCells()); - } + pointerdblclick: function(evt, x, y) { - } else { + this.notify('cell:pointerdblclick', evt, x, y); + }, - // depthFirst algorithm - cells = this.getEmbeddedCells(); - cells.forEach(function(cell) { - cells.push.apply(cells, cell.getEmbeddedCells(opt)); - }); - } + pointerclick: function(evt, x, y) { - } else { + this.notify('cell:pointerclick', evt, x, y); + }, - cells = joint.util.toArray(this.get('embeds')).map(this.graph.getCell, this.graph); - } + contextmenu: function(evt, x, y) { - return cells; - } - return []; + this.notify('cell:contextmenu', evt, x, y); }, - isEmbeddedIn: function(cell, opt) { + pointerdown: function(evt, x, y) { - var cellId = joint.util.isString(cell) ? cell : cell.id; - var parentId = this.get('parent'); + if (this.model.graph) { + this.model.startBatch('pointer'); + this._graph = this.model.graph; + } - opt = joint.util.defaults({ deep: true }, opt); + this.notify('cell:pointerdown', evt, x, y); + }, - // See getEmbeddedCells(). - if (this.graph && opt.deep) { + pointermove: function(evt, x, y) { - while (parentId) { - if (parentId === cellId) { - return true; - } - parentId = this.graph.getCell(parentId).get('parent'); - } + this.notify('cell:pointermove', evt, x, y); + }, - return false; + pointerup: function(evt, x, y) { - } else { + this.notify('cell:pointerup', evt, x, y); - // When this cell is not part of a collection check - // at least whether it's a direct child of given cell. - return parentId === cellId; + if (this._graph) { + // we don't want to trigger event on model as model doesn't + // need to be member of collection anymore (remove) + this._graph.stopBatch('pointer', { cell: this.model }); + delete this._graph; } }, - // Whether or not the cell is embedded in any other cell. - isEmbedded: function() { + mouseover: function(evt) { - return !!this.get('parent'); + this.notify('cell:mouseover', evt); }, - // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). - // Shallow cloning simply clones the cell and returns a new cell with different ID. - // Deep cloning clones the cell and all its embedded cells recursively. - clone: function(opt) { - - opt = opt || {}; + mouseout: function(evt) { - if (!opt.deep) { - // Shallow cloning. + this.notify('cell:mouseout', evt); + }, - var clone = Backbone.Model.prototype.clone.apply(this, arguments); - // We don't want the clone to have the same ID as the original. - clone.set('id', joint.util.uuid()); - // A shallow cloned element does not carry over the original embeds. - clone.unset('embeds'); - // And can not be embedded in any cell - // as the clone is not part of the graph. - clone.unset('parent'); + mouseenter: function(evt) { - return clone; + this.notify('cell:mouseenter', evt); + }, - } else { - // Deep cloning. + mouseleave: function(evt) { - // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. - return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); - } + this.notify('cell:mouseleave', evt); }, - // A convenient way to set nested properties. - // This method merges the properties you'd like to set with the ones - // stored in the cell and makes sure change events are properly triggered. - // You can either set a nested property with one object - // or use a property path. - // The most simple use case is: - // `cell.prop('name/first', 'John')` or - // `cell.prop({ name: { first: 'John' } })`. - // Nested arrays are supported too: - // `cell.prop('series/0/data/0/degree', 50)` or - // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. - prop: function(props, value, opt) { + mousewheel: function(evt, x, y, delta) { - var delim = '/'; - var isString = joint.util.isString(props); + this.notify('cell:mousewheel', evt, x, y, delta); + }, + + onevent: function(evt, eventName, x, y) { - if (isString || Array.isArray(props)) { - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. + this.notify(eventName, evt, x, y); + }, - if (arguments.length > 1) { + onmagnet: function() { - var path; - var pathArray; + // noop + }, - if (isString) { - path = props; - pathArray = path.split('/') - } else { - path = props.join(delim); - pathArray = props.slice(); - } + setInteractivity: function(value) { - var property = pathArray[0]; - var pathArrayLength = pathArray.length; + this.options.interactive = value; + } +}, { - opt = opt || {}; - opt.propertyPath = path; - opt.propertyValue = value; - opt.propertyPathArray = pathArray; + dispatchToolsEvent: function(paper, event) { + if ((typeof event === 'string') && (paper instanceof joint.dia.Paper)) { + paper.trigger('tools:event', event); + } + } +}); - if (pathArrayLength === 1) { - // Property is not nested. We can simply use `set()`. - return this.set(property, value, opt); - } - var update = {}; - // Initialize the nested object. Subobjects are either arrays or objects. - // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. - // Note that this imposes a limitation on object keys one can use with Inspector. - // Pure integer keys will cause issues and are therefore not allowed. - var initializer = update; - var prevProperty = property; +// joint.dia.Element base model. +// ----------------------------- - for (var i = 1; i < pathArrayLength; i++) { - var pathItem = pathArray[i]; - var isArrayIndex = Number.isFinite(isString ? Number(pathItem) : pathItem); - initializer = initializer[prevProperty] = isArrayIndex ? [] : {}; - prevProperty = pathItem; - } +joint.dia.Element = joint.dia.Cell.extend({ - // Fill update with the `value` on `path`. - update = joint.util.setByPath(update, pathArray, value, '/'); + defaults: { + position: { x: 0, y: 0 }, + size: { width: 1, height: 1 }, + angle: 0 + }, - var baseAttributes = joint.util.merge({}, this.attributes); - // if rewrite mode enabled, we replace value referenced by path with - // the new one (we don't merge). - opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); + initialize: function() { - // Merge update with the model attributes. - var attributes = joint.util.merge(baseAttributes, update); - // Finally, set the property to the updated attributes. - return this.set(property, attributes[property], opt); + this._initializePorts(); + joint.dia.Cell.prototype.initialize.apply(this, arguments); + }, - } else { + /** + * @abstract + */ + _initializePorts: function() { + // implemented in ports.js + }, - return joint.util.getByPath(this.attributes, props, delim); - } - } + isElement: function() { - return this.set(joint.util.merge({}, this.attributes, props), value); + return true; }, - // A convient way to unset nested properties - removeProp: function(path, opt) { + position: function(x, y, opt) { - // Once a property is removed from the `attrs` attribute - // the cellView will recognize a `dirty` flag and rerender itself - // in order to remove the attribute from SVG element. - opt = opt || {}; - opt.dirty = true; + var isSetter = joint.util.isNumber(y); - var pathArray = Array.isArray(path) ? path : path.split('/'); + opt = (isSetter ? opt : x) || {}; - if (pathArray.length === 1) { - // A top level property - return this.unset(path, opt); + // option `parentRelative` for setting the position relative to the element's parent. + if (opt.parentRelative) { + + // Getting the parent's position requires the collection. + // Cell.parent() holds cell id only. + if (!this.graph) throw new Error('Element must be part of a graph.'); + + var parent = this.getParentCell(); + var parentPosition = parent && !parent.isLink() + ? parent.get('position') + : { x: 0, y: 0 }; } - // A nested property - var property = pathArray[0]; - var nestedPath = pathArray.slice(1); - var propertyValue = joint.util.cloneDeep(this.get(property)); + if (isSetter) { - joint.util.unsetByPath(propertyValue, nestedPath, '/'); + if (opt.parentRelative) { + x += parentPosition.x; + y += parentPosition.y; + } - return this.set(property, propertyValue, opt); - }, + if (opt.deep) { + var currentPosition = this.get('position'); + this.translate(x - currentPosition.x, y - currentPosition.y, opt); + } else { + this.set('position', { x: x, y: y }, opt); + } - // A convenient way to set nested attributes. - attr: function(attrs, value, opt) { + return this; - var args = Array.from(arguments); - if (args.length === 0) { - return this.get('attrs'); + } else { // Getter returns a geometry point. + + var elementPosition = g.point(this.get('position')); + + return opt.parentRelative + ? elementPosition.difference(parentPosition) + : elementPosition; } + }, - if (Array.isArray(attrs)) { - args[0] = ['attrs'].concat(attrs); - } else if (joint.util.isString(attrs)) { - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. - args[0] = 'attrs/' + attrs; + translate: function(tx, ty, opt) { - } else { + tx = tx || 0; + ty = ty || 0; - args[0] = { 'attrs' : attrs }; + if (tx === 0 && ty === 0) { + // Like nothing has happened. + return this; } - return this.prop.apply(this, args); - }, + opt = opt || {}; + // Pass the initiator of the translation. + opt.translateBy = opt.translateBy || this.id; - // A convenient way to unset nested attributes - removeAttr: function(path, opt) { + var position = this.get('position') || { x: 0, y: 0 }; - if (Array.isArray(path)) { + if (opt.restrictedArea && opt.translateBy === this.id) { - return this.removeProp(['attrs'].concat(path)); + // We are restricting the translation for the element itself only. We get + // the bounding box of the element including all its embeds. + // All embeds have to be translated the exact same way as the element. + var bbox = this.getBBox({ deep: true }); + var ra = opt.restrictedArea; + //- - - - - - - - - - - - -> ra.x + ra.width + // - - - -> position.x | + // -> bbox.x + // ▓▓▓▓▓▓▓ | + // ░░░░░░░▓▓▓▓▓▓▓ + // ░░░░░░░░░ | + // ▓▓▓▓▓▓▓▓░░░░░░░ + // ▓▓▓▓▓▓▓▓ | + // <-dx-> | restricted area right border + // <-width-> | ░ translated element + // <- - bbox.width - -> ▓ embedded element + var dx = position.x - bbox.x; + var dy = position.y - bbox.y; + // Find the maximal/minimal coordinates that the element can be translated + // while complies the restrictions. + var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); + var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); + // recalculate the translation taking the resctrictions into account. + tx = x - position.x; + ty = y - position.y; } - return this.removeProp('attrs/' + path, opt); - }, + var translatedPosition = { + x: position.x + tx, + y: position.y + ty + }; - transition: function(path, value, opt, delim) { + // To find out by how much an element was translated in event 'change:position' handlers. + opt.tx = tx; + opt.ty = ty; - delim = delim || '/'; + if (opt.transition) { - var defaults = { - duration: 100, - delay: 10, - timingFunction: joint.util.timing.linear, - valueFunction: joint.util.interpolate.number - }; + if (!joint.util.isObject(opt.transition)) opt.transition = {}; - opt = joint.util.assign(defaults, opt); + this.transition('position', translatedPosition, joint.util.assign({}, opt.transition, { + valueFunction: joint.util.interpolate.object + })); - var firstFrameTime = 0; - var interpolatingFunction; + } else { - var setter = function(runtime) { + this.set('position', translatedPosition, opt); + } - var id, progress, propertyValue; + // Recursively call `translate()` on all the embeds cells. + joint.util.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); - firstFrameTime = firstFrameTime || runtime; - runtime -= firstFrameTime; - progress = runtime / opt.duration; + return this; + }, - if (progress < 1) { - this._transitionIds[path] = id = joint.util.nextFrame(setter); - } else { - progress = 1; - delete this._transitionIds[path]; - } + size: function(width, height, opt) { + + var currentSize = this.get('size'); + // Getter + // () signature + if (width === undefined) { + return { + width: currentSize.width, + height: currentSize.height + }; + } + // Setter + // (size, opt) signature + if (joint.util.isObject(width)) { + opt = height; + height = joint.util.isNumber(width.height) ? width.height : currentSize.height; + width = joint.util.isNumber(width.width) ? width.width : currentSize.width; + } + + return this.resize(width, height, opt); + }, + + resize: function(width, height, opt) { + + opt = opt || {}; + + this.startBatch('resize', opt); - propertyValue = interpolatingFunction(opt.timingFunction(progress)); + if (opt.direction) { - opt.transitionId = id; + var currentSize = this.get('size'); - this.prop(path, propertyValue, opt); + switch (opt.direction) { - if (!id) this.trigger('transition:end', this, path); + case 'left': + case 'right': + // Don't change height when resizing horizontally. + height = currentSize.height; + break; - }.bind(this); + case 'top': + case 'bottom': + // Don't change width when resizing vertically. + width = currentSize.width; + break; + } - var initiator = function(callback) { + // Get the angle and clamp its value between 0 and 360 degrees. + var angle = g.normalizeAngle(this.get('angle') || 0); - this.stopTransitions(path); + var quadrant = { + 'top-right': 0, + 'right': 0, + 'top-left': 1, + 'top': 1, + 'bottom-left': 2, + 'left': 2, + 'bottom-right': 3, + 'bottom': 3 + }[opt.direction]; - interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); + if (opt.absolute) { - this._transitionIds[path] = joint.util.nextFrame(callback); + // We are taking the element's rotation into account + quadrant += Math.floor((angle + 45) / 90); + quadrant %= 4; + } - this.trigger('transition:start', this, path); + // This is a rectangle in size of the unrotated element. + var bbox = this.getBBox(); - }.bind(this); + // Pick the corner point on the element, which meant to stay on its place before and + // after the rotation. + var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); - return setTimeout(initiator, opt.delay, setter); - }, + // Find an image of the previous indent point. This is the position, where is the + // point actually located on the screen. + var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); - getTransitions: function() { - return Object.keys(this._transitionIds); - }, + // Every point on the element rotates around a circle with the centre of rotation + // in the middle of the element while the whole element is being rotated. That means + // that the distance from a point in the corner of the element (supposed its always rect) to + // the center of the element doesn't change during the rotation and therefore it equals + // to a distance on unrotated element. + // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. + var radius = Math.sqrt((width * width) + (height * height)) / 2; - stopTransitions: function(path, delim) { + // Now we are looking for an angle between x-axis and the line starting at image of fixed point + // and ending at the center of the element. We call this angle `alpha`. - delim = delim || '/'; + // The image of a fixed point is located in n-th quadrant. For each quadrant passed + // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. + // + // 3 | 2 + // --c-- Quadrant positions around the element's center `c` + // 0 | 1 + // + var alpha = quadrant * Math.PI / 2; - var pathArray = path && path.split(delim); + // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis + // going through the center of the element) and line crossing the indent of the fixed point and the center + // of the element. This is the angle we need but on the unrotated element. + alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); - Object.keys(this._transitionIds).filter(pathArray && function(key) { + // Lastly we have to deduct the original angle the element was rotated by and that's it. + alpha -= g.toRad(angle); - return joint.util.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); + // With this angle and distance we can easily calculate the centre of the unrotated element. + // Note that fromPolar constructor accepts an angle in radians. + var center = g.point.fromPolar(radius, alpha, imageFixedPoint); - }).forEach(function(key) { + // The top left corner on the unrotated element has to be half a width on the left + // and half a height to the top from the center. This will be the origin of rectangle + // we were looking for. + var origin = g.point(center).offset(width / -2, height / -2); - joint.util.cancelFrame(this._transitionIds[key]); + // Resize the element (before re-positioning it). + this.set('size', { width: width, height: height }, opt); - delete this._transitionIds[key]; + // Finally, re-position the element. + this.position(origin.x, origin.y, opt); - this.trigger('transition:end', this, key); + } else { - }, this); + // Resize the element. + this.set('size', { width: width, height: height }, opt); + } + + this.stopBatch('resize', opt); return this; }, - // A shorcut making it easy to create constructs like the following: - // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. - addTo: function(graph, opt) { + scale: function(sx, sy, origin, opt) { - graph.addCell(this, opt); + var scaledBBox = this.getBBox().scale(sx, sy, origin); + this.startBatch('scale', opt); + this.position(scaledBBox.x, scaledBBox.y, opt); + this.resize(scaledBBox.width, scaledBBox.height, opt); + this.stopBatch('scale'); return this; }, - // A shortcut for an equivalent call: `paper.findViewByModel(cell)` - // making it easy to create constructs like the following: - // `cell.findView(paper).highlight()` - findView: function(paper) { + fitEmbeds: function(opt) { - return paper.findViewByModel(this); - }, + opt = opt || {}; - isElement: function() { + // Getting the children's size and position requires the collection. + // Cell.get('embdes') helds an array of cell ids only. + if (!this.graph) throw new Error('Element must be part of a graph.'); - return false; - }, + var embeddedCells = this.getEmbeddedCells(); - isLink: function() { + if (embeddedCells.length > 0) { - return false; - }, + this.startBatch('fit-embeds', opt); - startBatch: function(name, opt) { - if (this.graph) { this.graph.startBatch(name, joint.util.assign({}, opt, { cell: this })); } - return this; - }, + if (opt.deep) { + // Recursively apply fitEmbeds on all embeds first. + joint.util.invoke(embeddedCells, 'fitEmbeds', opt); + } - stopBatch: function(name, opt) { - if (this.graph) { this.graph.stopBatch(name, joint.util.assign({}, opt, { cell: this })); } - return this; - } + // Compute cell's size and position based on the children bbox + // and given padding. + var bbox = this.graph.getCellsBBox(embeddedCells); + var padding = joint.util.normalizeSides(opt.padding); -}, { + // Apply padding computed above to the bbox. + bbox.moveAndExpand({ + x: -padding.left, + y: -padding.top, + width: padding.right + padding.left, + height: padding.bottom + padding.top + }); - getAttributeDefinition: function(attrName) { + // Set new element dimensions finally. + this.set({ + position: { x: bbox.x, y: bbox.y }, + size: { width: bbox.width, height: bbox.height } + }, opt); - var defNS = this.attributes; - var globalDefNS = joint.dia.attributes; - return (defNS && defNS[attrName]) || globalDefNS[attrName]; + this.stopBatch('fit-embeds'); + } + + return this; }, - define: function(type, defaults, protoProps, staticProps) { + // Rotate element by `angle` degrees, optionally around `origin` point. + // If `origin` is not provided, it is considered to be the center of the element. + // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not + // the difference from the previous angle. + rotate: function(angle, absolute, origin, opt) { - protoProps = joint.util.assign({ - defaults: joint.util.defaultsDeep({ type: type }, defaults, this.prototype.defaults) - }, protoProps); + if (origin) { - var Cell = this.extend(protoProps, staticProps); - joint.util.setByPath(joint.shapes, type, Cell, '.'); - return Cell; - } -}); + var center = this.getBBox().center(); + var size = this.get('size'); + var position = this.get('position'); + center.rotate(origin, this.get('angle') - angle); + var dx = center.x - size.width / 2 - position.x; + var dy = center.y - size.height / 2 - position.y; + this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); + this.position(position.x + dx, position.y + dy, opt); + this.rotate(angle, absolute, null, opt); + this.stopBatch('rotate'); -// joint.dia.CellView base view and controller. -// -------------------------------------------- + } else { -// This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. + this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360, opt); + } -joint.dia.CellView = joint.mvc.View.extend({ + return this; + }, - tagName: 'g', + angle: function() { + return g.normalizeAngle(this.get('angle') || 0); + }, - svgElement: true, + getBBox: function(opt) { - className: function() { + opt = opt || {}; - var classNames = ['cell']; - var type = this.model.get('type'); + if (opt.deep && this.graph) { - if (type) { + // Get all the embedded elements using breadth first algorithm, + // that doesn't use recursion. + var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + // Add the model itself. + elements.push(this); - type.toLowerCase().split('.').forEach(function(value, index, list) { - classNames.push('type-' + list.slice(0, index + 1).join('-')); - }); + return this.graph.getCellsBBox(elements); } - return classNames.join(' '); - }, + var position = this.get('position'); + var size = this.get('size'); - attributes: function() { + return new g.Rect(position.x, position.y, size.width, size.height); + } +}); - return { 'model-id': this.model.id }; - }, +// joint.dia.Element base view and controller. +// ------------------------------------------- - constructor: function(options) { +joint.dia.ElementView = joint.dia.CellView.extend({ - // Make sure a global unique id is assigned to this view. Store this id also to the properties object. - // The global unique id makes sure that the same view can be rendered on e.g. different machines and - // still be associated to the same object among all those clients. This is necessary for real-time - // collaboration mechanism. - options.id = options.id || joint.util.guid(this); + /** + * @abstract + */ + _removePorts: function() { + // implemented in ports.js + }, - joint.mvc.View.call(this, options); + /** + * + * @abstract + */ + _renderPorts: function() { + // implemented in ports.js }, - init: function() { - - joint.util.bindAll(this, 'remove', 'update'); + className: function() { - // Store reference to this to the <g> DOM element so that the view is accessible through the DOM tree. - this.$el.data('view', this); + var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); - // Add the cell's type to the view's element as a data attribute. - this.$el.attr('data-type', this.model.get('type')); + classNames.push('element'); - this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); + return classNames.join(' '); }, - onChangeAttrs: function(cell, attrs, opt) { + metrics: null, - if (opt.dirty) { + initialize: function() { - // dirty flag could be set when a model attribute was removed and it needs to be cleared - // also from the DOM element. See cell.removeAttr(). - return this.render(); - } + joint.dia.CellView.prototype.initialize.apply(this, arguments); - return this.update(cell, attrs, opt); - }, + var model = this.model; - // Return `true` if cell link is allowed to perform a certain UI `feature`. - // Example: `can('vertexMove')`, `can('labelMove')`. - can: function(feature) { + this.listenTo(model, 'change:position', this.translate); + this.listenTo(model, 'change:size', this.resize); + this.listenTo(model, 'change:angle', this.rotate); + this.listenTo(model, 'change:markup', this.render); - var interactive = joint.util.isFunction(this.options.interactive) - ? this.options.interactive(this) - : this.options.interactive; + this._initializePorts(); - return (joint.util.isObject(interactive) && interactive[feature] !== false) || - (joint.util.isBoolean(interactive) && interactive !== false); + this.metrics = {}; }, - findBySelector: function(selector, root) { + /** + * @abstract + */ + _initializePorts: function() { - var $root = $(root || this.el); - // These are either descendants of `this.$el` of `this.$el` itself. - // `.` is a special selector used to select the wrapping `<g>` element. - return (selector === '.') ? $root : $root.find(selector); }, - notify: function(eventName) { + update: function(cell, renderingOnlyAttrs) { - if (this.paper) { + this.metrics = {}; - var args = Array.prototype.slice.call(arguments, 1); + this._removePorts(); - // Trigger the event on both the element itself and also on the paper. - this.trigger.apply(this, [eventName].concat(args)); + var model = this.model; + var modelAttrs = model.attr(); + this.updateDOMSubtreeAttributes(this.el, modelAttrs, { + rootBBox: new g.Rect(model.size()), + selectors: this.selectors, + scalableNode: this.scalableNode, + rotatableNode: this.rotatableNode, + // Use rendering only attributes if they differs from the model attributes + roAttributes: (renderingOnlyAttrs === modelAttrs) ? null : renderingOnlyAttrs + }); - // Paper event handlers receive the view object as the first argument. - this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); - } + this._renderPorts(); }, - getStrokeBBox: function(el) { - // Return a bounding box rectangle that takes into account stroke. - // Note that this is a naive and ad-hoc implementation that does not - // works only in certain cases and should be replaced as soon as browsers will - // start supporting the getStrokeBBox() SVG method. - // @TODO any better solution is very welcome! + rotatableSelector: 'rotatable', + scalableSelector: 'scalable', + scalableNode: null, + rotatableNode: null, - var isMagnet = !!el; + // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the + // default markup is not desirable. + renderMarkup: function() { - el = el || this.el; - var bbox = V(el).getBBox({ target: this.paper.viewport }); + var element = this.model; + var markup = element.get('markup') || element.markup; + if (!markup) throw new Error('dia.ElementView: markup required'); + if (Array.isArray(markup)) return this.renderJSONMarkup(markup); + if (typeof markup === 'string') return this.renderStringMarkup(markup); + throw new Error('dia.ElementView: invalid markup'); + }, - var strokeWidth; - if (isMagnet) { + renderJSONMarkup: function(markup) { - strokeWidth = V(el).attr('stroke-width'); + var doc = joint.util.parseDOMJSON(markup); + // Selectors + var selectors = this.selectors = doc.selectors; + var rootSelector = this.selector; + if (selectors[rootSelector]) throw new Error('dia.ElementView: ambiguous root selector.'); + selectors[rootSelector] = this.el; + // Cache transformation groups + this.rotatableNode = V(selectors[this.rotatableSelector]) || null; + this.scalableNode = V(selectors[this.scalableSelector]) || null; + // Fragment + this.vel.append(doc.fragment); + }, - } else { + renderStringMarkup: function(markup) { - strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); - } + var vel = this.vel; + vel.append(V(markup)); + // Cache transformation groups + this.rotatableNode = vel.findOne('.rotatable'); + this.scalableNode = vel.findOne('.scalable'); - strokeWidth = parseFloat(strokeWidth) || 0; + var selectors = this.selectors = {}; + selectors[this.selector] = this.el; + }, - return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); + render: function() { + + this.vel.empty(); + this.renderMarkup(); + if (this.scalableNode) { + // Double update is necessary for elements with the scalable group only + // Note the resize() triggers the other `update`. + this.update(); + } + this.resize(); + if (this.rotatableNode) { + // Translate transformation is applied on `this.el` while the rotation transformation + // on `this.rotatableNode` + this.rotate(); + this.translate(); + return this; + } + this.updateTransformation(); + return this; }, - getBBox: function() { + resize: function() { - return this.vel.getBBox({ target: this.paper.svg }); + if (this.scalableNode) return this.sgResize.apply(this, arguments); + if (this.model.attributes.angle) this.rotate(); + this.update(); }, - highlight: function(el, opt) { + translate: function() { - el = !el ? this.el : this.$(el)[0] || this.el; + if (this.rotatableNode) return this.rgTranslate(); + this.updateTransformation(); + }, - // set partial flag if the highlighted element is not the entire view. - opt = opt || {}; - opt.partial = (el !== this.el); + rotate: function() { - this.notify('cell:highlight', el, opt); - return this; + if (this.rotatableNode) return this.rgRotate(); + this.updateTransformation(); }, - unhighlight: function(el, opt) { + updateTransformation: function() { - el = !el ? this.el : this.$(el)[0] || this.el; + var transformation = this.getTranslateString(); + var rotateString = this.getRotateString(); + if (rotateString) transformation += ' ' + rotateString; + this.vel.attr('transform', transformation); + }, - opt = opt || {}; - opt.partial = el != this.el; + getTranslateString: function() { - this.notify('cell:unhighlight', el, opt); - return this; + var position = this.model.attributes.position; + return 'translate(' + position.x + ',' + position.y + ')'; }, - // Find the closest element that has the `magnet` attribute set to `true`. If there was not such - // an element found, return the root element of the cell view. - findMagnet: function(el) { + getRotateString: function() { + var attributes = this.model.attributes; + var angle = attributes.angle; + if (!angle) return null; + var size = attributes.size; + return 'rotate(' + angle + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'; + }, - var $el = this.$(el); - var $rootEl = this.$el; + getBBox: function(opt) { - if ($el.length === 0) { - $el = $rootEl; + var bbox; + if (opt && opt.useModelGeometry) { + var model = this.model; + bbox = model.getBBox().bbox(model.angle()); + } else { + bbox = this.getNodeBBox(this.el); } - do { + return this.paper.localToPaperRect(bbox); + }, - var magnet = $el.attr('magnet'); - if ((magnet || $el.is($rootEl)) && magnet !== 'false') { - return $el[0]; - } + nodeCache: function(magnet) { - $el = $el.parent(); + var id = V.ensureId(magnet); + var metrics = this.metrics[id]; + if (!metrics) metrics = this.metrics[id] = {}; + return metrics; + }, - } while ($el.length > 0); + getNodeData: function(magnet) { - // If the overall cell has set `magnet === false`, then return `undefined` to - // announce there is no magnet found for this cell. - // This is especially useful to set on cells that have 'ports'. In this case, - // only the ports have set `magnet === true` and the overall element has `magnet === false`. - return undefined; + var metrics = this.nodeCache(magnet); + if (!metrics.data) metrics.data = {}; + return metrics.data; }, - // Construct a unique selector for the `el` element within this view. - // `prevSelector` is being collected through the recursive call. - // No value for `prevSelector` is expected when using this method. - getSelector: function(el, prevSelector) { + getNodeBBox: function(magnet) { - if (el === this.el) { - return prevSelector; - } + var rect = this.getNodeBoundingRect(magnet); + var magnetMatrix = this.getNodeMatrix(magnet); + var translateMatrix = this.getRootTranslateMatrix(); + var rotateMatrix = this.getRootRotateMatrix(); + return V.transformRect(rect, translateMatrix.multiply(rotateMatrix).multiply(magnetMatrix)); + }, - var selector; + getNodeBoundingRect: function(magnet) { - if (el) { + var metrics = this.nodeCache(magnet); + if (metrics.boundingRect === undefined) metrics.boundingRect = V(magnet).getBBox(); + return new g.Rect(metrics.boundingRect); + }, - var nthChild = V(el).index() + 1; - selector = el.tagName + ':nth-child(' + nthChild + ')'; + getNodeUnrotatedBBox: function(magnet) { - if (prevSelector) { - selector += ' > ' + prevSelector; - } + var rect = this.getNodeBoundingRect(magnet); + var magnetMatrix = this.getNodeMatrix(magnet); + var translateMatrix = this.getRootTranslateMatrix(); + return V.transformRect(rect, translateMatrix.multiply(magnetMatrix)); + }, - selector = this.getSelector(el.parentNode, selector); - } + getNodeShape: function(magnet) { - return selector; + var metrics = this.nodeCache(magnet); + if (metrics.geometryShape === undefined) metrics.geometryShape = V(magnet).toGeometryShape(); + return metrics.geometryShape.clone(); }, - getAttributeDefinition: function(attrName) { + getNodeMatrix: function(magnet) { - return this.model.constructor.getAttributeDefinition(attrName); + var metrics = this.nodeCache(magnet); + if (metrics.magnetMatrix === undefined) { + var target = this.rotatableNode || this.el; + metrics.magnetMatrix = V(magnet).getTransformToElement(target); + } + return V.createSVGMatrix(metrics.magnetMatrix); }, - setNodeAttributes: function(node, attrs) { + getRootTranslateMatrix: function() { - if (!joint.util.isEmpty(attrs)) { - if (node instanceof SVGElement) { - V(node).attr(attrs); - } else { - $(node).attr(attrs); - } - } + var model = this.model; + var position = model.position(); + var mt = V.createSVGMatrix().translate(position.x, position.y); + return mt; }, - processNodeAttributes: function(node, attrs) { + getRootRotateMatrix: function() { - var attrName, attrVal, def, i, n; - var normalAttrs, setAttrs, positionAttrs, offsetAttrs; - var relatives = []; - // divide the attributes between normal and special - for (attrName in attrs) { - if (!attrs.hasOwnProperty(attrName)) continue; - attrVal = attrs[attrName]; - def = this.getAttributeDefinition(attrName); - if (def && (!joint.util.isFunction(def.qualify) || def.qualify.call(this, attrVal, node, attrs))) { - if (joint.util.isString(def.set)) { - normalAttrs || (normalAttrs = {}); - normalAttrs[def.set] = attrVal; - } - if (attrVal !== null) { - relatives.push(attrName, def); - } - } else { - normalAttrs || (normalAttrs = {}); - normalAttrs[joint.util.toKebabCase(attrName)] = attrVal; - } + var mr = V.createSVGMatrix(); + var model = this.model; + var angle = model.angle(); + if (angle) { + var bbox = model.getBBox(); + var cx = bbox.width / 2; + var cy = bbox.height / 2; + mr = mr.translate(cx, cy).rotate(angle).translate(-cx, -cy); } + return mr; + }, - // handle the rest of attributes via related method - // from the special attributes namespace. - for (i = 0, n = relatives.length; i < n; i+=2) { - attrName = relatives[i]; - def = relatives[i+1]; - attrVal = attrs[attrName]; - if (joint.util.isFunction(def.set)) { - setAttrs || (setAttrs = {}); - setAttrs[attrName] = attrVal; - } - if (joint.util.isFunction(def.position)) { - positionAttrs || (positionAttrs = {}); - positionAttrs[attrName] = attrVal; - } - if (joint.util.isFunction(def.offset)) { - offsetAttrs || (offsetAttrs = {}); - offsetAttrs[attrName] = attrVal; - } - } + // Rotatable & Scalable Group + // always slower, kept mainly for backwards compatibility - return { - raw: attrs, - normal: normalAttrs, - set: setAttrs, - position: positionAttrs, - offset: offsetAttrs - }; + rgRotate: function() { + + this.rotatableNode.attr('transform', this.getRotateString()); }, - updateRelativeAttributes: function(node, attrs, refBBox, opt) { + rgTranslate: function() { - opt || (opt = {}); + this.vel.attr('transform', this.getTranslateString()); + }, - var attrName, attrVal, def; - var rawAttrs = attrs.raw || {}; - var nodeAttrs = attrs.normal || {}; - var setAttrs = attrs.set; - var positionAttrs = attrs.position; - var offsetAttrs = attrs.offset; + sgResize: function(cell, changed, opt) { - for (attrName in setAttrs) { - attrVal = setAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // SET - set function should return attributes to be set on the node, - // which will affect the node dimensions based on the reference bounding - // box. e.g. `width`, `height`, `d`, `rx`, `ry`, `points - var setResult = def.set.call(this, attrVal, refBBox.clone(), node, rawAttrs); - if (joint.util.isObject(setResult)) { - joint.util.assign(nodeAttrs, setResult); - } else if (setResult !== undefined) { - nodeAttrs[attrName] = setResult; - } - } + var model = this.model; + var angle = model.get('angle') || 0; + var size = model.get('size') || { width: 1, height: 1 }; + var scalable = this.scalableNode; - if (node instanceof HTMLElement) { - // TODO: setting the `transform` attribute on HTMLElements - // via `node.style.transform = 'matrix(...)';` would introduce - // a breaking change (e.g. basic.TextBlock). - this.setNodeAttributes(node, nodeAttrs); - return; + // Getting scalable group's bbox. + // Due to a bug in webkit's native SVG .getBBox implementation, the bbox of groups with path children includes the paths' control points. + // To work around the issue, we need to check whether there are any path elements inside the scalable group. + var recursive = false; + if (scalable.node.getElementsByTagName('path').length > 0) { + // If scalable has at least one descendant that is a path, we need to switch to recursive bbox calculation. + // If there are no path descendants, group bbox calculation works and so we can use the (faster) native function directly. + recursive = true; } + var scalableBBox = scalable.getBBox({ recursive: recursive }); - // The final translation of the subelement. - var nodeTransform = nodeAttrs.transform; - var nodeMatrix = V.transformStringToMatrix(nodeTransform); - var nodePosition = g.Point(nodeMatrix.e, nodeMatrix.f); - if (nodeTransform) { - nodeAttrs = joint.util.omit(nodeAttrs, 'transform'); - nodeMatrix.e = nodeMatrix.f = 0; - } + // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making + // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. + var sx = (size.width / (scalableBBox.width || 1)); + var sy = (size.height / (scalableBBox.height || 1)); + scalable.attr('transform', 'scale(' + sx + ',' + sy + ')'); - // Calculate node scale determined by the scalable group - // only if later needed. - var sx, sy, translation; - if (positionAttrs || offsetAttrs) { - var nodeScale = this.getNodeScale(node, opt.scalableNode); - sx = nodeScale.sx; - sy = nodeScale.sy; - } + // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` + // Order of transformations is significant but we want to reconstruct the object always in the order: + // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, + // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the + // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation + // around the center of the resized object (which is a different origin then the origin of the previous rotation) + // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. - var positioned = false; - for (attrName in positionAttrs) { - attrVal = positionAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // POSITION - position function should return a point from the - // reference bounding box. The default position of the node is x:0, y:0 of - // the reference bounding box or could be further specify by some - // SVG attributes e.g. `x`, `y` - translation = def.position.call(this, attrVal, refBBox.clone(), node, rawAttrs); - if (translation) { - nodePosition.offset(g.Point(translation).scale(sx, sy)); - positioned || (positioned = true); - } - } + // Cancel the rotation but now around a different origin, which is the center of the scaled object. + var rotatable = this.rotatableNode; + var rotation = rotatable && rotatable.attr('transform'); + if (rotation && rotation !== null) { - // The node bounding box could depend on the `size` set from the previous loop. - // Here we know, that all the size attributes have been already set. - this.setNodeAttributes(node, nodeAttrs); + rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); + var rotatableBBox = scalable.getBBox({ target: this.paper.viewport }); - var offseted = false; - if (offsetAttrs) { - // Check if the node is visible - var nodeClientRect = node.getBoundingClientRect(); - if (nodeClientRect.width > 0 && nodeClientRect.height > 0) { - var nodeBBox = V.transformRect(node.getBBox(), nodeMatrix).scale(1 / sx, 1 / sy); - for (attrName in offsetAttrs) { - attrVal = offsetAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // OFFSET - offset function should return a point from the element - // bounding box. The default offset point is x:0, y:0 (origin) or could be further - // specify with some SVG attributes e.g. `text-anchor`, `cx`, `cy` - translation = def.offset.call(this, attrVal, nodeBBox, node, rawAttrs); - if (translation) { - nodePosition.offset(g.Point(translation).scale(sx, sy)); - offseted || (offseted = true); - } - } - } + // Store new x, y and perform rotate() again against the new rotation origin. + model.set('position', { x: rotatableBBox.x, y: rotatableBBox.y }, opt); + this.rotate(); } - // Do not touch node's transform attribute if there is no transformation applied. - if (nodeTransform !== undefined || positioned || offseted) { - // Round the coordinates to 1 decimal point. - nodePosition.round(1); - nodeMatrix.e = nodePosition.x; - nodeMatrix.f = nodePosition.y; - node.setAttribute('transform', V.matrixToTransformString(nodeMatrix)); - } + // Update must always be called on non-rotated element. Otherwise, relative positioning + // would work with wrong (rotated) bounding boxes. + this.update(); }, - getNodeScale: function(node, scalableNode) { + // Embedding mode methods. + // ----------------------- - // Check if the node is a descendant of the scalable group. - var sx, sy; - if (scalableNode && scalableNode.contains(node)) { - var scale = scalableNode.scale(); - sx = 1 / scale.sx; - sy = 1 / scale.sy; - } else { - sx = 1; - sy = 1; - } + prepareEmbedding: function(data) { - return { sx: sx, sy: sy }; - }, + data || (data = {}); - findNodesAttributes: function(attrs, root, selectorCache) { + var model = data.model || this.model; + var paper = data.paper || this.paper; + var graph = paper.model; - // TODO: merge attributes in order defined by `index` property + model.startBatch('to-front'); - var nodesAttrs = {}; + // Bring the model to the front with all his embeds. + model.toFront({ deep: true, ui: true }); - for (var selector in attrs) { - if (!attrs.hasOwnProperty(selector)) continue; - var $selected = selectorCache[selector] = this.findBySelector(selector, root); + // Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see + // the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z. + var maxZ = graph.get('cells').max('z').get('z'); + var connectedLinks = graph.getConnectedLinks(model, { deep: true }); - for (var i = 0, n = $selected.length; i < n; i++) { - var node = $selected[i]; - var nodeId = V.ensureId(node); - var nodeAttrs = attrs[selector]; - var prevNodeAttrs = nodesAttrs[nodeId]; - if (prevNodeAttrs) { - if (!prevNodeAttrs.merged) { - prevNodeAttrs.merged = true; - prevNodeAttrs.attributes = joint.util.cloneDeep(prevNodeAttrs.attributes); - } - joint.util.merge(prevNodeAttrs.attributes, nodeAttrs); - } else { - nodesAttrs[nodeId] = { - attributes: nodeAttrs, - node: node, - merged: false - }; - } - } - } + // Move to front also all the inbound and outbound links that are connected + // to any of the element descendant. If we bring to front only embedded elements, + // links connected to them would stay in the background. + joint.util.invoke(connectedLinks, 'set', 'z', maxZ + 1, { ui: true }); - return nodesAttrs; + model.stopBatch('to-front'); + + // Before we start looking for suitable parent we remove the current one. + var parentId = model.parent(); + parentId && graph.getCell(parentId).unembed(model, { ui: true }); }, - // Default is to process the `model.attributes.attrs` object and set attributes on subelements based on the selectors, - // unless `attrs` parameter was passed. - updateDOMSubtreeAttributes: function(rootNode, attrs, opt) { + processEmbedding: function(data) { - opt || (opt = {}); - opt.rootBBox || (opt.rootBBox = g.Rect()); + data || (data = {}); - // Cache table for query results and bounding box calculation. - // Note that `selectorCache` needs to be invalidated for all - // `updateAttributes` calls, as the selectors might pointing - // to nodes designated by an attribute or elements dynamically - // created. - var selectorCache = {}; - var bboxCache = {}; - var relativeItems = []; - var item, node, nodeAttrs, nodeData, processedAttrs; + var model = data.model || this.model; + var paper = data.paper || this.paper; + var paperOptions = paper.options; - var roAttrs = opt.roAttributes; - var nodesAttrs = this.findNodesAttributes(roAttrs || attrs, rootNode, selectorCache); - // `nodesAttrs` are different from all attributes, when - // rendering only attributes sent to this method. - var nodesAllAttrs = (roAttrs) - ? nodesAllAttrs = this.findNodesAttributes(attrs, rootNode, selectorCache) - : nodesAttrs; + var candidates = []; + if (joint.util.isFunction(paperOptions.findParentBy)) { + var parents = joint.util.toArray(paperOptions.findParentBy.call(paper.model, this)); + candidates = parents.filter(function(el) { + return el instanceof joint.dia.Cell && this.model.id !== el.id && !el.isEmbeddedIn(this.model); + }.bind(this)); + } else { + candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); + } - for (var nodeId in nodesAttrs) { - nodeData = nodesAttrs[nodeId]; - nodeAttrs = nodeData.attributes; - node = nodeData.node; - processedAttrs = this.processNodeAttributes(node, nodeAttrs); + if (paperOptions.frontParentOnly) { + // pick the element with the highest `z` index + candidates = candidates.slice(-1); + } - if (!processedAttrs.set && !processedAttrs.position && !processedAttrs.offset) { - // Set all the normal attributes right on the SVG/HTML element. - this.setNodeAttributes(node, processedAttrs.normal); + var newCandidateView = null; + var prevCandidateView = data.candidateEmbedView; - } else { + // iterate over all candidates starting from the last one (has the highest z-index). + for (var i = candidates.length - 1; i >= 0; i--) { - var nodeAllAttrs = nodesAllAttrs[nodeId] && nodesAllAttrs[nodeId].attributes; - var refSelector = (nodeAllAttrs && (nodeAttrs.ref === undefined)) - ? nodeAllAttrs.ref - : nodeAttrs.ref; + var candidate = candidates[i]; - var refNode; - if (refSelector) { - refNode = (selectorCache[refSelector] || this.findBySelector(refSelector, rootNode))[0]; - if (!refNode) { - throw new Error('dia.ElementView: "' + refSelector + '" reference does not exists.'); - } - } else { - refNode = null; - } + if (prevCandidateView && prevCandidateView.model.id == candidate.id) { - item = { - node: node, - refNode: refNode, - processedAttributes: processedAttrs, - allAttributes: nodeAllAttrs - }; + // candidate remains the same + newCandidateView = prevCandidateView; + break; - // If an element in the list is positioned relative to this one, then - // we want to insert this one before it in the list. - var itemIndex = relativeItems.findIndex(function(item) { - return item.refNode === node; - }); + } else { - if (itemIndex > -1) { - relativeItems.splice(itemIndex, 0, item); - } else { - relativeItems.push(item); + var view = candidate.findView(paper); + if (paperOptions.validateEmbedding.call(paper, this, view)) { + + // flip to the new candidate + newCandidateView = view; + break; } } } - for (var i = 0, n = relativeItems.length; i < n; i++) { - item = relativeItems[i]; - node = item.node; - refNode = item.refNode; + if (newCandidateView && newCandidateView != prevCandidateView) { + // A new candidate view found. Highlight the new one. + this.clearEmbedding(data); + data.candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); + } - // Find the reference element bounding box. If no reference was provided, we - // use the optional bounding box. - var refNodeId = refNode ? V.ensureId(refNode) : ''; - var refBBox = bboxCache[refNodeId]; - if (!refBBox) { - // Get the bounding box of the reference element relative to the `rotatable` `<g>` (without rotation) - // or to the root `<g>` element if no rotatable group present if reference node present. - // Uses the bounding box provided. - refBBox = bboxCache[refNodeId] = (refNode) - ? V(refNode).getBBox({ target: (opt.rotatableNode || rootNode) }) - : opt.rootBBox; - } + if (!newCandidateView && prevCandidateView) { + // No candidate view found. Unhighlight the previous candidate. + this.clearEmbedding(data); + } + }, - if (roAttrs) { - // if there was a special attribute affecting the position amongst passed-in attributes - // we have to merge it with the rest of the element's attributes as they are necessary - // to update the position relatively (i.e `ref-x` && 'ref-dx') - processedAttrs = this.processNodeAttributes(node, item.allAttributes); - this.mergeProcessedAttributes(processedAttrs, item.processedAttributes); + clearEmbedding: function(data) { - } else { - processedAttrs = item.processedAttributes; - } + data || (data = {}); - this.updateRelativeAttributes(node, processedAttrs, refBBox, opt); + var candidateView = data.candidateEmbedView; + if (candidateView) { + // No candidate view found. Unhighlight the previous candidate. + candidateView.unhighlight(null, { embedding: true }); + data.candidateEmbedView = null; } }, - mergeProcessedAttributes: function(processedAttrs, roProcessedAttrs) { + finalizeEmbedding: function(data) { + + data || (data = {}); + + var candidateView = data.candidateEmbedView; + var model = data.model || this.model; + var paper = data.paper || this.paper; - processedAttrs.set || (processedAttrs.set = {}); - processedAttrs.position || (processedAttrs.position = {}); - processedAttrs.offset || (processedAttrs.offset = {}); + if (candidateView) { - joint.util.assign(processedAttrs.set, roProcessedAttrs.set); - joint.util.assign(processedAttrs.position, roProcessedAttrs.position); - joint.util.assign(processedAttrs.offset, roProcessedAttrs.offset); + // We finished embedding. Candidate view is chosen to become the parent of the model. + candidateView.model.embed(model, { ui: true }); + candidateView.unhighlight(null, { embedding: true }); - // Handle also the special transform property. - var transform = processedAttrs.normal && processedAttrs.normal.transform; - if (transform !== undefined && roProcessedAttrs.normal) { - roProcessedAttrs.normal.transform = transform; + data.candidateEmbedView = null; } - processedAttrs.normal = roProcessedAttrs.normal; + + joint.util.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); }, // Interaction. The controller part. // --------------------------------- - // Interaction is handled by the paper and delegated to the view in interest. - // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. - // If necessary, real coordinates can be obtained from the `evt` event object. - - // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, - // i.e. `joint.dia.Element` and `joint.dia.Link`. - pointerdblclick: function(evt, x, y) { - this.notify('cell:pointerdblclick', evt, x, y); + joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments); + this.notify('element:pointerdblclick', evt, x, y); }, pointerclick: function(evt, x, y) { - this.notify('cell:pointerclick', evt, x, y); + joint.dia.CellView.prototype.pointerclick.apply(this, arguments); + this.notify('element:pointerclick', evt, x, y); + }, + + contextmenu: function(evt, x, y) { + + joint.dia.CellView.prototype.contextmenu.apply(this, arguments); + this.notify('element:contextmenu', evt, x, y); }, pointerdown: function(evt, x, y) { - if (this.model.graph) { - this.model.startBatch('pointer'); - this._graph = this.model.graph; - } + joint.dia.CellView.prototype.pointerdown.apply(this, arguments); + this.notify('element:pointerdown', evt, x, y); - this.notify('cell:pointerdown', evt, x, y); + this.dragStart(evt, x, y); }, pointermove: function(evt, x, y) { - this.notify('cell:pointermove', evt, x, y); + var data = this.eventData(evt); + switch (data.action) { + case 'move': + this.drag(evt, x, y); + break; + case 'magnet': + this.dragMagnet(evt, x, y); + break; + } + + if (!data.stopPropagation) { + joint.dia.CellView.prototype.pointermove.apply(this, arguments); + this.notify('element:pointermove', evt, x, y); + } + + // Make sure the element view data is passed along. + // It could have been wiped out in the handlers above. + this.eventData(evt, data); }, pointerup: function(evt, x, y) { - this.notify('cell:pointerup', evt, x, y); + var data = this.eventData(evt); + switch (data.action) { + case 'move': + this.dragEnd(evt, x, y); + break; + case 'magnet': + this.dragMagnetEnd(evt, x, y); + return; + } - if (this._graph) { - // we don't want to trigger event on model as model doesn't - // need to be member of collection anymore (remove) - this._graph.stopBatch('pointer', { cell: this.model }); - delete this._graph; + if (!data.stopPropagation) { + this.notify('element:pointerup', evt, x, y); + joint.dia.CellView.prototype.pointerup.apply(this, arguments); } }, mouseover: function(evt) { - this.notify('cell:mouseover', evt); + joint.dia.CellView.prototype.mouseover.apply(this, arguments); + this.notify('element:mouseover', evt); }, mouseout: function(evt) { - this.notify('cell:mouseout', evt); + joint.dia.CellView.prototype.mouseout.apply(this, arguments); + this.notify('element:mouseout', evt); }, mouseenter: function(evt) { - this.notify('cell:mouseenter', evt); + joint.dia.CellView.prototype.mouseenter.apply(this, arguments); + this.notify('element:mouseenter', evt); }, mouseleave: function(evt) { - this.notify('cell:mouseleave', evt); + joint.dia.CellView.prototype.mouseleave.apply(this, arguments); + this.notify('element:mouseleave', evt); }, mousewheel: function(evt, x, y, delta) { - this.notify('cell:mousewheel', evt, x, y, delta); + joint.dia.CellView.prototype.mousewheel.apply(this, arguments); + this.notify('element:mousewheel', evt, x, y, delta); }, - contextmenu: function(evt, x, y) { + onmagnet: function(evt, x, y) { - this.notify('cell:contextmenu', evt, x, y); + this.dragMagnetStart(evt, x, y); + + var stopPropagation = this.eventData(evt).stopPropagation; + if (stopPropagation) evt.stopPropagation(); }, - event: function(evt, eventName, x, y) { + // Drag Start Handlers - this.notify(eventName, evt, x, y); + dragStart: function(evt, x, y) { + + if (!this.can('elementMove')) return; + + this.eventData(evt, { + action: 'move', + x: x, + y: y, + restrictedArea: this.paper.getRestrictedArea(this) + }); }, - setInteractivity: function(value) { + dragMagnetStart: function(evt, x, y) { - this.options.interactive = value; - } -}); + if (!this.can('addLinkFromMagnet')) return; -// joint.dia.Element base model. -// ----------------------------- + this.model.startBatch('add-link'); -joint.dia.Element = joint.dia.Cell.extend({ + var paper = this.paper; + var graph = paper.model; + var magnet = evt.target; + var link = paper.getDefaultLink(this, magnet); + var sourceEnd = this.getLinkEnd(magnet, x, y, link, 'source'); + var targetEnd = { x: x, y: y }; + + link.set({ source: sourceEnd, target: targetEnd }); + link.addTo(graph, { async: false, ui: true }); + + var linkView = link.findView(paper); + joint.dia.CellView.prototype.pointerdown.apply(linkView, arguments); + linkView.notify('link:pointerdown', evt, x, y); + var data = linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); + linkView.eventData(evt, data); + + this.eventData(evt, { + action: 'magnet', + linkView: linkView, + stopPropagation: true + }); - defaults: { - position: { x: 0, y: 0 }, - size: { width: 1, height: 1 }, - angle: 0 + this.paper.delegateDragEvents(this, evt.data); }, - initialize: function() { + // Drag Handlers - this._initializePorts(); - joint.dia.Cell.prototype.initialize.apply(this, arguments); + drag: function(evt, x, y) { + + var paper = this.paper; + var grid = paper.options.gridSize; + var element = this.model; + var position = element.position(); + var data = this.eventData(evt); + + // Make sure the new element's position always snaps to the current grid after + // translate as the previous one could be calculated with a different grid size. + var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - data.x, grid); + var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - data.y, grid); + + element.translate(tx, ty, { restrictedArea: data.restrictedArea, ui: true }); + + var embedding = !!data.embedding; + if (paper.options.embeddingMode) { + if (!embedding) { + // Prepare the element for embedding only if the pointer moves. + // We don't want to do unnecessary action with the element + // if an user only clicks/dblclicks on it. + this.prepareEmbedding(data); + embedding = true; + } + this.processEmbedding(data); + } + + this.eventData(evt, { + x: g.snapToGrid(x, grid), + y: g.snapToGrid(y, grid), + embedding: embedding + }); }, - /** - * @abstract - */ - _initializePorts: function() { - // implemented in ports.js + dragMagnet: function(evt, x, y) { + + var data = this.eventData(evt); + var linkView = data.linkView; + if (linkView) linkView.pointermove(evt, x, y); }, - isElement: function() { + // Drag End Handlers - return true; + dragEnd: function(evt, x, y) { + + var data = this.eventData(evt); + if (data.embedding) this.finalizeEmbedding(data); }, - position: function(x, y, opt) { + dragMagnetEnd: function(evt, x, y) { - var isSetter = joint.util.isNumber(y); + var data = this.eventData(evt); + var linkView = data.linkView; + if (linkView) linkView.pointerup(evt, x, y); - opt = (isSetter ? opt : x) || {}; + this.model.stopBatch('add-link'); + } - // option `parentRelative` for setting the position relative to the element's parent. - if (opt.parentRelative) { +}); - // Getting the parent's position requires the collection. - // Cell.get('parent') helds cell id only. - if (!this.graph) throw new Error('Element must be part of a graph.'); - var parent = this.graph.getCell(this.get('parent')); - var parentPosition = parent && !parent.isLink() - ? parent.get('position') - : { x: 0, y: 0 }; - } +// joint.dia.Link base model. +// -------------------------- - if (isSetter) { +joint.dia.Link = joint.dia.Cell.extend({ - if (opt.parentRelative) { - x += parentPosition.x; - y += parentPosition.y; - } + // The default markup for links. + markup: [ + '<path class="connection" stroke="black" d="M 0 0 0 0"/>', + '<path class="marker-source" fill="black" stroke="black" d="M 0 0 0 0"/>', + '<path class="marker-target" fill="black" stroke="black" d="M 0 0 0 0"/>', + '<path class="connection-wrap" d="M 0 0 0 0"/>', + '<g class="labels"/>', + '<g class="marker-vertices"/>', + '<g class="marker-arrowheads"/>', + '<g class="link-tools"/>' + ].join(''), - if (opt.deep) { - var currentPosition = this.get('position'); - this.translate(x - currentPosition.x, y - currentPosition.y, opt); - } else { - this.set('position', { x: x, y: y }, opt); + toolMarkup: [ + '<g class="link-tool">', + '<g class="tool-remove" event="remove">', + '<circle r="11" />', + '<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z" />', + '<title>Remove link.', + '', + '', + '', + '', + 'Link options.', + '', + '' + ].join(''), + + doubleToolMarkup: undefined, + + // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). + // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for + // dragging vertices (changin their position). The latter is used for removing vertices. + vertexMarkup: [ + '', + '', + '', + '', + 'Remove vertex.', + '', + '' + ].join(''), + + arrowheadMarkup: [ + '', + '', + '' + ].join(''), + + // may be overwritten by user to change default label (its markup, attrs, position) + defaultLabel: undefined, + + // deprecated + // may be overwritten by user to change default label markup + // lower priority than defaultLabel.markup + labelMarkup: undefined, + + // private + _builtins: { + defaultLabel: { + // builtin default markup: + // used if neither defaultLabel.markup + // nor label.markup is set + markup: [ + { + tagName: 'rect', + selector: 'rect' // faster than tagName CSS selector + }, { + tagName: 'text', + selector: 'text' // faster than tagName CSS selector + } + ], + // builtin default attributes: + // applied only if builtin default markup is used + attrs: { + text: { + fill: '#000000', + fontSize: 14, + textAnchor: 'middle', + yAlignment: 'middle', + pointerEvents: 'none' + }, + rect: { + ref: 'text', + fill: '#ffffff', + rx: 3, + ry: 3, + refWidth: 1, + refHeight: 1, + refX: 0, + refY: 0 + } + }, + // builtin default position: + // used if neither defaultLabel.position + // nor label.position is set + position: { + distance: 0.5 } + } + }, - return this; + defaults: { + type: 'link', + source: {}, + target: {} + }, - } else { // Getter returns a geometry point. + isLink: function() { - var elementPosition = g.point(this.get('position')); + return true; + }, - return opt.parentRelative - ? elementPosition.difference(parentPosition) - : elementPosition; + disconnect: function(opt) { + + return this.set({ + source: { x: 0, y: 0 }, + target: { x: 0, y: 0 } + }, opt); + }, + + source: function(source, args, opt) { + + // getter + if (source === undefined) { + return joint.util.clone(this.get('source')); + } + + // setter + var localSource; + var localOpt; + + // `source` is a cell + // take only its `id` and combine with `args` + var isCellProvided = source instanceof joint.dia.Cell; + if (isCellProvided) { // three arguments + localSource = joint.util.clone(args) || {}; + localSource.id = source.id; + localOpt = opt; + return this.set('source', localSource, localOpt); + } + + // `source` is a g.Point + // take only its `x` and `y` and combine with `args` + var isPointProvided = source instanceof g.Point; + if (isPointProvided) { // three arguments + localSource = joint.util.clone(args) || {}; + localSource.x = source.x; + localSource.y = source.y; + localOpt = opt; + return this.set('source', localSource, localOpt); } + + // `source` is an object + // no checking + // two arguments + localSource = source; + localOpt = args; + return this.set('source', localSource, localOpt); }, - translate: function(tx, ty, opt) { + target: function(target, args, opt) { - tx = tx || 0; - ty = ty || 0; + // getter + if (target === undefined) { + return joint.util.clone(this.get('target')); + } - if (tx === 0 && ty === 0) { - // Like nothing has happened. - return this; + // setter + var localTarget; + var localOpt; + + // `target` is a cell + // take only its `id` argument and combine with `args` + var isCellProvided = target instanceof joint.dia.Cell; + if (isCellProvided) { // three arguments + localTarget = joint.util.clone(args) || {}; + localTarget.id = target.id; + localOpt = opt; + return this.set('target', localTarget, localOpt); } - opt = opt || {}; - // Pass the initiator of the translation. - opt.translateBy = opt.translateBy || this.id; + // `target` is a g.Point + // take only its `x` and `y` and combine with `args` + var isPointProvided = target instanceof g.Point; + if (isPointProvided) { // three arguments + localTarget = joint.util.clone(args) || {}; + localTarget.x = target.x; + localTarget.y = target.y; + localOpt = opt; + return this.set('target', localTarget, localOpt); + } - var position = this.get('position') || { x: 0, y: 0 }; + // `target` is an object + // no checking + // two arguments + localTarget = target; + localOpt = args; + return this.set('target', localTarget, localOpt); + }, - if (opt.restrictedArea && opt.translateBy === this.id) { + router: function(name, args, opt) { - // We are restricting the translation for the element itself only. We get - // the bounding box of the element including all its embeds. - // All embeds have to be translated the exact same way as the element. - var bbox = this.getBBox({ deep: true }); - var ra = opt.restrictedArea; - //- - - - - - - - - - - - -> ra.x + ra.width - // - - - -> position.x | - // -> bbox.x - // ▓▓▓▓▓▓▓ | - // ░░░░░░░▓▓▓▓▓▓▓ - // ░░░░░░░░░ | - // ▓▓▓▓▓▓▓▓░░░░░░░ - // ▓▓▓▓▓▓▓▓ | - // <-dx-> | restricted area right border - // <-width-> | ░ translated element - // <- - bbox.width - -> ▓ embedded element - var dx = position.x - bbox.x; - var dy = position.y - bbox.y; - // Find the maximal/minimal coordinates that the element can be translated - // while complies the restrictions. - var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); - var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); - // recalculate the translation taking the resctrictions into account. - tx = x - position.x; - ty = y - position.y; + // getter + if (name === undefined) { + router = this.get('router'); + if (!router) { + if (this.get('manhattan')) return { name: 'orthogonal' }; // backwards compatibility + return null; + } + if (typeof router === 'object') return joint.util.clone(router); + return router; // e.g. a function } - var translatedPosition = { - x: position.x + tx, - y: position.y + ty - }; + // setter + var isRouterProvided = ((typeof name === 'object') || (typeof name === 'function')); + var localRouter = isRouterProvided ? name : { name: name, args: args }; + var localOpt = isRouterProvided ? args : opt; + + return this.set('router', localRouter, localOpt); + }, + + connector: function(name, args, opt) { - // To find out by how much an element was translated in event 'change:position' handlers. - opt.tx = tx; - opt.ty = ty; + // getter + if (name === undefined) { + connector = this.get('connector'); + if (!connector) { + if (this.get('smooth')) return { name: 'smooth' }; // backwards compatibility + return null; + } + if (typeof connector === 'object') return joint.util.clone(connector); + return connector; // e.g. a function + } - if (opt.transition) { + // setter + var isConnectorProvided = ((typeof name === 'object' || typeof name === 'function')); + var localConnector = isConnectorProvided ? name : { name: name, args: args }; + var localOpt = isConnectorProvided ? args : opt; - if (!joint.util.isObject(opt.transition)) opt.transition = {}; + return this.set('connector', localConnector, localOpt); + }, - this.transition('position', translatedPosition, joint.util.assign({}, opt.transition, { - valueFunction: joint.util.interpolate.object - })); + // Labels API - } else { + // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. + label: function(idx, label, opt) { - this.set('position', translatedPosition, opt); - } + var labels = this.labels(); - // Recursively call `translate()` on all the embeds cells. - joint.util.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : 0; + if (idx < 0) idx = labels.length + idx; - return this; + // getter + if (arguments.length <= 1) return this.prop(['labels', idx]); + // setter + return this.prop(['labels', idx], label, opt); }, - size: function(width, height, opt) { + labels: function(labels, opt) { - var currentSize = this.get('size'); - // Getter - // () signature - if (width === undefined) { - return { - width: currentSize.width, - height: currentSize.height - }; - } - // Setter - // (size, opt) signature - if (joint.util.isObject(width)) { - opt = height; - height = joint.util.isNumber(width.height) ? width.height : currentSize.height; - width = joint.util.isNumber(width.width) ? width.width : currentSize.width; + // getter + if (arguments.length === 0) { + labels = this.get('labels'); + if (!Array.isArray(labels)) return []; + return labels.slice(); } + // setter + if (!Array.isArray(labels)) labels = []; + return this.set('labels', labels, opt); + }, - return this.resize(width, height, opt); + insertLabel: function(idx, label, opt) { + + if (!label) throw new Error('dia.Link: no label provided'); + + var labels = this.labels(); + var n = labels.length; + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : n; + if (idx < 0) idx = n + idx + 1; + + labels.splice(idx, 0, label); + return this.labels(labels, opt); }, - resize: function(width, height, opt) { + // convenience function + // add label to end of labels array + appendLabel: function(label, opt) { - opt = opt || {}; + return this.insertLabel(-1, label, opt); + }, - this.startBatch('resize', opt); + removeLabel: function(idx, opt) { - if (opt.direction) { + var labels = this.labels(); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : -1; - var currentSize = this.get('size'); + labels.splice(idx, 1); + return this.labels(labels, opt); + }, - switch (opt.direction) { + // Vertices API - case 'left': - case 'right': - // Don't change height when resizing horizontally. - height = currentSize.height; - break; + vertex: function(idx, vertex, opt) { - case 'top': - case 'bottom': - // Don't change width when resizing vertically. - width = currentSize.width; - break; - } + var vertices = this.vertices(); - // Get the angle and clamp its value between 0 and 360 degrees. - var angle = g.normalizeAngle(this.get('angle') || 0); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : 0; + if (idx < 0) idx = vertices.length + idx; - var quadrant = { - 'top-right': 0, - 'right': 0, - 'top-left': 1, - 'top': 1, - 'bottom-left': 2, - 'left': 2, - 'bottom-right': 3, - 'bottom': 3 - }[opt.direction]; + // getter + if (arguments.length <= 1) return this.prop(['vertices', idx]); + // setter + return this.prop(['vertices', idx], vertex, opt); + }, - if (opt.absolute) { + vertices: function(vertices, opt) { - // We are taking the element's rotation into account - quadrant += Math.floor((angle + 45) / 90); - quadrant %= 4; - } + // getter + if (arguments.length === 0) { + vertices = this.get('vertices'); + if (!Array.isArray(vertices)) return []; + return vertices.slice(); + } + // setter + if (!Array.isArray(vertices)) vertices = []; + return this.set('vertices', vertices, opt); + }, - // This is a rectangle in size of the unrotated element. - var bbox = this.getBBox(); + insertVertex: function(idx, vertex, opt) { - // Pick the corner point on the element, which meant to stay on its place before and - // after the rotation. - var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); + if (!vertex) throw new Error('dia.Link: no vertex provided'); - // Find an image of the previous indent point. This is the position, where is the - // point actually located on the screen. - var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); + var vertices = this.vertices(); + var n = vertices.length; + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : n; + if (idx < 0) idx = n + idx + 1; - // Every point on the element rotates around a circle with the centre of rotation - // in the middle of the element while the whole element is being rotated. That means - // that the distance from a point in the corner of the element (supposed its always rect) to - // the center of the element doesn't change during the rotation and therefore it equals - // to a distance on unrotated element. - // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. - var radius = Math.sqrt((width * width) + (height * height)) / 2; + vertices.splice(idx, 0, vertex); + return this.vertices(vertices, opt); + }, - // Now we are looking for an angle between x-axis and the line starting at image of fixed point - // and ending at the center of the element. We call this angle `alpha`. + removeVertex: function(idx, opt) { - // The image of a fixed point is located in n-th quadrant. For each quadrant passed - // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. - // - // 3 | 2 - // --c-- Quadrant positions around the element's center `c` - // 0 | 1 - // - var alpha = quadrant * Math.PI / 2; + var vertices = this.vertices(); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : -1; - // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis - // going through the center of the element) and line crossing the indent of the fixed point and the center - // of the element. This is the angle we need but on the unrotated element. - alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); + vertices.splice(idx, 1); + return this.vertices(vertices, opt); + }, - // Lastly we have to deduct the original angle the element was rotated by and that's it. - alpha -= g.toRad(angle); + // Transformations - // With this angle and distance we can easily calculate the centre of the unrotated element. - // Note that fromPolar constructor accepts an angle in radians. - var center = g.point.fromPolar(radius, alpha, imageFixedPoint); + translate: function(tx, ty, opt) { - // The top left corner on the unrotated element has to be half a width on the left - // and half a height to the top from the center. This will be the origin of rectangle - // we were looking for. - var origin = g.point(center).offset(width / -2, height / -2); + // enrich the option object + opt = opt || {}; + opt.translateBy = opt.translateBy || this.id; + opt.tx = tx; + opt.ty = ty; - // Resize the element (before re-positioning it). - this.set('size', { width: width, height: height }, opt); + return this.applyToPoints(function(p) { + return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; + }, opt); + }, - // Finally, re-position the element. - this.position(origin.x, origin.y, opt); + scale: function(sx, sy, origin, opt) { - } else { + return this.applyToPoints(function(p) { + return g.point(p).scale(sx, sy, origin).toJSON(); + }, opt); + }, - // Resize the element. - this.set('size', { width: width, height: height }, opt); + applyToPoints: function(fn, opt) { + + if (!joint.util.isFunction(fn)) { + throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); } - this.stopBatch('resize', opt); + var attrs = {}; - return this; - }, + var source = this.source(); + if (!source.id) { + attrs.source = fn(source); + } - scale: function(sx, sy, origin, opt) { + var target = this.target(); + if (!target.id) { + attrs.target = fn(target); + } - var scaledBBox = this.getBBox().scale(sx, sy, origin); - this.startBatch('scale', opt); - this.position(scaledBBox.x, scaledBBox.y, opt); - this.resize(scaledBBox.width, scaledBBox.height, opt); - this.stopBatch('scale'); - return this; + var vertices = this.vertices(); + if (vertices.length > 0) { + attrs.vertices = vertices.map(fn); + } + + return this.set(attrs, opt); }, - fitEmbeds: function(opt) { + reparent: function(opt) { - opt = opt || {}; + var newParent; - // Getting the children's size and position requires the collection. - // Cell.get('embdes') helds an array of cell ids only. - if (!this.graph) throw new Error('Element must be part of a graph.'); + if (this.graph) { - var embeddedCells = this.getEmbeddedCells(); + var source = this.getSourceElement(); + var target = this.getTargetElement(); + var prevParent = this.getParentCell(); - if (embeddedCells.length > 0) { + if (source && target) { + newParent = this.graph.getCommonAncestor(source, target); + } - this.startBatch('fit-embeds', opt); + if (prevParent && (!newParent || newParent.id !== prevParent.id)) { + // Unembed the link if source and target has no common ancestor + // or common ancestor changed + prevParent.unembed(this, opt); + } - if (opt.deep) { - // Recursively apply fitEmbeds on all embeds first. - joint.util.invoke(embeddedCells, 'fitEmbeds', opt); + if (newParent) { + newParent.embed(this, opt); } + } - // Compute cell's size and position based on the children bbox - // and given padding. - var bbox = this.graph.getCellsBBox(embeddedCells); - var padding = joint.util.normalizeSides(opt.padding); + return newParent; + }, - // Apply padding computed above to the bbox. - bbox.moveAndExpand({ - x: -padding.left, - y: -padding.top, - width: padding.right + padding.left, - height: padding.bottom + padding.top - }); + hasLoop: function(opt) { - // Set new element dimensions finally. - this.set({ - position: { x: bbox.x, y: bbox.y }, - size: { width: bbox.width, height: bbox.height } - }, opt); + opt = opt || {}; - this.stopBatch('fit-embeds'); + var sourceId = this.source().id; + var targetId = this.target().id; + + if (!sourceId || !targetId) { + // Link "pinned" to the paper does not have a loop. + return false; } - return this; + var loop = sourceId === targetId; + + // Note that there in the deep mode a link can have a loop, + // even if it connects only a parent and its embed. + // A loop "target equals source" is valid in both shallow and deep mode. + if (!loop && opt.deep && this.graph) { + + var sourceElement = this.getSourceElement(); + var targetElement = this.getTargetElement(); + + loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); + } + + return loop; + }, + + // unlike source(), this method returns null if source is a point + getSourceElement: function() { + + var source = this.source(); + var graph = this.graph; + + return (source && source.id && graph && graph.getCell(source.id)) || null; + }, + + // unlike target(), this method returns null if target is a point + getTargetElement: function() { + + var target = this.target(); + var graph = this.graph; + + return (target && target.id && graph && graph.getCell(target.id)) || null; }, - // Rotate element by `angle` degrees, optionally around `origin` point. - // If `origin` is not provided, it is considered to be the center of the element. - // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not - // the difference from the previous angle. - rotate: function(angle, absolute, origin, opt) { + // Returns the common ancestor for the source element, + // target element and the link itself. + getRelationshipAncestor: function() { - if (origin) { + var connectionAncestor; - var center = this.getBBox().center(); - var size = this.get('size'); - var position = this.get('position'); - center.rotate(origin, this.get('angle') - angle); - var dx = center.x - size.width / 2 - position.x; - var dy = center.y - size.height / 2 - position.y; - this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); - this.position(position.x + dx, position.y + dy, opt); - this.rotate(angle, absolute, null, opt); - this.stopBatch('rotate'); + if (this.graph) { - } else { + var cells = [ + this, + this.getSourceElement(), // null if source is a point + this.getTargetElement() // null if target is a point + ].filter(function(item) { + return !!item; + }); - this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360, opt); + connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); } - return this; + return connectionAncestor || null; }, - getBBox: function(opt) { + // Is source, target and the link itself embedded in a given cell? + isRelationshipEmbeddedIn: function(cell) { - opt = opt || {}; + var cellId = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : cell.id; + var ancestor = this.getRelationshipAncestor(); - if (opt.deep && this.graph) { + return !!ancestor && (ancestor.id === cellId || ancestor.isEmbeddedIn(cellId)); + }, - // Get all the embedded elements using breadth first algorithm, - // that doesn't use recursion. - var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - // Add the model itself. - elements.push(this); + // Get resolved default label. + _getDefaultLabel: function() { - return this.graph.getCellsBBox(elements); - } + var defaultLabel = this.get('defaultLabel') || this.defaultLabel || {}; - var position = this.get('position'); - var size = this.get('size'); + var label = {}; + label.markup = defaultLabel.markup || this.get('labelMarkup') || this.labelMarkup; + label.position = defaultLabel.position; + label.attrs = defaultLabel.attrs; + label.size = defaultLabel.size; - return g.rect(position.x, position.y, size.width, size.height); + return label; } -}); - -// joint.dia.Element base view and controller. -// ------------------------------------------- +}, + { + endsEqual: function(a, b) { + var portsEqual = a.port === b.port || !a.port && !b.port; + return a.id === b.id && portsEqual; + } + }); -joint.dia.ElementView = joint.dia.CellView.extend({ - /** - * @abstract - */ - _removePorts: function() { - // implemented in ports.js - }, +// joint.dia.Link base view and controller. +// ---------------------------------------- - /** - * - * @abstract - */ - _renderPorts: function() { - // implemented in ports.js - }, +joint.dia.LinkView = joint.dia.CellView.extend({ className: function() { var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); - classNames.push('element'); + classNames.push('link'); return classNames.join(' '); }, - initialize: function() { + options: { + + shortLinkLength: 105, + doubleLinkTools: false, + longLinkLength: 155, + linkToolsOffset: 40, + doubleLinkToolsOffset: 65, + sampleInterval: 50, + }, + + _labelCache: null, + _labelSelectors: null, + _markerCache: null, + _V: null, + _dragData: null, // deprecated + + metrics: null, + decimalsRounding: 2, + + initialize: function(options) { joint.dia.CellView.prototype.initialize.apply(this, arguments); - var model = this.model; + // create methods in prototype, so they can be accessed from any instance and + // don't need to be create over and over + if (typeof this.constructor.prototype.watchSource !== 'function') { + this.constructor.prototype.watchSource = this.createWatcher('source'); + this.constructor.prototype.watchTarget = this.createWatcher('target'); + } - this.listenTo(model, 'change:position', this.translate); - this.listenTo(model, 'change:size', this.resize); - this.listenTo(model, 'change:angle', this.rotate); - this.listenTo(model, 'change:markup', this.render); + // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to + // `` nodes wrapped by Vectorizer. This allows for quick access to the + // nodes in `updateLabelPosition()` in order to update the label positions. + this._labelCache = {}; - this._initializePorts(); + // a cache of label selectors + this._labelSelectors = {}; + + // keeps markers bboxes and positions again for quicker access + this._markerCache = {}; + + // cache of default markup nodes + this._V = {}, + + // connection path metrics + this.metrics = {}, + + // bind events + this.startListening(); }, - /** - * @abstract - */ - _initializePorts: function() { + startListening: function() { + var model = this.model; + + this.listenTo(model, 'change:markup', this.render); + this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); + this.listenTo(model, 'change:toolMarkup', this.onToolsChange); + this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); + this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); + this.listenTo(model, 'change:source', this.onSourceChange); + this.listenTo(model, 'change:target', this.onTargetChange); }, - update: function(cell, renderingOnlyAttrs) { + onSourceChange: function(cell, source, opt) { - this._removePorts(); + // Start watching the new source model. + this.watchSource(cell, source); + // This handler is called when the source attribute is changed. + // This can happen either when someone reconnects the link (or moves arrowhead), + // or when an embedded link is translated by its ancestor. + // 1. Always do update. + // 2. Do update only if the opposite end ('target') is also a point. + var model = this.model; + if (!opt.translateBy || !model.get('target').id || !source.id) { + this.update(model, null, opt); + } + }, + + onTargetChange: function(cell, target, opt) { + // Start watching the new target model. + this.watchTarget(cell, target); + // See `onSourceChange` method. var model = this.model; - var modelAttrs = model.attr(); - this.updateDOMSubtreeAttributes(this.el, modelAttrs, { - rootBBox: g.Rect(model.size()), - scalableNode: this.scalableNode, - rotatableNode: this.rotatableNode, - // Use rendering only attributes if they differs from the model attributes - roAttributes: (renderingOnlyAttrs === modelAttrs) ? null : renderingOnlyAttrs - }); + if (!opt.translateBy || (model.get('source').id && !target.id && joint.util.isEmpty(model.get('vertices')))) { + this.update(model, null, opt); + } + }, - this._renderPorts(); + onVerticesChange: function(cell, changed, opt) { + + this.renderVertexMarkers(); + + // If the vertices have been changed by a translation we do update only if the link was + // the only link that was translated. If the link was translated via another element which the link + // is embedded in, this element will be translated as well and that triggers an update. + // Note that all embeds in a model are sorted - first comes links, then elements. + if (!opt.translateBy || opt.translateBy === this.model.id) { + // Vertices were changed (not as a reaction on translate) + // or link.translate() was called or + this.update(cell, null, opt); + } }, - // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the - // default markup is not desirable. - renderMarkup: function() { + onToolsChange: function() { - var markup = this.model.get('markup') || this.model.markup; + this.renderTools().updateToolsPosition(); + }, - if (markup) { + onLabelsChange: function(link, labels, opt) { - var svg = joint.util.template(markup)(); - var nodes = V(svg); + var requireRender = true; - this.vel.append(nodes); + var previousLabels = this.model.previous('labels'); - } else { + if (previousLabels) { + // Here is an optimalization for cases when we know, that change does + // not require rerendering of all labels. + if (('propertyPathArray' in opt) && ('propertyValue' in opt)) { + // The label is setting by `prop()` method + var pathArray = opt.propertyPathArray || []; + var pathLength = pathArray.length; + if (pathLength > 1) { + // We are changing a single label here e.g. 'labels/0/position' + var labelExists = !!previousLabels[pathArray[1]]; + if (labelExists) { + if (pathLength === 2) { + // We are changing the entire label. Need to check if the + // markup is also being changed. + requireRender = ('markup' in Object(opt.propertyValue)); + } else if (pathArray[2] !== 'markup') { + // We are changing a label property but not the markup + requireRender = false; + } + } + } + } + } - throw new Error('properties.markup is missing while the default render() implementation is used.'); + if (requireRender) { + this.renderLabels(); + } else { + this.updateLabels(); } + + this.updateLabelPositions(); }, - render: function() { + // Rendering. + // ---------- - this.$el.empty(); + render: function() { + this.vel.empty(); + this._V = {}; this.renderMarkup(); - this.rotatableNode = this.vel.findOne('.rotatable'); - var scalable = this.scalableNode = this.vel.findOne('.scalable'); - if (scalable) { - // Double update is necessary for elements with the scalable group only - // Note the resize() triggers the other `update`. - this.update(); - } - this.resize(); - this.rotate(); - this.translate(); + // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox + // returns zero values) + this.renderLabels(); + // start watching the ends of the link for changes + var model = this.model; + this.watchSource(model, model.source()) + .watchTarget(model, model.target()) + .update(); return this; }, - resize: function(cell, changed, opt) { + renderMarkup: function() { + + var link = this.model; + var markup = link.get('markup') || link.markup; + if (!markup) throw new Error('dia.LinkView: markup required'); + if (Array.isArray(markup)) return this.renderJSONMarkup(markup); + if (typeof markup === 'string') return this.renderStringMarkup(markup); + throw new Error('dia.LinkView: invalid markup'); + }, + + renderJSONMarkup: function(markup) { + + var doc = joint.util.parseDOMJSON(markup); + // Selectors + var selectors = this.selectors = doc.selectors; + var rootSelector = this.selector; + if (selectors[rootSelector]) throw new Error('dia.LinkView: ambiguous root selector.'); + selectors[rootSelector] = this.el; + // Fragment + this.vel.append(doc.fragment); + }, + + renderStringMarkup: function(markup) { + + // A special markup can be given in the `properties.markup` property. This might be handy + // if e.g. arrowhead markers should be `` elements or any other element than ``s. + // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors + // of elements with special meaning though. Therefore, those classes should be preserved in any + // special markup passed in `properties.markup`. + var children = V(markup); + // custom markup may contain only one children + if (!Array.isArray(children)) children = [children]; + // Cache all children elements for quicker access. + var cache = this._V; // vectorized markup; + for (var i = 0, n = children.length; i < n; i++) { + var child = children[i]; + var className = child.attr('class'); + if (className) { + // Strip the joint class name prefix, if there is one. + className = joint.util.removeClassNamePrefix(className); + cache[$.camelCase(className)] = child; + } + } + // partial rendering + this.renderTools(); + this.renderVertexMarkers(); + this.renderArrowheadMarkers(); + this.vel.append(children); + }, + + _getLabelMarkup: function(labelMarkup) { - var model = this.model; - var size = model.get('size') || { width: 1, height: 1 }; - var angle = model.get('angle') || 0; - - var scalable = this.scalableNode; - if (!scalable) { - - if (angle !== 0) { - // update the origin of the rotation - this.rotate(); - } - // update the ref attributes - this.update(); + if (!labelMarkup) return undefined; - // If there is no scalable elements, than there is nothing to scale. - return; - } + if (Array.isArray(labelMarkup)) return this._getLabelJSONMarkup(labelMarkup); + if (typeof labelMarkup === 'string') return this._getLabelStringMarkup(labelMarkup); + throw new Error('dia.linkView: invalid label markup'); + }, - // Getting scalable group's bbox. - // Due to a bug in webkit's native SVG .getBBox implementation, the bbox of groups with path children includes the paths' control points. - // To work around the issue, we need to check whether there are any path elements inside the scalable group. - var recursive = false; - if (scalable.node.getElementsByTagName('path').length > 0) { - // If scalable has at least one descendant that is a path, we need to switch to recursive bbox calculation. - // If there are no path descendants, group bbox calculation works and so we can use the (faster) native function directly. - recursive = true; - } - var scalableBBox = scalable.getBBox({ recursive: recursive }); + _getLabelJSONMarkup: function(labelMarkup) { - // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making - // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. - var sx = (size.width / (scalableBBox.width || 1)); - var sy = (size.height / (scalableBBox.height || 1)); - scalable.attr('transform', 'scale(' + sx + ',' + sy + ')'); + return joint.util.parseDOMJSON(labelMarkup); // fragment and selectors + }, - // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` - // Order of transformations is significant but we want to reconstruct the object always in the order: - // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, - // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the - // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation - // around the center of the resized object (which is a different origin then the origin of the previous rotation) - // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. + _getLabelStringMarkup: function(labelMarkup) { - // Cancel the rotation but now around a different origin, which is the center of the scaled object. - var rotatable = this.rotatableNode; - var rotation = rotatable && rotatable.attr('transform'); - if (rotation && rotation !== 'null') { + var children = V(labelMarkup); + var fragment = document.createDocumentFragment(); - rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); - var rotatableBBox = scalable.getBBox({ target: this.paper.viewport }); + if (!Array.isArray(children)) { + fragment.append(children.node); - // Store new x, y and perform rotate() again against the new rotation origin. - model.set('position', { x: rotatableBBox.x, y: rotatableBBox.y }, opt); - this.rotate(); + } else { + for (var i = 0, n = children.length; i < n; i++) { + var currentChild = children[i].node; + fragment.appendChild(currentChild); + } } - // Update must always be called on non-rotated element. Otherwise, relative positioning - // would work with wrong (rotated) bounding boxes. - this.update(); + return { fragment: fragment, selectors: {} }; // no selectors }, - translate: function(model, changes, opt) { - - var position = this.model.get('position') || { x: 0, y: 0 }; + // Label markup fragment may come wrapped in , or not. + // If it doesn't, add the container here. + _normalizeLabelMarkup: function(markup) { - this.vel.attr('transform', 'translate(' + position.x + ',' + position.y + ')'); - }, + if (!markup) return undefined; - rotate: function() { + var fragment = markup.fragment; + if (!(markup.fragment instanceof DocumentFragment) || !markup.fragment.hasChildNodes()) throw new Error('dia.LinkView: invalid label markup.'); - var rotatable = this.rotatableNode; - if (!rotatable) { - // If there is no rotatable elements, then there is nothing to rotate. - return; - } + var vNode; + var childNodes = fragment.childNodes; - var angle = this.model.get('angle') || 0; - var size = this.model.get('size') || { width: 1, height: 1 }; + if ((childNodes.length > 1) || childNodes[0].nodeName.toUpperCase() !== 'G') { + // default markup fragment is not wrapped in + // add a container - var ox = size.width / 2; - var oy = size.height / 2; + vNode = V('g'); + vNode.append(fragment); + vNode.addClass('label'); - if (angle !== 0) { - rotatable.attr('transform', 'rotate(' + angle + ',' + ox + ',' + oy + ')'); } else { - rotatable.removeAttr('transform'); + vNode = V(childNodes[0]); + vNode.addClass('label'); } + + return { node: vNode.node, selectors: markup.selectors }; }, - getBBox: function(opt) { + renderLabels: function() { - if (opt && opt.useModelGeometry) { - var bbox = this.model.getBBox().bbox(this.model.get('angle')); - return this.paper.localToPaperRect(bbox); + var cache = this._V; + var vLabels = cache.labels; + var labelCache = this._labelCache = {}; + var labelSelectors = this._labelSelectors = {}; + + if (vLabels) vLabels.empty(); + + var model = this.model; + var labels = model.get('labels') || []; + var labelsCount = labels.length; + if (labelsCount === 0) return this; + + if (!vLabels) { + // there is no label container in the markup but some labels are defined + // add a container + vLabels = cache.labels = V('g').addClass('labels').appendTo(this.el); } - return joint.dia.CellView.prototype.getBBox.apply(this, arguments); - }, + for (var i = 0; i < labelsCount; i++) { - // Embedding mode methods - // ---------------------- + var label = labels[i]; + var labelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(label.markup)); - prepareEmbedding: function(opt) { + var node; + var selectors; + if (labelMarkup) { + node = labelMarkup.node; + selectors = labelMarkup.selectors; - opt = opt || {}; + } else { + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(builtinDefaultLabel.markup)); - var model = opt.model || this.model; - var paper = opt.paper || this.paper; - var graph = paper.model; + var defaultLabel = model._getDefaultLabel(); + var defaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(defaultLabel.markup)); - model.startBatch('to-front', opt); + var defaultMarkup = defaultLabelMarkup || builtinDefaultLabelMarkup; - // Bring the model to the front with all his embeds. - model.toFront({ deep: true, ui: true }); + node = defaultMarkup.node; + selectors = defaultMarkup.selectors; + } - // Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see - // the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z. - var maxZ = graph.get('cells').max('z').get('z'); - var connectedLinks = graph.getConnectedLinks(model, { deep: true }); + var vLabel = V(node); + vLabel.attr('label-idx', i); // assign label-idx + vLabel.appendTo(vLabels); + labelCache[i] = vLabel; // cache node for `updateLabels()` so it can just update label node positions - // Move to front also all the inbound and outbound links that are connected - // to any of the element descendant. If we bring to front only embedded elements, - // links connected to them would stay in the background. - joint.util.invoke(connectedLinks, 'set', 'z', maxZ + 1, { ui: true }); + selectors[this.selector] = vLabel.node; + labelSelectors[i] = selectors; // cache label selectors for `updateLabels()` + } - model.stopBatch('to-front'); + this.updateLabels(); - // Before we start looking for suitable parent we remove the current one. - var parentId = model.get('parent'); - parentId && graph.getCell(parentId).unembed(model, { ui: true }); + return this; }, - processEmbedding: function(opt) { + // merge default label attrs into label attrs + // keep `undefined` or `null` because `{}` means something else + _mergeLabelAttrs: function(hasCustomMarkup, labelAttrs, defaultLabelAttrs, builtinDefaultLabelAttrs) { - opt = opt || {}; + if (labelAttrs === null) return null; + if (labelAttrs === undefined) { - var model = opt.model || this.model; - var paper = opt.paper || this.paper; + if (defaultLabelAttrs === null) return null; + if (defaultLabelAttrs === undefined) { - var paperOptions = paper.options; - var candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); + if (hasCustomMarkup) return undefined; + return builtinDefaultLabelAttrs; + } - if (paperOptions.frontParentOnly) { - // pick the element with the highest `z` index - candidates = candidates.slice(-1); + if (hasCustomMarkup) return defaultLabelAttrs; + return joint.util.merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs); } - var newCandidateView = null; - var prevCandidateView = this._candidateEmbedView; + if (hasCustomMarkup) return joint.util.merge({}, defaultLabelAttrs, labelAttrs); + return joint.util.merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs, labelAttrs); + }, - // iterate over all candidates starting from the last one (has the highest z-index). - for (var i = candidates.length - 1; i >= 0; i--) { + updateLabels: function() { - var candidate = candidates[i]; + if (!this._V.labels) return this; - if (prevCandidateView && prevCandidateView.model.id == candidate.id) { + var model = this.model; + var labels = model.get('labels') || []; + var canLabelMove = this.can('labelMove'); - // candidate remains the same - newCandidateView = prevCandidateView; - break; + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelAttrs = builtinDefaultLabel.attrs; - } else { + var defaultLabel = model._getDefaultLabel(); + var defaultLabelMarkup = defaultLabel.markup; + var defaultLabelAttrs = defaultLabel.attrs; - var view = candidate.findView(paper); - if (paperOptions.validateEmbedding.call(paper, this, view)) { + for (var i = 0, n = labels.length; i < n; i++) { - // flip to the new candidate - newCandidateView = view; - break; - } - } - } + var vLabel = this._labelCache[i]; + vLabel.attr('cursor', (canLabelMove ? 'move' : 'default')); - if (newCandidateView && newCandidateView != prevCandidateView) { - // A new candidate view found. Highlight the new one. - this.clearEmbedding(); - this._candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); - } + var selectors = this._labelSelectors[i]; - if (!newCandidateView && prevCandidateView) { - // No candidate view found. Unhighlight the previous candidate. - this.clearEmbedding(); - } - }, + var label = labels[i]; + var labelMarkup = label.markup; + var labelAttrs = label.attrs; - clearEmbedding: function() { + var attrs = this._mergeLabelAttrs( + (labelMarkup || defaultLabelMarkup), + labelAttrs, + defaultLabelAttrs, + builtinDefaultLabelAttrs + ); - var candidateView = this._candidateEmbedView; - if (candidateView) { - // No candidate view found. Unhighlight the previous candidate. - candidateView.unhighlight(null, { embedding: true }); - this._candidateEmbedView = null; + this.updateDOMSubtreeAttributes(vLabel.node, attrs, { + rootBBox: new g.Rect(label.size), + selectors: selectors + }); } + + return this; }, - finalizeEmbedding: function(opt) { + renderTools: function() { - opt = opt || {}; + if (!this._V.linkTools) return this; - var candidateView = this._candidateEmbedView; - var model = opt.model || this.model; - var paper = opt.paper || this.paper; + // Tools are a group of clickable elements that manipulate the whole link. + // A good example of this is the remove tool that removes the whole link. + // Tools appear after hovering the link close to the `source` element/point of the link + // but are offset a bit so that they don't cover the `marker-arrowhead`. - if (candidateView) { + var $tools = $(this._V.linkTools.node).empty(); + var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); + var tool = V(toolTemplate()); - // We finished embedding. Candidate view is chosen to become the parent of the model. - candidateView.model.embed(model, { ui: true }); - candidateView.unhighlight(null, { embedding: true }); + $tools.append(tool.node); - delete this._candidateEmbedView; - } + // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. + this._toolCache = tool; - joint.util.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); - }, + // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the + // link as well but only if the link is longer than `longLinkLength`. + if (this.options.doubleLinkTools) { - // Interaction. The controller part. - // --------------------------------- + var tool2; + if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { + toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); + tool2 = V(toolTemplate()); + } else { + tool2 = tool.clone(); + } - pointerdown: function(evt, x, y) { + $tools.append(tool2.node); + this._tool2Cache = tool2; + } - var paper = this.paper; + return this; + }, - if ( - evt.target.getAttribute('magnet') && - this.can('addLinkFromMagnet') && - paper.options.validateMagnet.call(paper, this, evt.target) - ) { + renderVertexMarkers: function() { - this.model.startBatch('add-link'); + if (!this._V.markerVertices) return this; - var link = paper.getDefaultLink(this, evt.target); + var $markerVertices = $(this._V.markerVertices.node).empty(); - link.set({ - source: { - id: this.model.id, - selector: this.getSelector(evt.target), - port: evt.target.getAttribute('port') - }, - target: { x: x, y: y } - }); + // A special markup can be given in the `properties.vertexMarkup` property. This might be handy + // if default styling (elements) are not desired. This makes it possible to use any + // SVG elements for .marker-vertex and .marker-vertex-remove tools. + var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); - paper.model.addCell(link); + this.model.vertices().forEach(function(vertex, idx) { - var linkView = this._linkView = paper.findViewByModel(link); + $markerVertices.append(V(markupTemplate(joint.util.assign({ idx: idx }, vertex))).node); + }); - linkView.pointerdown(evt, x, y); - linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); + return this; + }, - } else { + renderArrowheadMarkers: function() { - this._dx = x; - this._dy = y; + // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. + if (!this._V.markerArrowheads) return this; - this.restrictedArea = paper.getRestrictedArea(this); + var $markerArrowheads = $(this._V.markerArrowheads.node); - joint.dia.CellView.prototype.pointerdown.apply(this, arguments); - this.notify('element:pointerdown', evt, x, y); - } - }, + $markerArrowheads.empty(); - pointermove: function(evt, x, y) { + // A special markup can be given in the `properties.vertexMarkup` property. This might be handy + // if default styling (elements) are not desired. This makes it possible to use any + // SVG elements for .marker-vertex and .marker-vertex-remove tools. + var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); - if (this._linkView) { + this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); + this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); - // let the linkview deal with this event - this._linkView.pointermove(evt, x, y); + $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); - } else { + return this; + }, - var grid = this.paper.options.gridSize; + // Updating. + // --------- - if (this.can('elementMove')) { + // Default is to process the `attrs` object and set attributes on subelements based on the selectors. + update: function(model, attributes, opt) { - var position = this.model.get('position'); + opt || (opt = {}); - // Make sure the new element's position always snaps to the current grid after - // translate as the previous one could be calculated with a different grid size. - var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - this._dx, grid); - var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - this._dy, grid); + // update the link path + this.updateConnection(opt); - this.model.translate(tx, ty, { restrictedArea: this.restrictedArea, ui: true }); + // update SVG attributes defined by 'attrs/'. + this.updateDOMSubtreeAttributes(this.el, this.model.attr(), { selectors: this.selectors }); - if (this.paper.options.embeddingMode) { + this.updateDefaultConnectionPath(); - if (!this._inProcessOfEmbedding) { - // Prepare the element for embedding only if the pointer moves. - // We don't want to do unnecessary action with the element - // if an user only clicks/dblclicks on it. - this.prepareEmbedding(); - this._inProcessOfEmbedding = true; - } + // update the label position etc. + this.updateLabelPositions(); + this.updateToolsPosition(); + this.updateArrowheadMarkers(); - this.processEmbedding(); - } - } + this.updateTools(opt); + // Local perpendicular flag (as opposed to one defined on paper). + // Could be enabled inside a connector/router. It's valid only + // during the update execution. + this.options.perpendicular = null; + // Mark that postponed update has been already executed. + this.updatePostponed = false; - this._dx = g.snapToGrid(x, grid); - this._dy = g.snapToGrid(y, grid); + return this; + }, - joint.dia.CellView.prototype.pointermove.apply(this, arguments); - this.notify('element:pointermove', evt, x, y); + removeRedundantLinearVertices: function(opt) { + var link = this.model; + var vertices = link.vertices(); + var conciseVertices = []; + var n = vertices.length; + var m = 0; + for (var i = 0; i < n; i++) { + var current = new g.Point(vertices[i]).round(); + var prev = new g.Point(conciseVertices[m - 1] || this.sourceAnchor).round(); + if (prev.equals(current)) continue; + var next = new g.Point(vertices[i + 1] || this.targetAnchor).round(); + if (prev.equals(next)) continue; + var line = new g.Line(prev, next); + if (line.pointOffset(current) === 0) continue; + conciseVertices.push(vertices[i]); + m++; } + if (n === m) return 0; + link.vertices(conciseVertices, opt); + return (n - m); }, - pointerup: function(evt, x, y) { + updateDefaultConnectionPath: function() { - if (this._linkView) { + var cache = this._V; - // Let the linkview deal with this event. - this._linkView.pointerup(evt, x, y); - this._linkView = null; - this.model.stopBatch('add-link'); + if (cache.connection) { + cache.connection.attr('d', this.getSerializedConnection()); + } - } else { + if (cache.connectionWrap) { + cache.connectionWrap.attr('d', this.getSerializedConnection()); + } - if (this._inProcessOfEmbedding) { - this.finalizeEmbedding(); - this._inProcessOfEmbedding = false; - } + if (cache.markerSource && cache.markerTarget) { + this._translateAndAutoOrientArrows(cache.markerSource, cache.markerTarget); + } + }, - this.notify('element:pointerup', evt, x, y); - joint.dia.CellView.prototype.pointerup.apply(this, arguments); + getEndView: function(type) { + switch (type) { + case 'source': + return this.sourceView || null; + case 'target': + return this.targetView || null; + default: + throw new Error('dia.LinkView: type parameter required.'); } }, - mouseenter: function(evt) { + getEndAnchor: function(type) { + switch (type) { + case 'source': + return new g.Point(this.sourceAnchor); + case 'target': + return new g.Point(this.targetAnchor); + default: + throw new Error('dia.LinkView: type parameter required.'); + } + }, - joint.dia.CellView.prototype.mouseenter.apply(this, arguments); - this.notify('element:mouseenter', evt); + getEndMagnet: function(type) { + switch (type) { + case 'source': + var sourceView = this.sourceView; + if (!sourceView) break; + return this.sourceMagnet || sourceView.el; + case 'target': + var targetView = this.targetView; + if (!targetView) break; + return this.targetMagnet || targetView.el; + default: + throw new Error('dia.LinkView: type parameter required.'); + } + return null; }, - mouseleave: function(evt) { + updateConnection: function(opt) { - joint.dia.CellView.prototype.mouseleave.apply(this, arguments); - this.notify('element:mouseleave', evt); - } -}); + opt = opt || {}; + + var model = this.model; + var route, path; + if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { + // The link is being translated by an ancestor that will + // shift source point, target point and all vertices + // by an equal distance. + var tx = opt.tx || 0; + var ty = opt.ty || 0; -// joint.dia.Link base model. -// -------------------------- + route = (new g.Polyline(this.route)).translate(tx, ty).points; -joint.dia.Link = joint.dia.Cell.extend({ + // translate source and target connection and marker points. + this._translateConnectionPoints(tx, ty); - // The default markup for links. - markup: [ - '', - '', - '', - '', - '', - '', - '', - '' - ].join(''), + // translate the path itself + path = this.path; + path.translate(tx, ty); - labelMarkup: [ - '', - '', - '', - '' - ].join(''), + } else { - toolMarkup: [ - '', - '', - '', - '', - 'Remove link.', - '', - '', - '', - '', - 'Link options.', - '', - '' - ].join(''), + var vertices = model.vertices(); + // 1. Find Anchors - // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). - // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for - // dragging vertices (changin their position). The latter is used for removing vertices. - vertexMarkup: [ - '', - '', - '', - '', - 'Remove vertex.', - '', - '' - ].join(''), + var anchors = this.findAnchors(vertices); + var sourceAnchor = this.sourceAnchor = anchors.source; + var targetAnchor = this.targetAnchor = anchors.target; - arrowheadMarkup: [ - '', - '', - '' - ].join(''), + // 2. Find Route + route = this.findRoute(vertices, opt); - defaults: { + // 3. Find Connection Points + var connectionPoints = this.findConnectionPoints(route, sourceAnchor, targetAnchor); + var sourcePoint = this.sourcePoint = connectionPoints.source; + var targetPoint = this.targetPoint = connectionPoints.target; - type: 'link', - source: {}, - target: {} - }, + // 3b. Find Marker Connection Point - Backwards Compatibility + var markerPoints = this.findMarkerPoints(route, sourcePoint, targetPoint); - isLink: function() { + // 4. Find Connection + path = this.findPath(route, markerPoints.source || sourcePoint, markerPoints.target || targetPoint); + } - return true; + this.route = route; + this.path = path; + this.metrics = {}; }, - disconnect: function() { + findMarkerPoints: function(route, sourcePoint, targetPoint) { - return this.set({ source: g.point(0, 0), target: g.point(0, 0) }); - }, + var firstWaypoint = route[0]; + var lastWaypoint = route[route.length - 1]; - // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. - label: function(idx, value, opt) { + // Move the source point by the width of the marker taking into account + // its scale around x-axis. Note that scale is the only transform that + // makes sense to be set in `.marker-source` attributes object + // as all other transforms (translate/rotate) will be replaced + // by the `translateAndAutoOrient()` function. + var cache = this._markerCache; + // cache source and target points + var sourceMarkerPoint, targetMarkerPoint; - idx = idx || 0; + if (this._V.markerSource) { - // Is it a getter? - if (arguments.length <= 1) { - return this.prop(['labels', idx]); + cache.sourceBBox = cache.sourceBBox || this._V.markerSource.getBBox(); + sourceMarkerPoint = g.point(sourcePoint).move( + firstWaypoint || targetPoint, + cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 + ).round(); } - return this.prop(['labels', idx], value, opt); - }, + if (this._V.markerTarget) { - translate: function(tx, ty, opt) { + cache.targetBBox = cache.targetBBox || this._V.markerTarget.getBBox(); + targetMarkerPoint = g.point(targetPoint).move( + lastWaypoint || sourcePoint, + cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 + ).round(); + } - // enrich the option object - opt = opt || {}; - opt.translateBy = opt.translateBy || this.id; - opt.tx = tx; - opt.ty = ty; + // if there was no markup for the marker, use the connection point. + cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); + cache.targetPoint = targetMarkerPoint || targetPoint.clone(); - return this.applyToPoints(function(p) { - return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; - }, opt); + return { + source: sourceMarkerPoint, + target: targetMarkerPoint + } }, - scale: function(sx, sy, origin, opt) { + findAnchors: function(vertices) { - return this.applyToPoints(function(p) { - return g.point(p).scale(sx, sy, origin).toJSON(); - }, opt); - }, + var model = this.model; + var firstVertex = vertices[0]; + var lastVertex = vertices[vertices.length - 1]; + var sourceDef = model.get('source'); + var targetDef = model.get('target'); + var sourceView = this.sourceView; + var targetView = this.targetView; + var sourceMagnet, targetMagnet; + + // Anchor Source + var sourceAnchor; + if (sourceView) { + sourceMagnet = (this.sourceMagnet || sourceView.el); + var sourceAnchorRef; + if (firstVertex) { + sourceAnchorRef = new g.Point(firstVertex); + } else if (targetView) { + // TODO: the source anchor reference is not a point, how to deal with this? + sourceAnchorRef = this.targetMagnet || targetView.el; + } else { + sourceAnchorRef = new g.Point(targetDef); + } + sourceAnchor = this.getAnchor(sourceDef.anchor, sourceView, sourceMagnet, sourceAnchorRef, 'source'); + } else { + sourceAnchor = new g.Point(sourceDef); + } - applyToPoints: function(fn, opt) { + // Anchor Target + var targetAnchor; + if (targetView) { + targetMagnet = (this.targetMagnet || targetView.el); + var targetAnchorRef = new g.Point(lastVertex || sourceAnchor); + targetAnchor = this.getAnchor(targetDef.anchor, targetView, targetMagnet, targetAnchorRef, 'target'); + } else { + targetAnchor = new g.Point(targetDef); + } - if (!joint.util.isFunction(fn)) { - throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); + // Con + return { + source: sourceAnchor, + target: targetAnchor } + }, - var attrs = {}; + findConnectionPoints: function(route, sourceAnchor, targetAnchor) { - var source = this.get('source'); - if (!source.id) { - attrs.source = fn(source); + var firstWaypoint = route[0]; + var lastWaypoint = route[route.length - 1]; + var model = this.model; + var sourceDef = model.get('source'); + var targetDef = model.get('target'); + var sourceView = this.sourceView; + var targetView = this.targetView; + var paperOptions = this.paper.options; + var sourceMagnet, targetMagnet; + + // Connection Point Source + var sourcePoint; + if (sourceView) { + sourceMagnet = (this.sourceMagnet || sourceView.el); + var sourceConnectionPointDef = sourceDef.connectionPoint || paperOptions.defaultConnectionPoint; + var sourcePointRef = firstWaypoint || targetAnchor; + var sourceLine = new g.Line(sourcePointRef, sourceAnchor); + sourcePoint = this.getConnectionPoint(sourceConnectionPointDef, sourceView, sourceMagnet, sourceLine, 'source'); + } else { + sourcePoint = sourceAnchor; + } + // Connection Point Target + var targetPoint; + if (targetView) { + targetMagnet = (this.targetMagnet || targetView.el); + var targetConnectionPointDef = targetDef.connectionPoint || paperOptions.defaultConnectionPoint; + var targetPointRef = lastWaypoint || sourceAnchor; + var targetLine = new g.Line(targetPointRef, targetAnchor); + targetPoint = this.getConnectionPoint(targetConnectionPointDef, targetView, targetMagnet, targetLine, 'target'); + } else { + targetPoint = targetAnchor; } - var target = this.get('target'); - if (!target.id) { - attrs.target = fn(target); + return { + source: sourcePoint, + target: targetPoint } + }, - var vertices = this.get('vertices'); - if (vertices && vertices.length > 0) { - attrs.vertices = vertices.map(fn); + getAnchor: function(anchorDef, cellView, magnet, ref, endType) { + + if (!anchorDef) { + var paperOptions = this.paper.options; + if (paperOptions.perpendicularLinks || this.options.perpendicular) { + // Backwards compatibility + // If `perpendicularLinks` flag is set on the paper and there are vertices + // on the link, then try to find a connection point that makes the link perpendicular + // even though the link won't point to the center of the targeted object. + anchorDef = { name: 'perpendicular' }; + } else { + anchorDef = paperOptions.defaultAnchor; + } } - return this.set(attrs, opt); + if (!anchorDef) throw new Error('Anchor required.'); + var anchorFn; + if (typeof anchorDef === 'function') { + anchorFn = anchorDef; + } else { + var anchorName = anchorDef.name; + anchorFn = joint.anchors[anchorName]; + if (typeof anchorFn !== 'function') throw new Error('Unknown anchor: ' + anchorName); + } + var anchor = anchorFn.call(this, cellView, magnet, ref, anchorDef.args || {}, endType, this); + if (anchor) return anchor.round(this.decimalsRounding); + return new g.Point() }, - reparent: function(opt) { - var newParent; + getConnectionPoint: function(connectionPointDef, view, magnet, line, endType) { - if (this.graph) { + var connectionPoint; + var anchor = line.end; + // Backwards compatibility + var paperOptions = this.paper.options; + if (typeof paperOptions.linkConnectionPoint === 'function') { + connectionPoint = paperOptions.linkConnectionPoint(this, view, magnet, line.start, endType); + if (connectionPoint) return connectionPoint; + } - var source = this.graph.getCell(this.get('source').id); - var target = this.graph.getCell(this.get('target').id); - var prevParent = this.graph.getCell(this.get('parent')); + if (!connectionPointDef) return anchor; + var connectionPointFn; + if (typeof connectionPointDef === 'function') { + connectionPointFn = connectionPointDef; + } else { + var connectionPointName = connectionPointDef.name; + connectionPointFn = joint.connectionPoints[connectionPointName]; + if (typeof connectionPointFn !== 'function') throw new Error('Unknown connection point: ' + connectionPointName); + } + connectionPoint = connectionPointFn.call(this, line, view, magnet, connectionPointDef.args || {}, endType, this); + if (connectionPoint) return connectionPoint.round(this.decimalsRounding); + return anchor; + }, - if (source && target) { - newParent = this.graph.getCommonAncestor(source, target); - } + _translateConnectionPoints: function(tx, ty) { - if (prevParent && (!newParent || newParent.id !== prevParent.id)) { - // Unembed the link if source and target has no common ancestor - // or common ancestor changed - prevParent.unembed(this, opt); - } + var cache = this._markerCache; - if (newParent) { - newParent.embed(this, opt); - } - } + cache.sourcePoint.offset(tx, ty); + cache.targetPoint.offset(tx, ty); + this.sourcePoint.offset(tx, ty); + this.targetPoint.offset(tx, ty); + this.sourceAnchor.offset(tx, ty); + this.targetAnchor.offset(tx, ty); + }, - return newParent; + // if label position is a number, normalize it to a position object + // this makes sure that label positions can be merged properly + _normalizeLabelPosition: function(labelPosition) { + + if (typeof labelPosition === 'number') return { distance: labelPosition, offset: null, args: null }; + return labelPosition; }, - hasLoop: function(opt) { + updateLabelPositions: function() { - opt = opt || {}; + if (!this._V.labels) return this; - var sourceId = this.get('source').id; - var targetId = this.get('target').id; + var path = this.path; + if (!path) return this; - if (!sourceId || !targetId) { - // Link "pinned" to the paper does not have a loop. - return false; - } + // This method assumes all the label nodes are stored in the `this._labelCache` hash table + // by their indices in the `this.get('labels')` array. This is done in the `renderLabels()` method. - var loop = sourceId === targetId; + var model = this.model; + var labels = model.get('labels') || []; + if (!labels.length) return this; - // Note that there in the deep mode a link can have a loop, - // even if it connects only a parent and its embed. - // A loop "target equals source" is valid in both shallow and deep mode. - if (!loop && opt.deep && this.graph) { + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelPosition = builtinDefaultLabel.position; - var sourceElement = this.graph.getCell(sourceId); - var targetElement = this.graph.getCell(targetId); + var defaultLabel = model._getDefaultLabel(); + var defaultLabelPosition = this._normalizeLabelPosition(defaultLabel.position); - loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); - } + var defaultPosition = joint.util.merge({}, builtinDefaultLabelPosition, defaultLabelPosition); - return loop; - }, + for (var idx = 0, n = labels.length; idx < n; idx++) { - getSourceElement: function() { + var label = labels[idx]; + var labelPosition = this._normalizeLabelPosition(label.position); + + var position = joint.util.merge({}, defaultPosition, labelPosition); - var source = this.get('source'); + var labelPoint = this.getLabelCoordinates(position); + this._labelCache[idx].attr('transform', 'translate(' + labelPoint.x + ', ' + labelPoint.y + ')'); + } - return (source && source.id && this.graph && this.graph.getCell(source.id)) || null; + return this; }, - getTargetElement: function() { + updateToolsPosition: function() { - var target = this.get('target'); + if (!this._V.linkTools) return this; - return (target && target.id && this.graph && this.graph.getCell(target.id)) || null; - }, + // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. + // Note that the offset is hardcoded here. The offset should be always + // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking + // this up all the time would be slow. - // Returns the common ancestor for the source element, - // target element and the link itself. - getRelationshipAncestor: function() { + var scale = ''; + var offset = this.options.linkToolsOffset; + var connectionLength = this.getConnectionLength(); - var connectionAncestor; + // Firefox returns connectionLength=NaN in odd cases (for bezier curves). + // In that case we won't update tools position at all. + if (!Number.isNaN(connectionLength)) { - if (this.graph) { + // If the link is too short, make the tools half the size and the offset twice as low. + if (connectionLength < this.options.shortLinkLength) { + scale = 'scale(.5)'; + offset /= 2; + } - var cells = [ - this, - this.getSourceElement(), // null if source is a point - this.getTargetElement() // null if target is a point - ].filter(function(item) { - return !!item; - }); + var toolPosition = this.getPointAtLength(offset); - connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); - } + this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); - return connectionAncestor || null; - }, + if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { - // Is source, target and the link itself embedded in a given cell? - isRelationshipEmbeddedIn: function(cell) { + var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; - var cellId = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : cell.id; - var ancestor = this.getRelationshipAncestor(); + toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); + this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + this._tool2Cache.attr('visibility', 'visible'); - return !!ancestor && (ancestor.id === cellId || ancestor.isEmbeddedIn(cellId)); - } -}, - { - endsEqual: function(a, b) { + } else if (this.options.doubleLinkTools) { - var portsEqual = a.port === b.port || !a.port && !b.port; - return a.id === b.id && portsEqual; + this._tool2Cache.attr('visibility', 'hidden'); + } } - }); + return this; + }, -// joint.dia.Link base view and controller. -// ---------------------------------------- + updateArrowheadMarkers: function() { -joint.dia.LinkView = joint.dia.CellView.extend({ + if (!this._V.markerArrowheads) return this; - className: function() { + // getting bbox of an element with `display="none"` in IE9 ends up with access violation + if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; - var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); + var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; + this._V.sourceArrowhead.scale(sx); + this._V.targetArrowhead.scale(sx); - classNames.push('link'); + this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); - return classNames.join(' '); + return this; }, - options: { + // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, + // it stops listening on the previous one and starts listening to the new one. + createWatcher: function(endType) { - shortLinkLength: 100, - doubleLinkTools: false, - longLinkLength: 160, - linkToolsOffset: 40, - doubleLinkToolsOffset: 60, - sampleInterval: 50 - }, + // create handler for specific end type (source|target). + var onModelChange = function(endModel, opt) { + this.onEndModelChange(endType, endModel, opt); + }; - _z: null, + function watchEndModel(link, end) { - initialize: function(options) { + end = end || {}; - joint.dia.CellView.prototype.initialize.apply(this, arguments); + var endModel = null; + var previousEnd = link.previous(endType) || {}; - // create methods in prototype, so they can be accessed from any instance and - // don't need to be create over and over - if (typeof this.constructor.prototype.watchSource !== 'function') { - this.constructor.prototype.watchSource = this.createWatcher('source'); - this.constructor.prototype.watchTarget = this.createWatcher('target'); - } + if (previousEnd.id) { + this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); + } - // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to - // `` nodes wrapped by Vectorizer. This allows for quick access to the - // nodes in `updateLabelPosition()` in order to update the label positions. - this._labelCache = {}; + if (end.id) { + // If the observed model changes, it caches a new bbox and do the link update. + endModel = this.paper.getModelById(end.id); + this.listenTo(endModel, 'change', onModelChange); + } - // keeps markers bboxes and positions again for quicker access - this._markerCache = {}; + onModelChange.call(this, endModel, { cacheOnly: true }); - // bind events - this.startListening(); + return this; + } + + return watchEndModel; }, - startListening: function() { + onEndModelChange: function(endType, endModel, opt) { + var doUpdate = !opt.cacheOnly; var model = this.model; + var end = model.get(endType) || {}; - this.listenTo(model, 'change:markup', this.render); - this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); - this.listenTo(model, 'change:toolMarkup', this.onToolsChange); - this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); - this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); - this.listenTo(model, 'change:source', this.onSourceChange); - this.listenTo(model, 'change:target', this.onTargetChange); - }, + if (endModel) { - onSourceChange: function(cell, source, opt) { + var selector = this.constructor.makeSelector(end); + var oppositeEndType = endType == 'source' ? 'target' : 'source'; + var oppositeEnd = model.get(oppositeEndType) || {}; + var endId = end.id; + var oppositeEndId = oppositeEnd.id; + var oppositeSelector = oppositeEndId && this.constructor.makeSelector(oppositeEnd); - // Start watching the new source model. - this.watchSource(cell, source); - // This handler is called when the source attribute is changed. - // This can happen either when someone reconnects the link (or moves arrowhead), - // or when an embedded link is translated by its ancestor. - // 1. Always do update. - // 2. Do update only if the opposite end ('target') is also a point. - if (!opt.translateBy || !this.model.get('target').id) { - opt.updateConnectionOnly = true; - this.update(this.model, null, opt); - } - }, + // Caching end models bounding boxes. + // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached + // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed + // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). + if (opt.handleBy === this.cid && (endId === oppositeEndId) && selector == oppositeSelector) { - onTargetChange: function(cell, target, opt) { + // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the + // second time now. There is no need to calculate bbox and find magnet element again. + // It was calculated already for opposite link end. + this[endType + 'View'] = this[oppositeEndType + 'View']; + this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; - // Start watching the new target model. - this.watchTarget(cell, target); - // See `onSourceChange` method. - if (!opt.translateBy) { - opt.updateConnectionOnly = true; - this.update(this.model, null, opt); - } - }, + } else if (opt.translateBy) { + // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. + // If `opt.translateBy` is an ID of the element that was originally translated. - onVerticesChange: function(cell, changed, opt) { + // Noop - this.renderVertexMarkers(); + } else { + // The slowest path, source/target could have been rotated or resized or any attribute + // that affects the bounding box of the view might have been changed. - // If the vertices have been changed by a translation we do update only if the link was - // the only link that was translated. If the link was translated via another element which the link - // is embedded in, this element will be translated as well and that triggers an update. - // Note that all embeds in a model are sorted - first comes links, then elements. - if (!opt.translateBy || opt.translateBy === this.model.id) { - // Vertices were changed (not as a reaction on translate) - // or link.translate() was called or - opt.updateConnectionOnly = true; - this.update(cell, null, opt); - } - }, + var connectedModel = this.paper.model.getCell(endId); + if (!connectedModel) throw new Error('LinkView: invalid ' + endType + ' cell.'); + var connectedView = connectedModel.findView(this.paper); + if (connectedView) { + var connectedMagnet = connectedView.getMagnetFromLinkEnd(end); + if (connectedMagnet === connectedView.el) connectedMagnet = null; + this[endType + 'View'] = connectedView; + this[endType + 'Magnet'] = connectedMagnet; + } else { + // the view is not rendered yet + this[endType + 'View'] = this[endType + 'Magnet'] = null; + } + } - onToolsChange: function() { + if (opt.handleBy === this.cid && opt.translateBy && + model.isEmbeddedIn(endModel) && + !joint.util.isEmpty(model.get('vertices'))) { + // Loop link whose element was translated and that has vertices (that need to be translated with + // the parent in which my element is embedded). + // If the link is embedded, has a loop and vertices and the end model + // has been translated, do not update yet. There are vertices still to be updated (change:vertices + // event will come in the next turn). + doUpdate = false; + } - this.renderTools().updateToolsPosition(); - }, + if (!this.updatePostponed && oppositeEndId) { + // The update was not postponed (that can happen e.g. on the first change event) and the opposite + // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if + // we're reacting on change:source event, the oppositeEnd is the target model). - onLabelsChange: function(link, labels, opt) { + var oppositeEndModel = this.paper.getModelById(oppositeEndId); - var requireRender = true; + // Passing `handleBy` flag via event option. + // Note that if we are listening to the same model for event 'change' twice. + // The same event will be handled by this method also twice. + if (end.id === oppositeEnd.id) { + // We're dealing with a loop link. Tell the handlers in the next turn that they should update + // the link instead of me. (We know for sure there will be a next turn because + // loop links react on at least two events: change on the source model followed by a change on + // the target model). + opt.handleBy = this.cid; + } - var previousLabels = this.model.previous('labels'); + if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { - if (previousLabels) { - // Here is an optimalization for cases when we know, that change does - // not require rerendering of all labels. - if (('propertyPathArray' in opt) && ('propertyValue' in opt)) { - // The label is setting by `prop()` method - var pathArray = opt.propertyPathArray || []; - var pathLength = pathArray.length; - if (pathLength > 1) { - // We are changing a single label here e.g. 'labels/0/position' - var labelExists = !!previousLabels[pathArray[1]]; - if (labelExists) { - if (pathLength === 2) { - // We are changing the entire label. Need to check if the - // markup is also being changed. - requireRender = ('markup' in Object(opt.propertyValue)); - } else if (pathArray[2] !== 'markup') { - // We are changing a label property but not the markup - requireRender = false; - } - } + // Here are two options: + // - Source and target are connected to the same model (not necessarily the same port). + // - Both end models are translated by the same ancestor. We know that opposite end + // model will be translated in the next turn as well. + // In both situations there will be more changes on the model that trigger an + // update. So there is no need to update the linkView yet. + this.updatePostponed = true; + doUpdate = false; } } - } - if (requireRender) { - this.renderLabels(); } else { - this.updateLabels(); + + // the link end is a point ~ rect 1x1 + this[endType + 'View'] = this[endType + 'Magnet'] = null; } - this.updateLabelPositions(); + if (doUpdate) { + this.update(model, null, opt); + } }, - // Rendering - //---------- + _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { - render: function() { + // Make the markers "point" to their sticky points being auto-oriented towards + // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. + var route = joint.util.toArray(this.route); + if (sourceArrow) { + sourceArrow.translateAndAutoOrient( + this.sourcePoint, + route[0] || this.targetPoint, + this.paper.viewport + ); + } - this.$el.empty(); + if (targetArrow) { + targetArrow.translateAndAutoOrient( + this.targetPoint, + route[route.length - 1] || this.sourcePoint, + this.paper.viewport + ); + } + }, - // A special markup can be given in the `properties.markup` property. This might be handy - // if e.g. arrowhead markers should be `` elements or any other element than ``s. - // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors - // of elements with special meaning though. Therefore, those classes should be preserved in any - // special markup passed in `properties.markup`. - var model = this.model; - var markup = model.get('markup') || model.markup; - var children = V(markup); + _getDefaultLabelPositionArgs: function() { - // custom markup may contain only one children - if (!Array.isArray(children)) children = [children]; + var defaultLabel = this.model._getDefaultLabel(); + var defaultLabelPosition = defaultLabel.position || {}; + return defaultLabelPosition.args; + }, - // Cache all children elements for quicker access. - this._V = {}; // vectorized markup; - children.forEach(function(child) { + _getLabelPositionArgs: function(idx) { - var className = child.attr('class'); + var labelPosition = this.model.label(idx).position || {}; + return labelPosition.args; + }, - if (className) { - // Strip the joint class name prefix, if there is one. - className = joint.util.removeClassNamePrefix(className); - this._V[$.camelCase(className)] = child; - } + // merge default label position args into label position args + // keep `undefined` or `null` because `{}` means something else + _mergeLabelPositionArgs: function(labelPositionArgs, defaultLabelPositionArgs) { - }, this); + if (labelPositionArgs === null) return null; + if (labelPositionArgs === undefined) { - // Only the connection path is mandatory - if (!this._V.connection) throw new Error('link: no connection path in the markup'); + if (defaultLabelPositionArgs === null) return null; + return defaultLabelPositionArgs; + } - // partial rendering - this.renderTools(); - this.renderVertexMarkers(); - this.renderArrowheadMarkers(); + return joint.util.merge({}, defaultLabelPositionArgs, labelPositionArgs); + }, - this.vel.append(children); + // Add default label at given position at end of `labels` array. + // Assigns relative coordinates by default. + // `opt.absoluteDistance` forces absolute coordinates. + // `opt.reverseDistance` forces reverse absolute coordinates (if absoluteDistance = true). + // `opt.absoluteOffset` forces absolute coordinates for offset. + addLabel: function(x, y, opt) { - // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox - // returns zero values) - this.renderLabels(); + // accept input in form `{ x, y }, opt` or `x, y, opt` + var isPointProvided = (typeof x !== 'number'); + var localX = isPointProvided ? x.x : x; + var localY = isPointProvided ? x.y : y; + var localOpt = isPointProvided ? y : opt; - // start watching the ends of the link for changes - this.watchSource(model, model.get('source')) - .watchTarget(model, model.get('target')) - .update(); + var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs(); + var labelPositionArgs = localOpt; + var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); - return this; + var label = { position: this.getLabelPosition(localX, localY, positionArgs) }; + var idx = -1; + this.model.insertLabel(idx, label, localOpt); + return idx; }, - renderLabels: function() { + // Add a new vertex at calculated index to the `vertices` array. + addVertex: function(x, y, opt) { - var vLabels = this._V.labels; - if (!vLabels) { - return this; + // accept input in form `{ x, y }, opt` or `x, y, opt` + var isPointProvided = (typeof x !== 'number'); + var localX = isPointProvided ? x.x : x; + var localY = isPointProvided ? x.y : y; + var localOpt = isPointProvided ? y : opt; + + var vertex = { x: localX, y: localY }; + var idx = this.getVertexIndex(localX, localY); + this.model.insertVertex(idx, vertex, localOpt); + return idx; + }, + + // Send a token (an SVG element, usually a circle) along the connection path. + // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)` + // `opt.duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. + // `opt.directon` is optional and it determines whether the token goes from source to target or other way round (`reverse`) + // `opt.connection` is an optional selector to the connection path. + // `callback` is optional and is a function to be called once the token reaches the target. + sendToken: function(token, opt, callback) { + + function onAnimationEnd(vToken, callback) { + return function() { + vToken.remove(); + if (typeof callback === 'function') { + callback(); + } + }; } - vLabels.empty(); - - var model = this.model; - var labels = model.get('labels') || []; - var labelCache = this._labelCache = {}; - var labelsCount = labels.length; - if (labelsCount === 0) { - return this; + var duration, isReversed, selector; + if (joint.util.isObject(opt)) { + duration = opt.duration; + isReversed = (opt.direction === 'reverse'); + selector = opt.connection; + } else { + // Backwards compatibility + duration = opt; + isReversed = false; + selector = null; } - var labelTemplate = joint.util.template(model.get('labelMarkup') || model.labelMarkup); - // This is a prepared instance of a vectorized SVGDOM node for the label element resulting from - // compilation of the labelTemplate. The purpose is that all labels will just `clone()` this - // node to create a duplicate. - var labelNodeInstance = V(labelTemplate()); - - for (var i = 0; i < labelsCount; i++) { + duration = duration || 1000; - var label = labels[i]; - var labelMarkup = label.markup; - // Cache label nodes so that the `updateLabels()` can just update the label node positions. - var vLabelNode = labelCache[i] = (labelMarkup) - ? V('g').append(V(labelMarkup)) - : labelNodeInstance.clone(); + var animationAttributes = { + dur: duration + 'ms', + repeatCount: 1, + calcMode: 'linear', + fill: 'freeze' + }; - vLabelNode - .addClass('label') - .attr('label-idx', i) - .appendTo(vLabels); + if (isReversed) { + animationAttributes.keyPoints = '1;0'; + animationAttributes.keyTimes = '0;1'; } - this.updateLabels(); - - return this; - }, - - updateLabels: function() { + var vToken = V(token); + var connection; + if (typeof selector === 'string') { + // Use custom connection path. + connection = this.findBySelector(selector, this.el, this.selectors)[0]; + } else { + // Select connection path automatically. + var cache = this._V; + connection = (cache.connection) ? cache.connection.node : this.el.querySelector('path'); + } - if (!this._V.labels) { - return this; + if (!(connection instanceof SVGPathElement)) { + throw new Error('dia.LinkView: token animation requires a valid connection path.'); } - var labels = this.model.get('labels') || []; - var canLabelMove = this.can('labelMove'); + vToken + .appendTo(this.paper.viewport) + .animateAlongPath(animationAttributes, connection); - for (var i = 0, n = labels.length; i < n; i++) { + setTimeout(onAnimationEnd(vToken, callback), duration); + }, - var vLabel = this._labelCache[i]; - var label = labels[i]; + findRoute: function(vertices) { - vLabel.attr('cursor', (canLabelMove ? 'move' : 'default')); + vertices || (vertices = []); - var labelAttrs = label.attrs; - if (!label.markup) { - // Default attributes to maintain backwards compatibility - labelAttrs = joint.util.merge({ - text: { - textAnchor: 'middle', - fontSize: 14, - fill: '#000000', - pointerEvents: 'none', - yAlignment: 'middle' - }, - rect: { - ref: 'text', - fill: '#ffffff', - rx: 3, - ry: 3, - refWidth: 1, - refHeight: 1, - refX: 0, - refY: 0 - } - }, labelAttrs); - } + var namespace = joint.routers; + var router = this.model.router(); + var defaultRouter = this.paper.options.defaultRouter; - this.updateDOMSubtreeAttributes(vLabel.node, labelAttrs, { - rootBBox: g.Rect(label.size) - }); + if (!router) { + if (defaultRouter) router = defaultRouter; + else return vertices.map(g.Point, g); // no router specified } - return this; - }, + var routerFn = joint.util.isFunction(router) ? router : namespace[router.name]; + if (!joint.util.isFunction(routerFn)) { + throw new Error('dia.LinkView: unknown router: "' + router.name + '".'); + } - renderTools: function() { + var args = router.args || {}; - if (!this._V.linkTools) return this; + var route = routerFn.call( + this, // context + vertices, // vertices + args, // options + this // linkView + ); - // Tools are a group of clickable elements that manipulate the whole link. - // A good example of this is the remove tool that removes the whole link. - // Tools appear after hovering the link close to the `source` element/point of the link - // but are offset a bit so that they don't cover the `marker-arrowhead`. + if (!route) return vertices.map(g.Point, g); + return route; + }, - var $tools = $(this._V.linkTools.node).empty(); - var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); - var tool = V(toolTemplate()); + // Return the `d` attribute value of the `` element representing the link + // between `source` and `target`. + findPath: function(route, sourcePoint, targetPoint) { - $tools.append(tool.node); + var namespace = joint.connectors; + var connector = this.model.connector(); + var defaultConnector = this.paper.options.defaultConnector; - // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. - this._toolCache = tool; + if (!connector) { + connector = defaultConnector || {}; + } - // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the - // link as well but only if the link is longer than `longLinkLength`. - if (this.options.doubleLinkTools) { + var connectorFn = joint.util.isFunction(connector) ? connector : namespace[connector.name]; + if (!joint.util.isFunction(connectorFn)) { + throw new Error('dia.LinkView: unknown connector: "' + connector.name + '".'); + } - var tool2; - if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { - toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); - tool2 = V(toolTemplate()); - } else { - tool2 = tool.clone(); - } + var args = joint.util.clone(connector.args || {}); + args.raw = true; // Request raw g.Path as the result. - $tools.append(tool2.node); - this._tool2Cache = tool2; + var path = connectorFn.call( + this, // context + sourcePoint, // start point + targetPoint, // end point + route, // vertices + args, // options + this // linkView + ); + + if (typeof path === 'string') { + // Backwards compatibility for connectors not supporting `raw` option. + path = new g.Path(V.normalizePathData(path)); } - return this; + return path; }, - renderVertexMarkers: function() { + // Public API. + // ----------- - if (!this._V.markerVertices) return this; + getConnection: function() { - var $markerVertices = $(this._V.markerVertices.node).empty(); + var path = this.path; + if (!path) return null; - // A special markup can be given in the `properties.vertexMarkup` property. This might be handy - // if default styling (elements) are not desired. This makes it possible to use any - // SVG elements for .marker-vertex and .marker-vertex-remove tools. - var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); + return path.clone(); + }, - joint.util.toArray(this.model.get('vertices')).forEach(function(vertex, idx) { + getSerializedConnection: function() { - $markerVertices.append(V(markupTemplate(joint.util.assign({ idx: idx }, vertex))).node); - }); + var path = this.path; + if (!path) return null; - return this; + var metrics = this.metrics; + if (metrics.hasOwnProperty('data')) return metrics.data; + var data = path.serialize(); + metrics.data = data; + return data; }, - renderArrowheadMarkers: function() { + getConnectionSubdivisions: function() { - // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. - if (!this._V.markerArrowheads) return this; + var path = this.path; + if (!path) return null; - var $markerArrowheads = $(this._V.markerArrowheads.node); + var metrics = this.metrics; + if (metrics.hasOwnProperty('segmentSubdivisions')) return metrics.segmentSubdivisions; + var subdivisions = path.getSegmentSubdivisions(); + metrics.segmentSubdivisions = subdivisions; + return subdivisions; + }, - $markerArrowheads.empty(); + getConnectionLength: function() { - // A special markup can be given in the `properties.vertexMarkup` property. This might be handy - // if default styling (elements) are not desired. This makes it possible to use any - // SVG elements for .marker-vertex and .marker-vertex-remove tools. - var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); + var path = this.path; + if (!path) return 0; - this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); - this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); + var metrics = this.metrics; + if (metrics.hasOwnProperty('length')) return metrics.length; + var length = path.length({ segmentSubdivisions: this.getConnectionSubdivisions() }); + metrics.length = length; + return length; + }, - $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); + getPointAtLength: function(length) { - return this; - }, + var path = this.path; + if (!path) return null; - // Updating - //--------- + return path.pointAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - // Default is to process the `attrs` object and set attributes on subelements based on the selectors. - update: function(model, attributes, opt) { + getPointAtRatio: function(ratio) { - opt = opt || {}; + var path = this.path; + if (!path) return null; - if (!opt.updateConnectionOnly) { - // update SVG attributes defined by 'attrs/'. - this.updateDOMSubtreeAttributes(this.el, this.model.attr()); - } + return path.pointAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - // update the link path, label position etc. - this.updateConnection(opt); - this.updateLabelPositions(); - this.updateToolsPosition(); - this.updateArrowheadMarkers(); + getTangentAtLength: function(length) { - // Local perpendicular flag (as opposed to one defined on paper). - // Could be enabled inside a connector/router. It's valid only - // during the update execution. - this.options.perpendicular = null; - // Mark that postponed update has been already executed. - this.updatePostponed = false; + var path = this.path; + if (!path) return null; - return this; + return path.tangentAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() }); }, - updateConnection: function(opt) { + getTangentAtRatio: function(ratio) { - opt = opt || {}; + var path = this.path; + if (!path) return null; - var model = this.model; - var route; + return path.tangentAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { - // The link is being translated by an ancestor that will - // shift source point, target point and all vertices - // by an equal distance. - var tx = opt.tx || 0; - var ty = opt.ty || 0; + getClosestPoint: function(point) { - route = this.route = joint.util.toArray(this.route).map(function(point) { - // translate point by point by delta translation - return g.point(point).offset(tx, ty); - }); + var path = this.path; + if (!path) return null; - // translate source and target connection and marker points. - this._translateConnectionPoints(tx, ty); + return path.closestPoint(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - } else { - // Necessary path finding - route = this.route = this.findRoute(model.get('vertices') || [], opt); - // finds all the connection points taking new vertices into account - this._findConnectionPoints(route); - } + getClosestPointLength: function(point) { + + var path = this.path; + if (!path) return null; - var pathData = this.getPathData(route); + return path.closestPointLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getClosestPointRatio: function(point) { - // The markup needs to contain a `.connection` - this._V.connection.attr('d', pathData); - this._V.connectionWrap && this._V.connectionWrap.attr('d', pathData); + var path = this.path; + if (!path) return null; - this._translateAndAutoOrientArrows(this._V.markerSource, this._V.markerTarget); + return path.closestPointNormalizedLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); }, - _findConnectionPoints: function(vertices) { + // accepts options `absoluteDistance: boolean`, `reverseDistance: boolean`, `absoluteOffset: boolean` + // to move beyond connection endpoints, absoluteOffset has to be set + getLabelPosition: function(x, y, opt) { - // cache source and target points - var sourcePoint, targetPoint, sourceMarkerPoint, targetMarkerPoint; - var verticesArr = joint.util.toArray(vertices); + var position = {}; - var firstVertex = verticesArr[0]; + var localOpt = opt || {}; + if (opt) position.args = opt; - sourcePoint = this.getConnectionPoint( - 'source', this.model.get('source'), firstVertex || this.model.get('target') - ).round(); + var isDistanceRelative = !localOpt.absoluteDistance; // relative by default + var isDistanceAbsoluteReverse = (localOpt.absoluteDistance && localOpt.reverseDistance); // non-reverse by default + var isOffsetAbsolute = localOpt.absoluteOffset; // offset is non-absolute by default - var lastVertex = verticesArr[verticesArr.length - 1]; + var path = this.path; + var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }; - targetPoint = this.getConnectionPoint( - 'target', this.model.get('target'), lastVertex || sourcePoint - ).round(); + var labelPoint = new g.Point(x, y); + var t = path.closestPointT(labelPoint, pathOpt); - // Move the source point by the width of the marker taking into account - // its scale around x-axis. Note that scale is the only transform that - // makes sense to be set in `.marker-source` attributes object - // as all other transforms (translate/rotate) will be replaced - // by the `translateAndAutoOrient()` function. - var cache = this._markerCache; + // GET DISTANCE: - if (this._V.markerSource) { + var labelDistance = path.lengthAtT(t, pathOpt); + if (isDistanceRelative) labelDistance = (labelDistance / this.getConnectionLength()) || 0; // fix to prevent NaN for 0 length + if (isDistanceAbsoluteReverse) labelDistance = (-1 * (this.getConnectionLength() - labelDistance)) || 1; // fix for end point (-0 => 1) - cache.sourceBBox = cache.sourceBBox || this._V.markerSource.getBBox(); + position.distance = labelDistance; - sourceMarkerPoint = g.point(sourcePoint).move( - firstVertex || targetPoint, - cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 - ).round(); - } + // GET OFFSET: + // use absolute offset if: + // - opt.absoluteOffset is true, + // - opt.absoluteOffset is not true but there is no tangent - if (this._V.markerTarget) { + var tangent; + if (!isOffsetAbsolute) tangent = path.tangentAtT(t); - cache.targetBBox = cache.targetBBox || this._V.markerTarget.getBBox(); + var labelOffset; + if (tangent) { + labelOffset = tangent.pointOffset(labelPoint); - targetMarkerPoint = g.point(targetPoint).move( - lastVertex || sourcePoint, - cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 - ).round(); + } else { + var closestPoint = path.pointAtT(t); + var labelOffsetDiff = labelPoint.difference(closestPoint); + labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y }; } - // if there was no markup for the marker, use the connection point. - cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); - cache.targetPoint = targetMarkerPoint || targetPoint.clone(); + position.offset = labelOffset; - // make connection points public - this.sourcePoint = sourcePoint; - this.targetPoint = targetPoint; + return position; }, - _translateConnectionPoints: function(tx, ty) { - - var cache = this._markerCache; + getLabelCoordinates: function(labelPosition) { - cache.sourcePoint.offset(tx, ty); - cache.targetPoint.offset(tx, ty); - this.sourcePoint.offset(tx, ty); - this.targetPoint.offset(tx, ty); - }, + var labelDistance; + if (typeof labelPosition === 'number') labelDistance = labelPosition; + else if (typeof labelPosition.distance === 'number') labelDistance = labelPosition.distance; + else throw new Error('dia.LinkView: invalid label position distance.'); - updateLabelPositions: function() { + var isDistanceRelative = ((labelDistance > 0) && (labelDistance <= 1)); - if (!this._V.labels) return this; + var labelOffset = 0; + var labelOffsetCoordinates = { x: 0, y: 0 }; + if (labelPosition.offset) { + var positionOffset = labelPosition.offset; + if (typeof positionOffset === 'number') labelOffset = positionOffset; + if (positionOffset.x) labelOffsetCoordinates.x = positionOffset.x; + if (positionOffset.y) labelOffsetCoordinates.y = positionOffset.y; + } - // This method assumes all the label nodes are stored in the `this._labelCache` hash table - // by their indexes in the `this.get('labels')` array. This is done in the `renderLabels()` method. + var isOffsetAbsolute = ((labelOffsetCoordinates.x !== 0) || (labelOffsetCoordinates.y !== 0) || labelOffset === 0); - var labels = this.model.get('labels') || []; - if (!labels.length) return this; + var path = this.path; + var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }; - var samples; - var connectionElement = this._V.connection.node; - var connectionLength = connectionElement.getTotalLength(); + var distance = isDistanceRelative ? (labelDistance * this.getConnectionLength()) : labelDistance; - // Firefox returns connectionLength=NaN in odd cases (for bezier curves). - // In that case we won't update labels at all. - if (Number.isNaN(connectionLength)) { - return this; - } + var point; - for (var idx = 0, n = labels.length; idx < n; idx++) { + if (isOffsetAbsolute) { + point = path.pointAtLength(distance, pathOpt); + point.offset(labelOffsetCoordinates); - var label = labels[idx]; - var position = label.position; - var isPositionObject = joint.util.isObject(position); - var labelCoordinates; + } else { + var tangent = path.tangentAtLength(distance, pathOpt); - var distance = isPositionObject ? position.distance : position; - var offset = isPositionObject ? position.offset : { x: 0, y: 0 }; + if (tangent) { + tangent.rotate(tangent.start, -90); + tangent.setLength(labelOffset); + point = tangent.end; - if (Number.isFinite(distance)) { - distance = (distance > connectionLength) ? connectionLength : distance; // sanity check - distance = (distance < 0) ? connectionLength + distance : distance; - distance = (distance > 1) ? distance : connectionLength * distance; } else { - distance = connectionLength / 2; + // fallback - the connection has zero length + point = path.start; } + } - labelCoordinates = connectionElement.getPointAtLength(distance); - - if (joint.util.isObject(offset)) { - - // Just offset the label by the x,y provided in the offset object. - labelCoordinates = g.point(labelCoordinates).offset(offset); + return point; + }, - } else if (Number.isFinite(offset)) { + getVertexIndex: function(x, y) { - if (!samples) { - samples = this._samples || this._V.connection.sample(this.options.sampleInterval); - } + var model = this.model; + var vertices = model.vertices(); - // Offset the label by the amount provided in `offset` to an either - // side of the link. - - // 1. Find the closest sample & its left and right neighbours. - var minSqDistance = Infinity; - var closestSampleIndex, sample, sqDistance; - for (var i = 0, m = samples.length; i < m; i++) { - sample = samples[i]; - sqDistance = g.line(sample, labelCoordinates).squaredLength(); - if (sqDistance < minSqDistance) { - minSqDistance = sqDistance; - closestSampleIndex = i; - } - } - var prevSample = samples[closestSampleIndex - 1]; - var nextSample = samples[closestSampleIndex + 1]; - - // 2. Offset the label on the perpendicular line between - // the current label coordinate ("at `distance`") and - // the next sample. - var angle = 0; - if (nextSample) { - angle = g.point(labelCoordinates).theta(nextSample); - } else if (prevSample) { - angle = g.point(prevSample).theta(labelCoordinates); - } - labelCoordinates = g.point(labelCoordinates).offset(offset).rotate(labelCoordinates, angle - 90); - } + var vertexLength = this.getClosestPointLength(new g.Point(x, y)); - this._labelCache[idx].attr('transform', 'translate(' + labelCoordinates.x + ', ' + labelCoordinates.y + ')'); + var idx = 0; + for (var n = vertices.length; idx < n; idx++) { + var currentVertex = vertices[idx]; + var currentVertexLength = this.getClosestPointLength(currentVertex); + if (vertexLength < currentVertexLength) break; } - return this; + return idx; }, + // Interaction. The controller part. + // --------------------------------- + + pointerdblclick: function(evt, x, y) { - updateToolsPosition: function() { + joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments); + this.notify('link:pointerdblclick', evt, x, y); + }, - if (!this._V.linkTools) return this; + pointerclick: function(evt, x, y) { - // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. - // Note that the offset is hardcoded here. The offset should be always - // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking - // this up all the time would be slow. + joint.dia.CellView.prototype.pointerclick.apply(this, arguments); + this.notify('link:pointerclick', evt, x, y); + }, - var scale = ''; - var offset = this.options.linkToolsOffset; - var connectionLength = this.getConnectionLength(); + contextmenu: function(evt, x, y) { - // Firefox returns connectionLength=NaN in odd cases (for bezier curves). - // In that case we won't update tools position at all. - if (!Number.isNaN(connectionLength)) { + joint.dia.CellView.prototype.contextmenu.apply(this, arguments); + this.notify('link:contextmenu', evt, x, y); + }, - // If the link is too short, make the tools half the size and the offset twice as low. - if (connectionLength < this.options.shortLinkLength) { - scale = 'scale(.5)'; - offset /= 2; - } + pointerdown: function(evt, x, y) { - var toolPosition = this.getPointAtLength(offset); + joint.dia.CellView.prototype.pointerdown.apply(this, arguments); + this.notify('link:pointerdown', evt, x, y); - this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + // Backwards compatibility for the default markup + var className = evt.target.getAttribute('class'); + switch (className) { - if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { + case 'marker-vertex': + this.dragVertexStart(evt, x, y); + return; - var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; + case 'marker-vertex-remove': + case 'marker-vertex-remove-area': + this.dragVertexRemoveStart(evt, x, y); + return; - toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); - this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); - this._tool2Cache.attr('visibility', 'visible'); + case 'marker-arrowhead': + this.dragArrowheadStart(evt, x, y); + return; - } else if (this.options.doubleLinkTools) { + case 'connection': + case 'connection-wrap': + this.dragConnectionStart(evt, x, y); + return; - this._tool2Cache.attr('visibility', 'hidden'); - } + case 'marker-source': + case 'marker-target': + return; } - return this; + this.dragStart(evt, x, y); }, + pointermove: function(evt, x, y) { - updateArrowheadMarkers: function() { + // Backwards compatibility + var dragData = this._dragData; + if (dragData) this.eventData(evt, dragData); - if (!this._V.markerArrowheads) return this; + var data = this.eventData(evt); + switch (data.action) { - // getting bbox of an element with `display="none"` in IE9 ends up with access violation - if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; + case 'vertex-move': + this.dragVertex(evt, x, y); + break; - var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; - this._V.sourceArrowhead.scale(sx); - this._V.targetArrowhead.scale(sx); + case 'label-move': + this.dragLabel(evt, x, y); + break; - this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); + case 'arrowhead-move': + this.dragArrowhead(evt, x, y); + break; - return this; - }, + case 'move': + this.drag(evt, x, y); + break; + } - // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, - // it stops listening on the previous one and starts listening to the new one. - createWatcher: function(endType) { + // Backwards compatibility + if (dragData) joint.util.assign(dragData, this.eventData(evt)); - // create handler for specific end type (source|target). - var onModelChange = function(endModel, opt) { - this.onEndModelChange(endType, endModel, opt); - }; + joint.dia.CellView.prototype.pointermove.apply(this, arguments); + this.notify('link:pointermove', evt, x, y); + }, - function watchEndModel(link, end) { + pointerup: function(evt, x, y) { - end = end || {}; + // Backwards compatibility + var dragData = this._dragData; + if (dragData) { + this.eventData(evt, dragData); + this._dragData = null; + } - var endModel = null; - var previousEnd = link.previous(endType) || {}; + var data = this.eventData(evt); + switch (data.action) { - if (previousEnd.id) { - this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); - } + case 'vertex-move': + this.dragVertexEnd(evt, x, y); + break; - if (end.id) { - // If the observed model changes, it caches a new bbox and do the link update. - endModel = this.paper.getModelById(end.id); - this.listenTo(endModel, 'change', onModelChange); - } + case 'label-move': + this.dragLabelEnd(evt, x, y); + break; - onModelChange.call(this, endModel, { cacheOnly: true }); + case 'arrowhead-move': + this.dragArrowheadEnd(evt, x, y); + break; - return this; + case 'move': + this.dragEnd(evt, x, y); } - return watchEndModel; + this.notify('link:pointerup', evt, x, y); + joint.dia.CellView.prototype.pointerup.apply(this, arguments); }, - onEndModelChange: function(endType, endModel, opt) { + mouseover: function(evt) { - var doUpdate = !opt.cacheOnly; - var model = this.model; - var end = model.get(endType) || {}; + joint.dia.CellView.prototype.mouseover.apply(this, arguments); + this.notify('link:mouseover', evt); + }, - if (endModel) { + mouseout: function(evt) { - var selector = this.constructor.makeSelector(end); - var oppositeEndType = endType == 'source' ? 'target' : 'source'; - var oppositeEnd = model.get(oppositeEndType) || {}; - var oppositeSelector = oppositeEnd.id && this.constructor.makeSelector(oppositeEnd); + joint.dia.CellView.prototype.mouseout.apply(this, arguments); + this.notify('link:mouseout', evt); + }, - // Caching end models bounding boxes. - // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached - // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed - // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). - if (opt.handleBy === this.cid && selector == oppositeSelector) { + mouseenter: function(evt) { - // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the - // second time now. There is no need to calculate bbox and find magnet element again. - // It was calculated already for opposite link end. - this[endType + 'BBox'] = this[oppositeEndType + 'BBox']; - this[endType + 'View'] = this[oppositeEndType + 'View']; - this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; + joint.dia.CellView.prototype.mouseenter.apply(this, arguments); + this.notify('link:mouseenter', evt); + }, - } else if (opt.translateBy) { - // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. - // If `opt.translateBy` is an ID of the element that was originally translated. This allows us - // to just offset the cached bounding box by the translation instead of calculating the bounding - // box from scratch on every translate. + mouseleave: function(evt) { - var bbox = this[endType + 'BBox']; - bbox.x += opt.tx; - bbox.y += opt.ty; + joint.dia.CellView.prototype.mouseleave.apply(this, arguments); + this.notify('link:mouseleave', evt); + }, - } else { - // The slowest path, source/target could have been rotated or resized or any attribute - // that affects the bounding box of the view might have been changed. + mousewheel: function(evt, x, y, delta) { - var view = this.paper.findViewByModel(end.id); - var magnetElement = view.el.querySelector(selector); + joint.dia.CellView.prototype.mousewheel.apply(this, arguments); + this.notify('link:mousewheel', evt, x, y, delta); + }, - this[endType + 'BBox'] = view.getStrokeBBox(magnetElement); - this[endType + 'View'] = view; - this[endType + 'Magnet'] = magnetElement; - } + onevent: function(evt, eventName, x, y) { - if (opt.handleBy === this.cid && opt.translateBy && - model.isEmbeddedIn(endModel) && - !joint.util.isEmpty(model.get('vertices'))) { - // Loop link whose element was translated and that has vertices (that need to be translated with - // the parent in which my element is embedded). - // If the link is embedded, has a loop and vertices and the end model - // has been translated, do not update yet. There are vertices still to be updated (change:vertices - // event will come in the next turn). - doUpdate = false; + // Backwards compatibility + var linkTool = V(evt.target).findParentByClass('link-tool', this.el); + if (linkTool) { + // No further action to be executed + evt.stopPropagation(); + + // Allow `interactive.useLinkTools=false` + if (this.can('useLinkTools')) { + if (eventName === 'remove') { + // Built-in remove event + this.model.remove({ ui: true }); + + } else { + // link:options and other custom events inside the link tools + this.notify(eventName, evt, x, y); + } } - if (!this.updatePostponed && oppositeEnd.id) { - // The update was not postponed (that can happen e.g. on the first change event) and the opposite - // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if - // we're reacting on change:source event, the oppositeEnd is the target model). + } else { + joint.dia.CellView.prototype.onevent.apply(this, arguments); + } + }, - var oppositeEndModel = this.paper.getModelById(oppositeEnd.id); + onlabel: function(evt, x, y) { - // Passing `handleBy` flag via event option. - // Note that if we are listening to the same model for event 'change' twice. - // The same event will be handled by this method also twice. - if (end.id === oppositeEnd.id) { - // We're dealing with a loop link. Tell the handlers in the next turn that they should update - // the link instead of me. (We know for sure there will be a next turn because - // loop links react on at least two events: change on the source model followed by a change on - // the target model). - opt.handleBy = this.cid; - } + this.dragLabelStart(evt, x, y); - if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { + var stopPropagation = this.eventData(evt).stopPropagation; + if (stopPropagation) evt.stopPropagation(); + }, - // Here are two options: - // - Source and target are connected to the same model (not necessarily the same port). - // - Both end models are translated by the same ancestor. We know that opposite end - // model will be translated in the next turn as well. - // In both situations there will be more changes on the model that trigger an - // update. So there is no need to update the linkView yet. - this.updatePostponed = true; - doUpdate = false; - } - } + // Drag Start Handlers - } else { + dragConnectionStart: function(evt, x, y) { - // the link end is a point ~ rect 1x1 - this[endType + 'BBox'] = g.rect(end.x || 0, end.y || 0, 1, 1); - this[endType + 'View'] = this[endType + 'Magnet'] = null; - } + if (!this.can('vertexAdd')) return; - if (doUpdate) { - opt.updateConnectionOnly = true; - this.update(model, null, opt); - } + // Store the index at which the new vertex has just been placed. + // We'll be update the very same vertex position in `pointermove()`. + var vertexIdx = this.addVertex({ x: x, y: y }, { ui: true }); + this.eventData(evt, { + action: 'vertex-move', + vertexIdx: vertexIdx + }); }, - _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { + dragLabelStart: function(evt, x, y) { - // Make the markers "point" to their sticky points being auto-oriented towards - // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. - var route = joint.util.toArray(this.route); - if (sourceArrow) { - sourceArrow.translateAndAutoOrient( - this.sourcePoint, - route[0] || this.targetPoint, - this.paper.viewport - ); + if (!this.can('labelMove')) { + // Backwards compatibility: + // If labels can't be dragged no default action is triggered. + this.eventData(evt, { stopPropagation: true }); + return; } - if (targetArrow) { - targetArrow.translateAndAutoOrient( - this.targetPoint, - route[route.length - 1] || this.sourcePoint, - this.paper.viewport - ); - } - }, + var labelNode = evt.currentTarget; + var labelIdx = parseInt(labelNode.getAttribute('label-idx'), 10); - removeVertex: function(idx) { + var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs(); + var labelPositionArgs = this._getLabelPositionArgs(labelIdx); + var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); + + this.eventData(evt, { + action: 'label-move', + labelIdx: labelIdx, + positionArgs: positionArgs, + stopPropagation: true + }); - var vertices = joint.util.assign([], this.model.get('vertices')); + this.paper.delegateDragEvents(this, evt.data); + }, - if (vertices && vertices.length) { + dragVertexStart: function(evt, x, y) { - vertices.splice(idx, 1); - this.model.set('vertices', vertices, { ui: true }); - } + if (!this.can('vertexMove')) return; - return this; + var vertexNode = evt.target; + var vertexIdx = parseInt(vertexNode.getAttribute('idx'), 10); + this.eventData(evt, { + action: 'vertex-move', + vertexIdx: vertexIdx + }); }, - // This method ads a new vertex to the `vertices` array of `.connection`. This method - // uses a heuristic to find the index at which the new `vertex` should be placed at assuming - // the new vertex is somewhere on the path. - addVertex: function(vertex) { + dragVertexRemoveStart: function(evt, x, y) { - // As it is very hard to find a correct index of the newly created vertex, - // a little heuristics is taking place here. - // The heuristics checks if length of the newly created - // path is lot more than length of the old path. If this is the case, - // new vertex was probably put into a wrong index. - // Try to put it into another index and repeat the heuristics again. + if (!this.can('vertexRemove')) return; - var vertices = (this.model.get('vertices') || []).slice(); - // Store the original vertices for a later revert if needed. - var originalVertices = vertices.slice(); + var removeNode = evt.target; + var vertexIdx = parseInt(removeNode.getAttribute('idx'), 10); + this.model.removeVertex(vertexIdx); + }, - // A `` element used to compute the length of the path during heuristics. - var path = this._V.connection.node.cloneNode(false); + dragArrowheadStart: function(evt, x, y) { - // Length of the original path. - var originalPathLength = path.getTotalLength(); - // Current path length. - var pathLength; - // Tolerance determines the highest possible difference between the length - // of the old and new path. The number has been chosen heuristically. - var pathLengthTolerance = 20; - // Total number of vertices including source and target points. - var idx = vertices.length + 1; + if (!this.can('arrowheadMove')) return; - // Loop through all possible indexes and check if the difference between - // path lengths changes significantly. If not, the found index is - // most probably the right one. - while (idx--) { + var arrowheadNode = evt.target; + var arrowheadType = arrowheadNode.getAttribute('end'); + var data = this.startArrowheadMove(arrowheadType, { ignoreBackwardsCompatibility: true }); - vertices.splice(idx, 0, vertex); - V(path).attr('d', this.getPathData(this.findRoute(vertices))); + this.eventData(evt, data); + }, - pathLength = path.getTotalLength(); + dragStart: function(evt, x, y) { - // Check if the path lengths changed significantly. - if (pathLength - originalPathLength > pathLengthTolerance) { + if (!this.can('linkMove')) return; - // Revert vertices to the original array. The path length has changed too much - // so that the index was not found yet. - vertices = originalVertices.slice(); + this.eventData(evt, { + action: 'move', + dx: x, + dy: y + }); + }, - } else { + // Drag Handlers - break; - } - } + dragLabel: function(evt, x, y) { - if (idx === -1) { - // If no suitable index was found for such a vertex, make the vertex the first one. - idx = 0; - vertices.splice(idx, 0, vertex); - } + var data = this.eventData(evt); + var label = { position: this.getLabelPosition(x, y, data.positionArgs) }; + this.model.label(data.labelIdx, label); + }, - this.model.set('vertices', vertices, { ui: true }); + dragVertex: function(evt, x, y) { - return idx; + var data = this.eventData(evt); + this.model.vertex(data.vertexIdx, { x: x, y: y }, { ui: true }); }, - // Send a token (an SVG element, usually a circle) along the connection path. - // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)` - // `opt.duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. - // `opt.directon` is optional and it determines whether the token goes from source to target or other way round (`reverse`) - // `callback` is optional and is a function to be called once the token reaches the target. - sendToken: function(token, opt, callback) { + dragArrowhead: function(evt, x, y) { - function onAnimationEnd(vToken, callback) { - return function() { - vToken.remove(); - if (typeof callback === 'function') { - callback(); - } - }; - } + var data = this.eventData(evt); + + if (this.paper.options.snapLinks) { + + this._snapArrowhead(x, y, data); - var duration, isReversed; - if (joint.util.isObject(opt)) { - duration = opt.duration; - isReversed = (opt.direction === 'reverse'); } else { - // Backwards compatibility - duration = opt; - isReversed = false; + // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. + // It holds the element when a touchstart triggered. + var target = (evt.type === 'mousemove') + ? evt.target + : document.elementFromPoint(evt.clientX, evt.clientY); + + this._connectArrowhead(target, x, y, data); } + }, - duration = duration || 1000; + drag: function(evt, x, y) { - var animationAttributes = { - dur: duration + 'ms', - repeatCount: 1, - calcMode: 'linear', - fill: 'freeze' - }; + var data = this.eventData(evt); + this.model.translate(x - data.dx, y - data.dy, { ui: true }); + this.eventData(evt, { + dx: x, + dy: y + }); + }, - if (isReversed) { - animationAttributes.keyPoints = '1;0'; - animationAttributes.keyTimes = '0;1'; + // Drag End Handlers + + dragLabelEnd: function() { + // noop + }, + + dragVertexEnd: function() { + // noop + }, + + dragArrowheadEnd: function(evt, x, y) { + + var data = this.eventData(evt); + var paper = this.paper; + + if (paper.options.snapLinks) { + this._snapArrowheadEnd(data); + } else { + this._connectArrowheadEnd(data, x, y); } - var vToken = V(token); - var vPath = this._V.connection; + if (!paper.linkAllowed(this)) { + // If the changed link is not allowed, revert to its previous state. + this._disallow(data); + } else { + this._finishEmbedding(data); + this._notifyConnectEvent(data, evt); + } - vToken - .appendTo(this.paper.viewport) - .animateAlongPath(animationAttributes, vPath); + this._afterArrowheadMove(data); + + // mouseleave event is not triggered due to changing pointer-events to `none`. + if (!this.vel.contains(evt.target)) { + this.mouseleave(evt); + } + }, - setTimeout(onAnimationEnd(vToken, callback), duration); + dragEnd: function() { + // noop }, - findRoute: function(oldVertices) { + _disallow: function(data) { - var namespace = joint.routers; - var router = this.model.get('router'); - var defaultRouter = this.paper.options.defaultRouter; + switch (data.whenNotAllowed) { - if (!router) { + case 'remove': + this.model.remove({ ui: true }); + break; - if (this.model.get('manhattan')) { - // backwards compability - router = { name: 'orthogonal' }; - } else if (defaultRouter) { - router = defaultRouter; - } else { - return oldVertices; - } + case 'revert': + default: + this.model.set(data.arrowhead, data.initialEnd, { ui: true }); + break; } + }, - var args = router.args || {}; - var routerFn = joint.util.isFunction(router) ? router : namespace[router.name]; + _finishEmbedding: function(data) { - if (!joint.util.isFunction(routerFn)) { - throw new Error('unknown router: "' + router.name + '"'); + // Reparent the link if embedding is enabled + if (this.paper.options.embeddingMode && this.model.reparent()) { + // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). + data.z = null; } + }, - var newVertices = routerFn.call(this, oldVertices || [], args, this); + _notifyConnectEvent: function(data, evt) { - return newVertices; + var arrowhead = data.arrowhead; + var initialEnd = data.initialEnd; + var currentEnd = this.model.prop(arrowhead); + var endChanged = currentEnd && !joint.dia.Link.endsEqual(initialEnd, currentEnd); + if (endChanged) { + var paper = this.paper; + if (initialEnd.id) { + this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), data.initialMagnet, arrowhead); + } + if (currentEnd.id) { + this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), data.magnetUnderPointer, arrowhead); + } + } }, - // Return the `d` attribute value of the `` element representing the link - // between `source` and `target`. - getPathData: function(vertices) { + _snapArrowhead: function(x, y, data) { - var namespace = joint.connectors; - var connector = this.model.get('connector'); - var defaultConnector = this.paper.options.defaultConnector; + // checking view in close area of the pointer - if (!connector) { + var r = this.paper.options.snapLinks.radius || 50; + var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); - // backwards compability - if (this.model.get('smooth')) { - connector = { name: 'smooth' }; - } else { - connector = defaultConnector || {}; - } + if (data.closestView) { + data.closestView.unhighlight(data.closestMagnet, { + connecting: true, + snapping: true + }); } + data.closestView = data.closestMagnet = null; - var connectorFn = joint.util.isFunction(connector) ? connector : namespace[connector.name]; - var args = connector.args || {}; + var distance; + var minDistance = Number.MAX_VALUE; + var pointer = g.point(x, y); + var paper = this.paper; - if (!joint.util.isFunction(connectorFn)) { - throw new Error('unknown connector: "' + connector.name + '"'); - } + viewsInArea.forEach(function(view) { - var pathData = connectorFn.call( - this, - this._markerCache.sourcePoint, // Note that the value is translated by the size - this._markerCache.targetPoint, // of the marker. (We'r not using this.sourcePoint) - vertices || (this.model.get('vertices') || {}), - args, // options - this - ); + // skip connecting to the element in case '.': { magnet: false } attribute present + if (view.el.getAttribute('magnet') !== 'false') { - return pathData; - }, + // find distance from the center of the model to pointer coordinates + distance = view.model.getBBox().center().distance(pointer); - // Find a point that is the start of the connection. - // If `selectorOrPoint` is a point, then we're done and that point is the start of the connection. - // If the `selectorOrPoint` is an element however, we need to know a reference point (or element) - // that the link leads to in order to determine the start of the connection on the original element. - getConnectionPoint: function(end, selectorOrPoint, referenceSelectorOrPoint) { + // the connection is looked up in a circle area by `distance < r` + if (distance < r && distance < minDistance) { - var spot; + if (paper.options.validateConnection.apply( + paper, data.validateConnectionArgs(view, null) + )) { + minDistance = distance; + data.closestView = view; + data.closestMagnet = view.el; + } + } + } - // If the `selectorOrPoint` (or `referenceSelectorOrPoint`) is `undefined`, the `source`/`target` of the link model is `undefined`. - // We want to allow this however so that one can create links such as `var link = new joint.dia.Link` and - // set the `source`/`target` later. - joint.util.isEmpty(selectorOrPoint) && (selectorOrPoint = { x: 0, y: 0 }); - joint.util.isEmpty(referenceSelectorOrPoint) && (referenceSelectorOrPoint = { x: 0, y: 0 }); + view.$('[magnet]').each(function(index, magnet) { - if (!selectorOrPoint.id) { + var bbox = view.getNodeBBox(magnet); - // If the source is a point, we don't need a reference point to find the sticky point of connection. - spot = g.Point(selectorOrPoint); + distance = pointer.distance({ + x: bbox.x + bbox.width / 2, + y: bbox.y + bbox.height / 2 + }); - } else { + if (distance < r && distance < minDistance) { - // If the source is an element, we need to find a point on the element boundary that is closest - // to the reference point (or reference element). - // Get the bounding box of the spot relative to the paper viewport. This is necessary - // in order to follow paper viewport transformations (scale/rotate). - // `_sourceBbox` (`_targetBbox`) comes from `_sourceBboxUpdate` (`_sourceBboxUpdate`) - // method, it exists since first render and are automatically updated - var spotBBox = g.Rect(end === 'source' ? this.sourceBBox : this.targetBBox); + if (paper.options.validateConnection.apply( + paper, data.validateConnectionArgs(view, magnet) + )) { + minDistance = distance; + data.closestView = view; + data.closestMagnet = magnet; + } + } - var reference; + }.bind(this)); - if (!referenceSelectorOrPoint.id) { + }, this); - // Reference was passed as a point, therefore, we're ready to find the sticky point of connection on the source element. - reference = g.Point(referenceSelectorOrPoint); + var end; + var closestView = data.closestView; + var closestMagnet = data.closestMagnet; + var endType = data.arrowhead; + if (closestView) { + closestView.highlight(closestMagnet, { + connecting: true, + snapping: true + }); + end = closestView.getLinkEnd(closestMagnet, x, y, this.model, endType); + } else { + end = { x: x, y: y }; + } - } else { + this.model.set(endType, end || { x: x, y: y }, { ui: true }); + }, - // Reference was passed as an element, therefore we need to find a point on the reference - // element boundary closest to the source element. - // Get the bounding box of the spot relative to the paper viewport. This is necessary - // in order to follow paper viewport transformations (scale/rotate). - var referenceBBox = g.Rect(end === 'source' ? this.targetBBox : this.sourceBBox); + _snapArrowheadEnd: function(data) { - reference = referenceBBox.intersectionWithLineFromCenterToPoint(spotBBox.center()); - reference = reference || referenceBBox.center(); - } + // Finish off link snapping. + // Everything except view unhighlighting was already done on pointermove. + var closestView = data.closestView; + var closestMagnet = data.closestMagnet; + if (closestView && closestMagnet) { - var paperOptions = this.paper.options; - // If `perpendicularLinks` flag is set on the paper and there are vertices - // on the link, then try to find a connection point that makes the link perpendicular - // even though the link won't point to the center of the targeted object. - if (paperOptions.perpendicularLinks || this.options.perpendicular) { + closestView.unhighlight(closestMagnet, { connecting: true, snapping: true }); + data.magnetUnderPointer = closestView.findMagnet(closestMagnet); + } - var nearestSide; - var spotOrigin = spotBBox.origin(); - var spotCorner = spotBBox.corner(); + data.closestView = data.closestMagnet = null; + }, - if (spotOrigin.y <= reference.y && reference.y <= spotCorner.y) { + _connectArrowhead: function(target, x, y, data) { - nearestSide = spotBBox.sideNearestToPoint(reference); - switch (nearestSide) { - case 'left': - spot = g.Point(spotOrigin.x, reference.y); - break; - case 'right': - spot = g.Point(spotCorner.x, reference.y); - break; - default: - spot = spotBBox.center(); - break; - } + // checking views right under the pointer - } else if (spotOrigin.x <= reference.x && reference.x <= spotCorner.x) { + if (data.eventTarget !== target) { + // Unhighlight the previous view under pointer if there was one. + if (data.magnetUnderPointer) { + data.viewUnderPointer.unhighlight(data.magnetUnderPointer, { + connecting: true + }); + } - nearestSide = spotBBox.sideNearestToPoint(reference); - switch (nearestSide) { - case 'top': - spot = g.Point(reference.x, spotOrigin.y); - break; - case 'bottom': - spot = g.Point(reference.x, spotCorner.y); - break; - default: - spot = spotBBox.center(); - break; + data.viewUnderPointer = this.paper.findView(target); + if (data.viewUnderPointer) { + // If we found a view that is under the pointer, we need to find the closest + // magnet based on the real target element of the event. + data.magnetUnderPointer = data.viewUnderPointer.findMagnet(target); + + if (data.magnetUnderPointer && this.paper.options.validateConnection.apply( + this.paper, + data.validateConnectionArgs(data.viewUnderPointer, data.magnetUnderPointer) + )) { + // If there was no magnet found, do not highlight anything and assume there + // is no view under pointer we're interested in reconnecting to. + // This can only happen if the overall element has the attribute `'.': { magnet: false }`. + if (data.magnetUnderPointer) { + data.viewUnderPointer.highlight(data.magnetUnderPointer, { + connecting: true + }); } - } else { - - // If there is no intersection horizontally or vertically with the object bounding box, - // then we fall back to the regular situation finding straight line (not perpendicular) - // between the object and the reference point. - spot = spotBBox.intersectionWithLineFromCenterToPoint(reference); - spot = spot || spotBBox.center(); + // This type of connection is not valid. Disregard this magnet. + data.magnetUnderPointer = null; } - - } else if (paperOptions.linkConnectionPoint) { - - var view = (end === 'target') ? this.targetView : this.sourceView; - var magnet = (end === 'target') ? this.targetMagnet : this.sourceMagnet; - - spot = paperOptions.linkConnectionPoint(this, view, magnet, reference, end); - } else { - - spot = spotBBox.intersectionWithLineFromCenterToPoint(reference); - spot = spot || spotBBox.center(); + // Make sure we'll unset previous magnet. + data.magnetUnderPointer = null; } } - return spot; - }, + data.eventTarget = target; - // Public API - // ---------- + this.model.set(data.arrowhead, { x: x, y: y }, { ui: true }); + }, - getConnectionLength: function() { + _connectArrowheadEnd: function(data, x, y) { - return this._V.connection.node.getTotalLength(); - }, + var view = data.viewUnderPointer; + var magnet = data.magnetUnderPointer; + if (!magnet || !view) return; - getPointAtLength: function(length) { + view.unhighlight(magnet, { connecting: true }); - return this._V.connection.node.getPointAtLength(length); + var endType = data.arrowhead; + var end = view.getLinkEnd(magnet, x, y, this.model, endType); + this.model.set(endType, end, { ui: true }); }, - // Interaction. The controller part. - // --------------------------------- - - _beforeArrowheadMove: function() { + _beforeArrowheadMove: function(data) { - this._z = this.model.get('z'); + data.z = this.model.get('z'); this.model.toFront(); // Let the pointer propagate throught the link view elements so that @@ -10485,15 +16787,15 @@ joint.dia.LinkView = joint.dia.CellView.extend({ this.el.style.pointerEvents = 'none'; if (this.paper.options.markAvailable) { - this._markAvailableMagnets(); + this._markAvailableMagnets(data); } }, - _afterArrowheadMove: function() { + _afterArrowheadMove: function(data) { - if (this._z !== null) { - this.model.set('z', this._z, { ui: true }); - this._z = null; + if (data.z !== null) { + this.model.set('z', data.z, { ui: true }); + data.z = null; } // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation. @@ -10502,7 +16804,7 @@ joint.dia.LinkView = joint.dia.CellView.extend({ this.el.style.pointerEvents = 'visiblePainted'; if (this.paper.options.markAvailable) { - this._unmarkAvailableMagnets(); + this._unmarkAvailableMagnets(data); } }, @@ -10542,17 +16844,17 @@ joint.dia.LinkView = joint.dia.CellView.extend({ return validateConnectionArgs; }, - _markAvailableMagnets: function() { + _markAvailableMagnets: function(data) { function isMagnetAvailable(view, magnet) { var paper = view.paper; var validate = paper.options.validateConnection; - return validate.apply(paper, this._validateConnectionArgs(view, magnet)); + return validate.apply(paper, this.validateConnectionArgs(view, magnet)); } var paper = this.paper; var elements = paper.model.getElements(); - this._marked = {}; + data.marked = {}; for (var i = 0, n = elements.length; i < n; i++) { var view = elements[i].findView(paper); @@ -10563,465 +16865,81 @@ joint.dia.LinkView = joint.dia.CellView.extend({ var magnets = Array.prototype.slice.call(view.el.querySelectorAll('[magnet]')); if (view.el.getAttribute('magnet') !== 'false') { - // Element wrapping group is also a magnet - magnets.push(view.el); - } - - var availableMagnets = magnets.filter(isMagnetAvailable.bind(this, view)); - - if (availableMagnets.length > 0) { - // highlight all available magnets - for (var j = 0, m = availableMagnets.length; j < m; j++) { - view.highlight(availableMagnets[j], { magnetAvailability: true }); - } - // highlight the entire view - view.highlight(null, { elementAvailability: true }); - - this._marked[view.model.id] = availableMagnets; - } - } - }, - - _unmarkAvailableMagnets: function() { - - var markedKeys = Object.keys(this._marked); - var id; - var markedMagnets; - - for (var i = 0, n = markedKeys.length; i < n; i++) { - id = markedKeys[i]; - markedMagnets = this._marked[id]; - - var view = this.paper.findViewByModel(id); - if (view) { - for (var j = 0, m = markedMagnets.length; j < m; j++) { - view.unhighlight(markedMagnets[j], { magnetAvailability: true }); - } - view.unhighlight(null, { elementAvailability: true }); - } - } - - this._marked = null; - }, - - startArrowheadMove: function(end, opt) { - - opt = joint.util.defaults(opt || {}, { whenNotAllowed: 'revert' }); - // Allow to delegate events from an another view to this linkView in order to trigger arrowhead - // move without need to click on the actual arrowhead dom element. - this._action = 'arrowhead-move'; - this._whenNotAllowed = opt.whenNotAllowed; - this._arrowhead = end; - this._initialMagnet = this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null); - this._initialEnd = joint.util.assign({}, this.model.get(end)) || { x: 0, y: 0 }; - this._validateConnectionArgs = this._createValidateConnectionArgs(this._arrowhead); - this._beforeArrowheadMove(); - }, - - pointerdown: function(evt, x, y) { - - joint.dia.CellView.prototype.pointerdown.apply(this, arguments); - this.notify('link:pointerdown', evt, x, y); - - this._dx = x; - this._dy = y; - - // if are simulating pointerdown on a link during a magnet click, skip link interactions - if (evt.target.getAttribute('magnet') != null) return; - - var className = joint.util.removeClassNamePrefix(evt.target.getAttribute('class')); - var parentClassName = joint.util.removeClassNamePrefix(evt.target.parentNode.getAttribute('class')); - var labelNode; - if (parentClassName === 'label') { - className = parentClassName; - labelNode = evt.target.parentNode; - } else { - labelNode = evt.target; - } - - switch (className) { - - case 'marker-vertex': - if (this.can('vertexMove')) { - this._action = 'vertex-move'; - this._vertexIdx = evt.target.getAttribute('idx'); - } - break; - - case 'marker-vertex-remove': - case 'marker-vertex-remove-area': - if (this.can('vertexRemove')) { - this.removeVertex(evt.target.getAttribute('idx')); - } - break; - - case 'marker-arrowhead': - if (this.can('arrowheadMove')) { - this.startArrowheadMove(evt.target.getAttribute('end')); - } - break; - - case 'label': - if (this.can('labelMove')) { - this._action = 'label-move'; - this._labelIdx = parseInt(V(labelNode).attr('label-idx'), 10); - // Precalculate samples so that we don't have to do that - // over and over again while dragging the label. - this._samples = this._V.connection.sample(1); - this._linkLength = this._V.connection.node.getTotalLength(); - } - break; - - default: - - if (this.can('vertexAdd')) { - - // Store the index at which the new vertex has just been placed. - // We'll be update the very same vertex position in `pointermove()`. - this._vertexIdx = this.addVertex({ x: x, y: y }); - this._action = 'vertex-move'; - } - } - }, - - pointermove: function(evt, x, y) { - - switch (this._action) { - - case 'vertex-move': - - var vertices = joint.util.assign([], this.model.get('vertices')); - vertices[this._vertexIdx] = { x: x, y: y }; - this.model.set('vertices', vertices, { ui: true }); - break; - - case 'label-move': - - var dragPoint = { x: x, y: y }; - var samples = this._samples; - var minSqDistance = Infinity; - var closestSample; - var closestSampleIndex; - var p; - var sqDistance; - for (var i = 0, n = samples.length; i < n; i++) { - p = samples[i]; - sqDistance = g.line(p, dragPoint).squaredLength(); - if (sqDistance < minSqDistance) { - minSqDistance = sqDistance; - closestSample = p; - closestSampleIndex = i; - } - } - var prevSample = samples[closestSampleIndex - 1]; - var nextSample = samples[closestSampleIndex + 1]; - var offset = 0; - if (prevSample && nextSample) { - offset = g.line(prevSample, nextSample).pointOffset(dragPoint); - } else if (prevSample) { - offset = g.line(prevSample, closestSample).pointOffset(dragPoint); - } else if (nextSample) { - offset = g.line(closestSample, nextSample).pointOffset(dragPoint); - } - - this.model.label(this._labelIdx, { - position: { - distance: closestSample.distance / this._linkLength, - offset: offset - } - }); - break; - - case 'arrowhead-move': - - if (this.paper.options.snapLinks) { - - // checking view in close area of the pointer - - var r = this.paper.options.snapLinks.radius || 50; - var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); - - if (this._closestView) { - this._closestView.unhighlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - } - this._closestView = this._closestEnd = null; - - var distance; - var minDistance = Number.MAX_VALUE; - var pointer = g.point(x, y); - - viewsInArea.forEach(function(view) { - - // skip connecting to the element in case '.': { magnet: false } attribute present - if (view.el.getAttribute('magnet') !== 'false') { - - // find distance from the center of the model to pointer coordinates - distance = view.model.getBBox().center().distance(pointer); - - // the connection is looked up in a circle area by `distance < r` - if (distance < r && distance < minDistance) { - - if (this.paper.options.validateConnection.apply( - this.paper, this._validateConnectionArgs(view, null) - )) { - minDistance = distance; - this._closestView = view; - this._closestEnd = { id: view.model.id }; - } - } - } - - view.$('[magnet]').each(function(index, magnet) { - - var bbox = V(magnet).getBBox({ target: this.paper.viewport }); - - distance = pointer.distance({ - x: bbox.x + bbox.width / 2, - y: bbox.y + bbox.height / 2 - }); - - if (distance < r && distance < minDistance) { - - if (this.paper.options.validateConnection.apply( - this.paper, this._validateConnectionArgs(view, magnet) - )) { - minDistance = distance; - this._closestView = view; - this._closestEnd = { - id: view.model.id, - selector: view.getSelector(magnet), - port: magnet.getAttribute('port') - }; - } - } - - }.bind(this)); - - }, this); - - if (this._closestView) { - this._closestView.highlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - } - - this.model.set(this._arrowhead, this._closestEnd || { x: x, y: y }, { ui: true }); - - } else { - - // checking views right under the pointer - - // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. - // It holds the element when a touchstart triggered. - var target = (evt.type === 'mousemove') - ? evt.target - : document.elementFromPoint(evt.clientX, evt.clientY); - - if (this._eventTarget !== target) { - // Unhighlight the previous view under pointer if there was one. - if (this._magnetUnderPointer) { - this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { - connecting: true - }); - } - - this._viewUnderPointer = this.paper.findView(target); - if (this._viewUnderPointer) { - // If we found a view that is under the pointer, we need to find the closest - // magnet based on the real target element of the event. - this._magnetUnderPointer = this._viewUnderPointer.findMagnet(target); - - if (this._magnetUnderPointer && this.paper.options.validateConnection.apply( - this.paper, - this._validateConnectionArgs(this._viewUnderPointer, this._magnetUnderPointer) - )) { - // If there was no magnet found, do not highlight anything and assume there - // is no view under pointer we're interested in reconnecting to. - // This can only happen if the overall element has the attribute `'.': { magnet: false }`. - if (this._magnetUnderPointer) { - this._viewUnderPointer.highlight(this._magnetUnderPointer, { - connecting: true - }); - } - } else { - // This type of connection is not valid. Disregard this magnet. - this._magnetUnderPointer = null; - } - } else { - // Make sure we'll unset previous magnet. - this._magnetUnderPointer = null; - } - } - - this._eventTarget = target; - - this.model.set(this._arrowhead, { x: x, y: y }, { ui: true }); - } - break; - } - - this._dx = x; - this._dy = y; - - joint.dia.CellView.prototype.pointermove.apply(this, arguments); - this.notify('link:pointermove', evt, x, y); - }, - - pointerup: function(evt, x, y) { - - if (this._action === 'label-move') { - - this._samples = null; - - } else if (this._action === 'arrowhead-move') { - - var model = this.model; - var paper = this.paper; - var paperOptions = paper.options; - var arrowhead = this._arrowhead; - var initialEnd = this._initialEnd; - var magnetUnderPointer; - - if (paperOptions.snapLinks) { - - // Finish off link snapping. - // Everything except view unhighlighting was already done on pointermove. - if (this._closestView) { - this._closestView.unhighlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - - magnetUnderPointer = this._closestView.findMagnet(this._closestEnd.selector); - } - - this._closestView = this._closestEnd = null; - - } else { - - var viewUnderPointer = this._viewUnderPointer; - magnetUnderPointer = this._magnetUnderPointer; - - this._viewUnderPointer = null; - this._magnetUnderPointer = null; - - if (magnetUnderPointer) { - - viewUnderPointer.unhighlight(magnetUnderPointer, { connecting: true }); - // Find a unique `selector` of the element under pointer that is a magnet. If the - // `this._magnetUnderPointer` is the root element of the `this._viewUnderPointer` itself, - // the returned `selector` will be `undefined`. That means we can directly pass it to the - // `source`/`target` attribute of the link model below. - var selector = viewUnderPointer.getSelector(magnetUnderPointer); - var port = magnetUnderPointer.getAttribute('port'); - var arrowheadValue = { id: viewUnderPointer.model.id }; - if (port != null) arrowheadValue.port = port; - if (selector != null) arrowheadValue.selector = selector; - model.set(arrowhead, arrowheadValue, { ui: true }); - } - } - - // If the changed link is not allowed, revert to its previous state. - if (!paper.linkAllowed(this)) { - - switch (this._whenNotAllowed) { - - case 'remove': - model.remove({ ui: true }); - break; - - case 'revert': - default: - model.set(arrowhead, initialEnd, { ui: true }); - break; - } + // Element wrapping group is also a magnet + magnets.push(view.el); + } - } else { + var availableMagnets = magnets.filter(isMagnetAvailable.bind(data, view)); - // Reparent the link if embedding is enabled - if (paperOptions.embeddingMode && model.reparent()) { - // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). - this._z = null; + if (availableMagnets.length > 0) { + // highlight all available magnets + for (var j = 0, m = availableMagnets.length; j < m; j++) { + view.highlight(availableMagnets[j], { magnetAvailability: true }); } + // highlight the entire view + view.highlight(null, { elementAvailability: true }); - var currentEnd = model.prop(arrowhead); - var endChanged = currentEnd && !joint.dia.Link.endsEqual(initialEnd, currentEnd); - if (endChanged) { - - if (initialEnd.id) { - this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), this._initialMagnet, arrowhead); - } - if (currentEnd.id) { - this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), magnetUnderPointer, arrowhead); - } - } + data.marked[view.model.id] = availableMagnets; } - - this._afterArrowheadMove(); } - - this._action = null; - this._whenNotAllowed = null; - this._initialMagnet = null; - this._initialEnd = null; - this._validateConnectionArgs = null; - this._eventTarget = null; - - this.notify('link:pointerup', evt, x, y); - joint.dia.CellView.prototype.pointerup.apply(this, arguments); }, - mouseenter: function(evt) { + _unmarkAvailableMagnets: function(data) { - joint.dia.CellView.prototype.mouseenter.apply(this, arguments); - this.notify('link:mouseenter', evt); - }, + var markedKeys = Object.keys(data.marked); + var id; + var markedMagnets; - mouseleave: function(evt) { + for (var i = 0, n = markedKeys.length; i < n; i++) { + id = markedKeys[i]; + markedMagnets = data.marked[id]; - joint.dia.CellView.prototype.mouseleave.apply(this, arguments); - this.notify('link:mouseleave', evt); + var view = this.paper.findViewByModel(id); + if (view) { + for (var j = 0, m = markedMagnets.length; j < m; j++) { + view.unhighlight(markedMagnets[j], { magnetAvailability: true }); + } + view.unhighlight(null, { elementAvailability: true }); + } + } + + data.marked = null; }, - event: function(evt, eventName, x, y) { + startArrowheadMove: function(end, opt) { - // Backwards compatibility - var linkTool = V(evt.target).findParentByClass('link-tool', this.el); - if (linkTool) { - // No further action to be executed - evt.stopPropagation(); - // Allow `interactive.useLinkTools=false` - if (this.can('useLinkTools')) { - if (eventName === 'remove') { - // Built-in remove event - this.model.remove({ ui: true }); - } else { - // link:options and other custom events inside the link tools - this.notify(eventName, evt, x, y); - } - } + opt || (opt = {}); - } else { + // Allow to delegate events from an another view to this linkView in order to trigger arrowhead + // move without need to click on the actual arrowhead dom element. + var data = { + action: 'arrowhead-move', + arrowhead: end, + whenNotAllowed: opt.whenNotAllowed || 'revert', + initialMagnet: this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null), + initialEnd: joint.util.clone(this.model.get(end)), + validateConnectionArgs: this._createValidateConnectionArgs(end) + }; - joint.dia.CellView.prototype.event.apply(this, arguments); + this._beforeArrowheadMove(data); + + if (opt.ignoreBackwardsCompatibility !== true) { + this._dragData = data; } - } + return data; + } }, { makeSelector: function(end) { - var selector = '[model-id="' + end.id + '"]'; + var selector = ''; // `port` has a higher precendence over `selector`. This is because the selector to the magnet // might change while the name of the port can stay the same. if (end.port) { - selector += ' [port="' + end.port + '"]'; + selector += '[port="' + end.port + '"]'; } else if (end.selector) { - selector += ' ' + end.selector; + selector += end.selector; } return selector; @@ -11030,6 +16948,40 @@ joint.dia.LinkView = joint.dia.CellView.extend({ }); +Object.defineProperty(joint.dia.LinkView.prototype, 'sourceBBox', { + + enumerable: true, + + get: function() { + var sourceView = this.sourceView; + var sourceMagnet = this.sourceMagnet; + if (sourceView) { + if (!sourceMagnet) sourceMagnet = sourceView.el; + return sourceView.getNodeBBox(sourceMagnet); + } + var sourceDef = this.model.source(); + return new g.Rect(sourceDef.x, sourceDef.y, 1, 1); + } + +}); + +Object.defineProperty(joint.dia.LinkView.prototype, 'targetBBox', { + + enumerable: true, + + get: function() { + var targetView = this.targetView; + var targetMagnet = this.targetMagnet; + if (targetView) { + if (!targetMagnet) targetMagnet = targetView.el; + return targetView.getNodeBBox(targetMagnet); + } + var targetDef = this.model.target(); + return new g.Rect(targetDef.x, targetDef.y, 1, 1); + } +}); + + joint.dia.Paper = joint.mvc.View.extend({ className: 'paper', @@ -11087,8 +17039,10 @@ joint.dia.Paper = joint.mvc.View.extend({ // Prevent the default context menu from being displayed. preventContextMenu: true, + // Prevent the default action for blank:pointer. preventDefaultBlankAction: true, + // Restrict the translation of elements by given bounding box. // Option accepts a boolean: // true - the translation is restricted to the paper area @@ -11101,6 +17055,7 @@ joint.dia.Paper = joint.mvc.View.extend({ // Or a bounding box: // restrictTranslate: { x: 10, y: 10, width: 790, height: 590 } restrictTranslate: false, + // Marks all available magnets with 'available-magnet' class name and all available cells with // 'available-cell' class name. Marks them when dragging a link is started and unmark // when the dragging is stopped. @@ -11119,8 +17074,14 @@ joint.dia.Paper = joint.mvc.View.extend({ // e.g. { name: 'oneSide', args: { padding: 10 }} or a function defaultRouter: { name: 'normal' }, + defaultAnchor: { name: 'center' }, + + defaultConnectionPoint: { name: 'bbox' }, + /* CONNECTING */ + connectionStrategy: null, + // Check whether to add a new link to the graph when user clicks on an a magnet. validateMagnet: function(cellView, magnet) { return magnet.getAttribute('magnet') !== 'passive'; @@ -11145,7 +17106,7 @@ joint.dia.Paper = joint.mvc.View.extend({ }, // Determines the way how a cell finds a suitable parent when it's dragged over the paper. - // The cell with the highest z-index (visually on the top) will be choosen. + // The cell with the highest z-index (visually on the top) will be chosen. findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft' // If enabled only the element on the very front is taken into account for the embedding. @@ -11162,6 +17123,11 @@ joint.dia.Paper = joint.mvc.View.extend({ // i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 }; linkPinning: true, + // Custom validation after an interaction with a link ends. + // Recognizes a function. If `false` is returned, the link is disallowed (removed or reverted) + // (linkView, paper) => boolean + allowLink: null, + // Allowed number of mousemove events after which the pointerclick event will be still triggered. clickThreshold: 0, @@ -11176,23 +17142,37 @@ joint.dia.Paper = joint.mvc.View.extend({ }, events: { - + 'dblclick': 'pointerdblclick', + 'click': 'pointerclick', // triggered alongside pointerdown and pointerup if no movement + 'touchend': 'pointerclick', // triggered alongside pointerdown and pointerup if no movement + 'contextmenu': 'contextmenu', 'mousedown': 'pointerdown', - 'dblclick': 'mousedblclick', - 'click': 'mouseclick', 'touchstart': 'pointerdown', - 'touchend': 'mouseclick', - 'touchmove': 'pointermove', - 'mousemove': 'pointermove', - 'mouseover .joint-cell': 'cellMouseover', - 'mouseout .joint-cell': 'cellMouseout', - 'contextmenu': 'contextmenu', + 'mouseover': 'mouseover', + 'mouseout': 'mouseout', + 'mouseenter': 'mouseenter', + 'mouseleave': 'mouseleave', 'mousewheel': 'mousewheel', 'DOMMouseScroll': 'mousewheel', - 'mouseenter .joint-cell': 'cellMouseenter', - 'mouseleave .joint-cell': 'cellMouseleave', - 'mousedown .joint-cell [event]': 'cellEvent', - 'touchstart .joint-cell [event]': 'cellEvent' + 'mouseenter .joint-cell': 'mouseenter', + 'mouseleave .joint-cell': 'mouseleave', + 'mouseenter .joint-tools': 'mouseenter', + 'mouseleave .joint-tools': 'mouseleave', + 'mousedown .joint-cell [event]': 'onevent', // interaction with cell with `event` attribute set + 'touchstart .joint-cell [event]': 'onevent', + 'mousedown .joint-cell [magnet]': 'onmagnet', // interaction with cell with `magnet` attribute set + 'touchstart .joint-cell [magnet]': 'onmagnet', + 'mousedown .joint-link .label': 'onlabel', // interaction with link label + 'touchstart .joint-link .label': 'onlabel', + 'dragstart .joint-cell image': 'onImageDragStart' // firefox fix + }, + + documentEvents: { + 'mousemove': 'pointermove', + 'touchmove': 'pointermove', + 'mouseup': 'pointerup', + 'touchend': 'pointerup', + 'touchcancel': 'pointerup' }, _highlights: {}, @@ -11243,15 +17223,6 @@ joint.dia.Paper = joint.mvc.View.extend({ ); }, - bindDocumentEvents: function() { - var eventNS = this.getEventNamespace(); - this.$document.on('mouseup' + eventNS + ' touchend' + eventNS, this.pointerup); - }, - - unbindDocumentEvents: function() { - this.$document.off(this.getEventNamespace()); - }, - render: function() { this.$el.empty(); @@ -11259,9 +17230,10 @@ joint.dia.Paper = joint.mvc.View.extend({ this.svg = V('svg').attr({ width: '100%', height: '100%' }).node; this.viewport = V('g').addClass(joint.util.addClassNamePrefix('viewport')).node; this.defs = V('defs').node; - + this.tools = V('g').addClass(joint.util.addClassNamePrefix('tools-container')).node; // Append `` element to the SVG document. This is useful for filters and gradients. - V(this.svg).append([this.viewport, this.defs]); + // It's desired to have the defs defined before the viewport (e.g. to make a PDF document pick up defs properly). + V(this.svg).append([this.defs, this.viewport, this.tools]); this.$background = $('
').addClass(joint.util.addClassNamePrefix('paper-background')); if (this.options.background) { @@ -11293,6 +17265,7 @@ joint.dia.Paper = joint.mvc.View.extend({ // For storing the current transformation matrix (CTM) of the paper's viewport. _viewportMatrix: null, + // For verifying whether the CTM is up-to-date. The viewport transform attribute // could have been manipulated directly. _viewportTransformString: null, @@ -11324,7 +17297,10 @@ joint.dia.Paper = joint.mvc.View.extend({ // Setter: ctm = V.createSVGMatrix(ctm); - V(viewport).transform(ctm, { absolute: true }); + ctmString = V.matrixToTransformString(ctm); + viewport.setAttribute('transform', ctmString); + this.tools.setAttribute('transform', ctmString); + this._viewportMatrix = ctm; this._viewportTransformString = viewport.getAttribute('transform'); @@ -11336,15 +17312,18 @@ joint.dia.Paper = joint.mvc.View.extend({ return V.createSVGMatrix(this.viewport.getScreenCTM()); }, + _sortDelayingBatches: ['add', 'to-front', 'to-back'], + _onSort: function() { - if (!this.model.hasActiveBatch('add')) { + if (!this.model.hasActiveBatch(this._sortDelayingBatches)) { this.sortViews(); } }, _onBatchStop: function(data) { var name = data && data.batchName; - if (name === 'add' && !this.model.hasActiveBatch('add')) { + if (this._sortDelayingBatches.includes(name) && + !this.model.hasActiveBatch(this._sortDelayingBatches)) { this.sortViews(); } }, @@ -11353,7 +17332,6 @@ joint.dia.Paper = joint.mvc.View.extend({ //clean up all DOM elements/views to prevent memory leaks this.removeViews(); - this.unbindDocumentEvents(); }, setDimensions: function(width, height) { @@ -11529,6 +17507,13 @@ joint.dia.Paper = joint.mvc.View.extend({ this.translate(newOx, newOy); }, + // Return the dimensions of the content area in local units (without transformations). + getContentArea: function() { + + return V(this.viewport).getBBox(); + }, + + // Return the dimensions of the content bbox in client units (as it appears on screen). getContentBBox: function() { var crect = this.viewport.getBoundingClientRect(); @@ -11659,11 +17644,14 @@ joint.dia.Paper = joint.mvc.View.extend({ view.paper = this; view.render(); + return view; + }, + + onImageDragStart: function() { // This is the only way to prevent image dragging in Firefox that works. // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help. - $(view.el).find('image').on('dragstart', function() { return false; }); - return view; + return false; }, beforeRenderViews: function(cells) { @@ -11919,6 +17907,21 @@ joint.dia.Paper = joint.mvc.View.extend({ }, this); }, + removeTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'remove'); + return this; + }, + + hideTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'hide'); + return this; + }, + + showTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'show'); + return this; + }, + getModelById: function(id) { return this.model.getCell(id); @@ -11992,20 +17995,24 @@ joint.dia.Paper = joint.mvc.View.extend({ }, localToPagePoint: function(x, y) { + return this.localToPaperPoint(x, y).offset(this.pageOffset()); }, localToPageRect: function(x, y, width, height) { + return this.localToPaperRect(x, y, width, height).moveAndExpand(this.pageOffset()); }, pageToLocalPoint: function(x, y) { + var pagePoint = g.Point(x, y); var paperPoint = pagePoint.difference(this.pageOffset()); return this.paperToLocalPoint(paperPoint); }, pageToLocalRect: function(x, y, width, height) { + var pageOffset = this.pageOffset(); var paperRect = g.Rect(x, y, width, height); paperRect.x -= pageOffset.x; @@ -12014,72 +18021,38 @@ joint.dia.Paper = joint.mvc.View.extend({ }, clientOffset: function() { + var clientRect = this.svg.getBoundingClientRect(); return g.Point(clientRect.left, clientRect.top); }, pageOffset: function() { + return this.clientOffset().offset(window.scrollX, window.scrollY); }, - linkAllowed: function(linkViewOrModel) { - - var link; + linkAllowed: function(linkView) { - if (linkViewOrModel instanceof joint.dia.Link) { - link = linkViewOrModel; - } else if (linkViewOrModel instanceof joint.dia.LinkView) { - link = linkViewOrModel.model; - } else { - throw new Error('Must provide link model or view.'); + if (!(linkView instanceof joint.dia.LinkView)) { + throw new Error('Must provide a linkView.'); } - if (!this.options.multiLinks) { - - // Do not allow multiple links to have the same source and target. - - var source = link.get('source'); - var target = link.get('target'); - - if (source.id && target.id) { - - var sourceModel = link.getSourceElement(); - - if (sourceModel) { - - var connectedLinks = this.model.getConnectedLinks(sourceModel, { - outbound: true, - inbound: false - }); - - var numSameLinks = connectedLinks.filter(function(_link) { - - var _source = _link.get('source'); - var _target = _link.get('target'); - - return _source && _source.id === source.id && - (!_source.port || (_source.port === source.port)) && - _target && _target.id === target.id && - (!_target.port || (_target.port === target.port)); + var link = linkView.model; + var paperOptions = this.options; + var graph = this.model; + var ns = graph.constructor.validations; - }).length; - - if (numSameLinks > 1) { - return false; - } - } - } + if (!paperOptions.multiLinks) { + if (!ns.multiLinks.call(this, graph, link)) return false; } - if ( - !this.options.linkPinning && - ( - !joint.util.has(link.get('source'), 'id') || - !joint.util.has(link.get('target'), 'id') - ) - ) { + if (!paperOptions.linkPinning) { // Link pinning is not allowed and the link is not connected to the target. - return false; + if (!ns.linkPinning.call(this, graph, link)) return false; + } + + if (typeof paperOptions.allowLink === 'function') { + if (!paperOptions.allowLink.call(this, linkView, this)) return false; } return true; @@ -12094,8 +18067,9 @@ joint.dia.Paper = joint.mvc.View.extend({ : this.options.defaultLink.clone(); }, - // Cell highlighting - // ----------------- + // Cell highlighting. + // ------------------ + resolveHighlighter: function(opt) { opt = opt || {}; @@ -12196,9 +18170,10 @@ joint.dia.Paper = joint.mvc.View.extend({ // Interaction. // ------------ - mousedblclick: function(evt) { + pointerdblclick: function(evt) { evt.preventDefault(); + evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); @@ -12207,18 +18182,16 @@ joint.dia.Paper = joint.mvc.View.extend({ var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { - view.pointerdblclick(evt, localPoint.x, localPoint.y); } else { - this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y); } }, - mouseclick: function(evt) { + pointerclick: function(evt) { - // Trigger event when mouse not moved. + // Trigger event only if mouse has not moved. if (this._mousemoved <= this.options.clickThreshold) { evt = joint.util.normalizeEvent(evt); @@ -12229,46 +18202,19 @@ joint.dia.Paper = joint.mvc.View.extend({ var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { - view.pointerclick(evt, localPoint.x, localPoint.y); } else { - this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y); } } }, - // Guard guards the event received. If the event is not interesting, guard returns `true`. - // Otherwise, it return `false`. - guard: function(evt, view) { - - if (this.options.guard && this.options.guard(evt, view)) { - return true; - } - - if (evt.data && evt.data.guarded !== undefined) { - return evt.data.guarded; - } - - if (view && view.model && (view.model instanceof joint.dia.Cell)) { - return false; - } - - if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { - return false; - } - - return true; // Event guarded. Paper should not react on it in any way. - }, - contextmenu: function(evt) { - evt = joint.util.normalizeEvent(evt); + if (this.options.preventContextMenu) evt.preventDefault(); - if (this.options.preventContextMenu) { - evt.preventDefault(); - } + evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; @@ -12276,89 +18222,151 @@ joint.dia.Paper = joint.mvc.View.extend({ var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { - view.contextmenu(evt, localPoint.x, localPoint.y); } else { - this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y); } }, pointerdown: function(evt) { - this.bindDocumentEvents(); - evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; - this._mousemoved = 0; - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { evt.preventDefault(); - - this.sourceView = view; - view.pointerdown(evt, localPoint.x, localPoint.y); } else { - if (this.options.preventDefaultBlankAction) { - evt.preventDefault(); - } + if (this.options.preventDefaultBlankAction) evt.preventDefault(); this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y); } + + this.delegateDragEvents(view, evt.data); }, pointermove: function(evt) { - var view = this.sourceView; - if (view) { + evt.preventDefault(); - evt.preventDefault(); + // mouse moved counter + var data = this.eventData(evt); + data.mousemoved || (data.mousemoved = 0); + var mousemoved = ++data.mousemoved; + if (mousemoved <= this.options.moveThreshold) return; - // Mouse moved counter. - var mousemoved = ++this._mousemoved; - if (mousemoved > this.options.moveThreshold) { + evt = joint.util.normalizeEvent(evt); - evt = joint.util.normalizeEvent(evt); + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - view.pointermove(evt, localPoint.x, localPoint.y); - } + var view = data.sourceView; + if (view) { + view.pointermove(evt, localPoint.x, localPoint.y); + } else { + this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y); } + + this.eventData(evt, data); }, pointerup: function(evt) { - this.unbindDocumentEvents(); + this.undelegateDocumentEvents(); evt = joint.util.normalizeEvent(evt); var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - if (this.sourceView) { + var view = this.eventData(evt).sourceView; + if (view) { + view.pointerup(evt, localPoint.x, localPoint.y); + } else { + this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); + } + + this.delegateEvents(); + }, + + mouseover: function(evt) { + + evt = joint.util.normalizeEvent(evt); - this.sourceView.pointerup(evt, localPoint.x, localPoint.y); + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; - //"delete sourceView" occasionally throws an error in chrome (illegal access exception) - this.sourceView = null; + if (view) { + view.mouseover(evt); } else { + if (this.el === evt.target) return; // prevent border of paper from triggering this + this.trigger('blank:mouseover', evt); + } + }, - this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); + mouseout: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + if (view) { + view.mouseout(evt); + + } else { + if (this.el === evt.target) return; // prevent border of paper from triggering this + this.trigger('blank:mouseout', evt); + } + }, + + mouseenter: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + var relatedView = this.findView(evt.relatedTarget); + if (view) { + // mouse moved from tool over view? + if (relatedView === view) return; + view.mouseenter(evt); + } else { + if (relatedView) return; + // `paper` (more descriptive), not `blank` + this.trigger('paper:mouseenter', evt); + } + }, + + mouseleave: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + var relatedView = this.findView(evt.relatedTarget); + if (view) { + // mouse moved from view over tool? + if (relatedView === view) return; + view.mouseleave(evt); + } else { + if (relatedView) return; + // `paper` (more descriptive), not `blank` + this.trigger('paper:mouseleave', evt); } }, mousewheel: function(evt) { evt = joint.util.normalizeEvent(evt); + var view = this.findView(evt.target); if (this.guard(evt, view)) return; @@ -12367,66 +18375,91 @@ joint.dia.Paper = joint.mvc.View.extend({ var delta = Math.max(-1, Math.min(1, (originalEvent.wheelDelta || -originalEvent.detail))); if (view) { - view.mousewheel(evt, localPoint.x, localPoint.y, delta); } else { - this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta); } }, - cellMouseover: function(evt) { + onevent: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view) { - if (this.guard(evt, view)) return; - view.mouseover(evt); + var eventNode = evt.currentTarget; + var eventName = eventNode.getAttribute('event'); + if (eventName) { + var view = this.findView(eventNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + view.onevent(evt, eventName, localPoint.x, localPoint.y); + } } }, - cellMouseout: function(evt) { + onmagnet: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); + var magnetNode = evt.currentTarget; + var magnetValue = magnetNode.getAttribute('magnet'); + if (magnetValue) { + var view = this.findView(magnetNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + if (!this.options.validateMagnet(view, magnetNode)) return; + + var localPoint = this.snapToGrid(evt.clientX, evt.clientY); + view.onmagnet(evt, localPoint.x, localPoint.y); + } + } + }, + + onlabel: function(evt) { + + var labelNode = evt.currentTarget; + var view = this.findView(labelNode); if (view) { + + evt = joint.util.normalizeEvent(evt); if (this.guard(evt, view)) return; - view.mouseout(evt); + + var localPoint = this.snapToGrid(evt.clientX, evt.clientY); + view.onlabel(evt, localPoint.x, localPoint.y); } }, - cellMouseenter: function(evt) { + delegateDragEvents: function(view, data) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view && !this.guard(evt, view)) { - view.mouseenter(evt); - } + data || (data = {}); + this.eventData({ data: data }, { sourceView: view || null, mousemoved: 0 }); + this.delegateDocumentEvents(null, data); + this.undelegateEvents(); }, - cellMouseleave: function(evt) { + // Guard the specified event. If the event is not interesting, guard returns `true`. + // Otherwise, it returns `false`. + guard: function(evt, view) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view && !this.guard(evt, view)) { - view.mouseleave(evt); + if (this.options.guard && this.options.guard(evt, view)) { + return true; } - }, - cellEvent: function(evt) { + if (evt.data && evt.data.guarded !== undefined) { + return evt.data.guarded; + } - evt = joint.util.normalizeEvent(evt); + if (view && view.model && (view.model instanceof joint.dia.Cell)) { + return false; + } - var currentTarget = evt.currentTarget; - var eventName = currentTarget.getAttribute('event'); - if (eventName) { - var view = this.findView(currentTarget); - if (view && !this.guard(evt, view)) { - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - view.event(evt, eventName, localPoint.x, localPoint.y); - } + if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { + return false; } + + return true; // Event guarded. Paper should not react on it in any way. }, setGridSize: function(gridSize) { @@ -12448,22 +18481,22 @@ joint.dia.Paper = joint.mvc.View.extend({ return this; }, - _getGriRefs: function () { + _getGriRefs: function() { if (!this._gridCache) { this._gridCache = { root: V('svg', { width: '100%', height: '100%' }, V('defs')), patterns: {}, - add: function (id, vel) { + add: function(id, vel) { V(this.root.node.childNodes[0]).append(vel); this.patterns[id] = vel; this.root.append(V('rect', { width: "100%", height: "100%", fill: 'url(#' + id + ')' })); }, - get: function (id) { + get: function(id) { return this.patterns[id] }, - exist: function (id) { + exist: function(id) { return this.patterns[id] !== undefined; } } @@ -12472,7 +18505,7 @@ joint.dia.Paper = joint.mvc.View.extend({ return this._gridCache; }, - setGrid:function (drawGrid) { + setGrid: function(drawGrid) { this.clearGrid(); @@ -12480,13 +18513,13 @@ joint.dia.Paper = joint.mvc.View.extend({ this._gridSettings = []; var optionsList = Array.isArray(drawGrid) ? drawGrid : [drawGrid || {}]; - optionsList.forEach(function (item) { + optionsList.forEach(function(item) { this._gridSettings.push.apply(this._gridSettings, this._resolveDrawGridOption(item)); }, this); return this; }, - _resolveDrawGridOption: function (opt) { + _resolveDrawGridOption: function(opt) { var namespace = this.constructor.gridPatterns; if (joint.util.isString(opt) && Array.isArray(namespace[opt])) { @@ -12534,7 +18567,7 @@ joint.dia.Paper = joint.mvc.View.extend({ var ctm = this.matrix(); var refs = this._getGriRefs(); - this._gridSettings.forEach(function (gridLayerSetting, index) { + this._gridSettings.forEach(function(gridLayerSetting, index) { var id = 'pattern_' + index; var options = joint.util.merge(gridLayerSetting, localOptions[index], { @@ -12700,9 +18733,11 @@ joint.dia.Paper = joint.mvc.View.extend({ joint.util.invoke(this._views, 'setInteractivity', value); }, - // Paper Defs + // Paper definitions. + // ------------------ isDefined: function(defId) { + return !!this.svg.getElementById(defId); }, @@ -12821,7 +18856,6 @@ joint.dia.Paper = joint.mvc.View.extend({ return markerId; } - }, { backgroundPatterns: { @@ -13486,9 +19520,23 @@ joint.dia.Paper = joint.mvc.View.extend({ util.assign(joint.dia.ElementView.prototype, { - portContainerMarkup: '', - portMarkup: '', - portLabelMarkup: '', + portContainerMarkup: 'g', + portMarkup: [{ + tagName: 'circle', + selector: 'circle', + attributes: { + 'r': 10, + 'fill': '#FFFFFF', + 'stroke': '#000000' + } + }], + portLabelMarkup: [{ + tagName: 'text', + selector: 'text', + attributes: { + 'fill': '#000000' + } + }], /** @type {Object} */ _portElementsCache: null, @@ -13603,6 +19651,14 @@ joint.dia.Paper = joint.mvc.View.extend({ return this._createPortElement(port); }, + findPortNode: function(portId, selector) { + var portCache = this._portElementsCache[portId]; + if (!portCache) return null; + var portRoot = portCache.portContentElement.node; + var portSelectors = portCache.portContentSelectors; + return this.findBySelector(selector, portRoot, portSelectors)[0]; + }, + /** * @private */ @@ -13629,28 +19685,86 @@ joint.dia.Paper = joint.mvc.View.extend({ */ _createPortElement: function(port) { - var portContentElement = V(this._getPortMarkup(port)); - var portLabelContentElement = V(this._getPortLabelMarkup(port.label)); - if (portContentElement && portContentElement.length > 1) { - throw new Error('ElementView: Invalid port markup - multiple roots.'); + var portElement; + var labelElement; + + var portMarkup = this._getPortMarkup(port); + var portSelectors; + if (Array.isArray(portMarkup)) { + var portDoc = util.parseDOMJSON(portMarkup); + var portFragment = portDoc.fragment; + if (portFragment.childNodes.length > 1) { + portElement = V('g').append(portFragment); + } else { + portElement = V(portFragment.firstChild); + } + portSelectors = portDoc.selectors; + } else { + portElement = V(portMarkup); + if (Array.isArray(portElement)) { + portElement = V('g').append(portElement); + } + } + + if (!portElement) { + throw new Error('ElementView: Invalid port markup.'); } - portContentElement.attr({ + portElement.attr({ 'port': port.id, 'port-group': port.group }); - var portElement = V(this.portContainerMarkup) - .append(portContentElement) - .append(portLabelContentElement); + var labelMarkup = this._getPortLabelMarkup(port.label); + var labelSelectors; + if (Array.isArray(labelMarkup)) { + var labelDoc = util.parseDOMJSON(labelMarkup); + var labelFragment = labelDoc.fragment; + if (labelFragment.childNodes.length > 1) { + labelElement = V('g').append(labelFragment); + } else { + labelElement = V(labelFragment.firstChild); + } + labelSelectors = labelDoc.selectors; + } else { + labelElement = V(labelMarkup); + if (Array.isArray(labelElement)) { + labelElement = V('g').append(labelElement); + } + } + + if (!labelElement) { + throw new Error('ElementView: Invalid port label markup.'); + } + + var portContainerSelectors; + if (portSelectors && labelSelectors) { + for (var key in labelSelectors) { + if (portSelectors[key]) throw new Error('ElementView: selectors within port must be unique.'); + } + portContainerSelectors = util.assign({}, portSelectors, labelSelectors); + } else { + portContainerSelectors = portSelectors || labelSelectors; + } + + var portContainerElement = V(this.portContainerMarkup) + .addClass('joint-port') + .append([ + portElement.addClass('joint-port-body'), + labelElement.addClass('joint-port-label') + ]); this._portElementsCache[port.id] = { - portElement: portElement, - portLabelElement: portLabelContentElement + portElement: portContainerElement, + portLabelElement: labelElement, + portSelectors: portContainerSelectors, + portLabelSelectors: labelSelectors, + portContentElement: portElement, + portContentSelectors: portSelectors }; - return portElement; + return portContainerElement; }, /** @@ -13669,14 +19783,16 @@ joint.dia.Paper = joint.mvc.View.extend({ var portTransformation = metrics.portTransformation; this.applyPortTransform(cached.portElement, portTransformation); this.updateDOMSubtreeAttributes(cached.portElement.node, metrics.portAttrs, { - rootBBox: g.Rect(metrics.portSize) + rootBBox: new g.Rect(metrics.portSize), + selectors: cached.portSelectors }); var labelTransformation = metrics.labelTransformation; if (labelTransformation) { this.applyPortTransform(cached.portLabelElement, labelTransformation, (-portTransformation.angle || 0)); this.updateDOMSubtreeAttributes(cached.portLabelElement.node, labelTransformation.attrs, { - rootBBox: g.Rect(metrics.labelSize) + rootBBox: new g.Rect(metrics.labelSize), + selectors: cached.portLabelSelectors }); } } @@ -13979,7 +20095,7 @@ joint.shapes.basic.PortsModelInterface = { joint.util.assign(attrs, portAttributes); }, this); - joint.util.toArray(('outPorts')).forEach(function(portName, index, ports) { + joint.util.toArray(this.get('outPorts')).forEach(function(portName, index, ports) { var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out'); this._portSelectors = this._portSelectors.concat(Object.keys(portAttributes)); joint.util.assign(attrs, portAttributes); @@ -14096,7 +20212,7 @@ joint.shapes.basic.Generic.define('basic.TextBlock', { updateSize: function(cell, size) { - // Selector `foreignObject' doesn't work accross all browsers, we'r using class selector instead. + // Selector `foreignObject' doesn't work across all browsers, we're using class selector instead. // We have to clone size as we don't want attributes.div.style to be same object as attributes.size. this.attr({ '.fobj': joint.util.assign({}, size), @@ -14113,7 +20229,7 @@ joint.shapes.basic.Generic.define('basic.TextBlock', { // Content element is a
element. this.attr({ '.content': { - html: content + html: joint.util.sanitizeHTML(content) } }); @@ -14208,45 +20324,735 @@ joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({ } }); +(function(dia, util, env, V) { + + 'use strict'; + + // ELEMENTS + + var Element = dia.Element; + + Element.define('standard.Rectangle', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body', + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Circle', { + attrs: { + body: { + refCx: '50%', + refCy: '50%', + refR: '50%', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'circle', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Ellipse', { + attrs: { + body: { + refCx: '50%', + refCy: '50%', + refRx: '50%', + refRy: '50%', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'ellipse', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Path', { + attrs: { + body: { + refD: 'M 0 0 L 10 0 10 10 0 10 Z', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Polygon', { + attrs: { + body: { + refPoints: '0 0 10 0 10 10 0 10', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'polygon', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Polyline', { + attrs: { + body: { + refPoints: '0 0 10 0 10 10 0 10 0 0', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'polyline', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Image', { + attrs: { + image: { + refWidth: '100%', + refHeight: '100%', + // xlinkHref: '[URL]' + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'image', + selector: 'image' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.BorderedImage', { + attrs: { + border: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + strokeWidth: 2 + }, + image: { + // xlinkHref: '[URL]' + refWidth: -1, + refHeight: -1, + x: 0.5, + y: 0.5 + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'image', + selector: 'image' + }, { + tagName: 'rect', + selector: 'border', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.EmbeddedImage', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + fill: '#FFFFFF', + strokeWidth: 2 + }, + image: { + // xlinkHref: '[URL]' + refWidth: '30%', + refHeight: -20, + x: 10, + y: 10, + preserveAspectRatio: 'xMidYMin' + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'left', + refX: '30%', + refX2: 20, // 10 + 10 + refY: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, { + tagName: 'image', + selector: 'image' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.HeaderedRectangle', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + header: { + refWidth: '100%', + height: 30, + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + headerText: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: 15, + fontSize: 16, + fill: '#333333' + }, + bodyText: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + refY2: 15, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, { + tagName: 'rect', + selector: 'header' + }, { + tagName: 'text', + selector: 'headerText' + }, { + tagName: 'text', + selector: 'bodyText' + }] + }); + + var CYLINDER_TILT = 10; + + joint.dia.Element.define('standard.Cylinder', { + attrs: { + body: { + lateralArea: CYLINDER_TILT, + fill: '#FFFFFF', + stroke: '#333333', + strokeWidth: 2 + }, + top: { + refCx: '50%', + cy: CYLINDER_TILT, + refRx: '50%', + ry: CYLINDER_TILT, + fill: '#FFFFFF', + stroke: '#333333', + strokeWidth: 2 + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 15, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'body' + }, { + tagName: 'ellipse', + selector: 'top' + }, { + tagName: 'text', + selector: 'label' + }], + + topRy: function(t, opt) { + // getter + if (t === undefined) return this.attr('body/lateralArea'); + + // setter + var isPercentage = util.isPercentage(t); + + var bodyAttrs = { lateralArea: t }; + var topAttrs = isPercentage + ? { refCy: t, refRy: t, cy: null, ry: null } + : { refCy: null, refRy: null, cy: t, ry: t }; + + return this.attr({ body: bodyAttrs, top: topAttrs }, opt); + } + + }, { + attributes: { + lateralArea: { + set: function(t, refBBox) { + var isPercentage = util.isPercentage(t); + if (isPercentage) t = parseFloat(t) / 100; + + var x = refBBox.x; + var y = refBBox.y; + var w = refBBox.width; + var h = refBBox.height; + + // curve control point variables + var rx = w / 2; + var ry = isPercentage ? (h * t) : t; + + var kappa = V.KAPPA; + var cx = kappa * rx; + var cy = kappa * (isPercentage ? (h * t) : t); + + // shape variables + var xLeft = x; + var xCenter = x + (w / 2); + var xRight = x + w; + + var ySideTop = y + ry; + var yCurveTop = ySideTop - ry; + var ySideBottom = y + h - ry; + var yCurveBottom = y + h; + + // return calculated shape + var data = [ + 'M', xLeft, ySideTop, + 'L', xLeft, ySideBottom, + 'C', x, (ySideBottom + cy), (xCenter - cx), yCurveBottom, xCenter, yCurveBottom, + 'C', (xCenter + cx), yCurveBottom, xRight, (ySideBottom + cy), xRight, ySideBottom, + 'L', xRight, ySideTop, + 'C', xRight, (ySideTop - cy), (xCenter + cx), yCurveTop, xCenter, yCurveTop, + 'C', (xCenter - cx), yCurveTop, xLeft, (ySideTop - cy), xLeft, ySideTop, + 'Z' + ]; + return { d: data.join(' ') }; + } + } + } + }); + + var foLabelMarkup = { + tagName: 'foreignObject', + selector: 'foreignObject', + attributes: { + 'overflow': 'hidden' + }, + children: [{ + tagName: 'div', + namespaceURI: 'http://www.w3.org/1999/xhtml', + selector: 'label', + style: { + width: '100%', + height: '100%', + position: 'static', + backgroundColor: 'transparent', + textAlign: 'center', + margin: 0, + padding: '0px 5px', + boxSizing: 'border-box', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }] + }; + + var svgLabelMarkup = { + tagName: 'text', + selector: 'label', + attributes: { + 'text-anchor': 'middle' + } + }; + + Element.define('standard.TextBlock', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + fill: '#ffffff', + strokeWidth: 2 + }, + foreignObject: { + refWidth: '100%', + refHeight: '100%' + }, + label: { + style: { + fontSize: 14 + } + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, + (env.test('svgforeignobject')) ? foLabelMarkup : svgLabelMarkup + ] + }, { + attributes: { + text: { + set: function(text, refBBox, node, attrs) { + if (node instanceof HTMLElement) { + node.textContent = text; + } else { + // No foreign object + var style = attrs.style || {}; + var wrapValue = { text: text, width: -5, height: '100%' }; + var wrapAttrs = util.assign({ textVerticalAnchor: 'middle' }, style); + dia.attributes.textWrap.set.call(this, wrapValue, refBBox, node, wrapAttrs); + return { fill: style.color || null }; + } + }, + position: function(text, refBBox, node) { + // No foreign object + if (node instanceof SVGElement) return refBBox.center(); + } + } + } + }); + + // LINKS + + var Link = dia.Link; + + Link.define('standard.Link', { + attrs: { + line: { + connection: true, + stroke: '#333333', + strokeWidth: 2, + strokeLinejoin: 'round', + targetMarker: { + type: 'path', + d: 'M 10 -5 0 0 10 5 z' + } + }, + wrapper: { + connection: true, + strokeWidth: 10, + strokeLinejoin: 'round' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'wrapper', + attributes: { + 'fill': 'none', + 'cursor': 'pointer', + 'stroke': 'transparent' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none', + 'pointer-events': 'none' + } + }] + }); + + Link.define('standard.DoubleLink', { + attrs: { + line: { + connection: true, + stroke: '#DDDDDD', + strokeWidth: 4, + strokeLinejoin: 'round', + targetMarker: { + type: 'path', + stroke: '#000000', + d: 'M 10 -3 10 -10 -2 0 10 10 10 3' + } + }, + outline: { + connection: true, + stroke: '#000000', + strokeWidth: 6, + strokeLinejoin: 'round' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'outline', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none' + } + }] + }); + + Link.define('standard.ShadowLink', { + attrs: { + line: { + connection: true, + stroke: '#FF0000', + strokeWidth: 20, + strokeLinejoin: 'round', + targetMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M 0 -10 -10 0 0 10 z' + }, + sourceMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M -10 -10 0 0 -10 10 0 10 0 -10 z' + } + }, + shadow: { + connection: true, + refX: 3, + refY: 6, + stroke: '#000000', + strokeOpacity: 0.2, + strokeWidth: 20, + strokeLinejoin: 'round', + targetMarker: { + 'type': 'path', + 'd': 'M 0 -10 -10 0 0 10 z', + 'stroke': 'none' + }, + sourceMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M -10 -10 0 0 -10 10 0 10 0 -10 z' + } + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'shadow', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none' + } + }] + }); + + +})(joint.dia, joint.util, joint.env, V); + joint.routers.manhattan = (function(g, _, joint, util) { 'use strict'; var config = { - // size of the step to find a route + // size of the step to find a route (the grid of the manhattan pathfinder) step: 10, - // use of the perpendicular linkView option to connect center of element with first vertex + // the number of route finding loops that cause the router to abort + // returns fallback route instead + maximumLoops: 2000, + + // the number of decimal places to round floating point coordinates + precision: 10, + + // maximum change of direction + maxAllowedDirectionChange: 90, + + // should the router use perpendicular linkView option? + // does not connect anchor of element but rather a point close-by that is orthogonal + // this looks much better perpendicular: true, - // should be source or target not to be consider as an obstacle + // should the source and/or target not be considered as obstacles? excludeEnds: [], // 'source', 'target' - // should be any element with a certain type not to be consider as an obstacle + // should certain types of elements not be considered as obstacles? excludeTypes: ['basic.Text'], - // if number of route finding loops exceed the maximum, stops searching and returns - // fallback route - maximumLoops: 2000, - // possible starting directions from an element - startDirections: ['left', 'right', 'top', 'bottom'], + startDirections: ['top', 'right', 'bottom', 'left'], // possible ending directions to an element - endDirections: ['left', 'right', 'top', 'bottom'], + endDirections: ['top', 'right', 'bottom', 'left'], - // specify directions above + // specify the directions used above and what they mean directionMap: { + top: { x: 0, y: -1 }, right: { x: 1, y: 0 }, bottom: { x: 0, y: 1 }, - left: { x: -1, y: 0 }, - top: { x: 0, y: -1 } + left: { x: -1, y: 0 } + }, + + // cost of an orthogonal step + cost: function() { + + return this.step; + }, + + // an array of directions to find next points on the route + // different from start/end directions + directions: function() { + + var step = this.step; + var cost = this.cost(); + + return [ + { offsetX: step , offsetY: 0 , cost: cost }, + { offsetX: 0 , offsetY: step , cost: cost }, + { offsetX: -step , offsetY: 0 , cost: cost }, + { offsetX: 0 , offsetY: -step , cost: cost } + ]; + }, + + // a penalty received for direction change + penalties: function() { + + return { + 0: 0, + 45: this.step / 2, + 90: this.step / 2 + }; }, - // maximum change of the direction - maxAllowedDirectionChange: 90, - // padding applied on the element bounding boxes paddingBox: function() { @@ -14260,52 +21066,43 @@ joint.routers.manhattan = (function(g, _, joint, util) { }; }, - // an array of directions to find next points on the route - directions: function() { + // a router to use when the manhattan router fails + // (one of the partial routes returns null) + fallbackRouter: function(vertices, opt, linkView) { - var step = this.step; + if (!util.isFunction(joint.routers.orthogonal)) { + throw new Error('Manhattan requires the orthogonal router as default fallback.'); + } - return [ - { offsetX: step , offsetY: 0 , cost: step }, - { offsetX: 0 , offsetY: step , cost: step }, - { offsetX: -step , offsetY: 0 , cost: step }, - { offsetX: 0 , offsetY: -step , cost: step } - ]; + return joint.routers.orthogonal(vertices, util.assign({}, config, opt), linkView); }, - // a penalty received for direction change - penalties: function() { + /* Deprecated */ + // a simple route used in situations when main routing method fails + // (exceed max number of loop iterations, inaccessible) + fallbackRoute: function(from, to, opt) { - return { - 0: 0, - 45: this.step / 2, - 90: this.step / 2 - }; - }, + return null; // null result will trigger the fallbackRouter - // * Deprecated * - // a simple route used in situations, when main routing method fails - // (exceed loops, inaccessible). - /* i.e. - function(from, to, opts) { - // Find an orthogonal route ignoring obstacles. - var point = ((opts.previousDirAngle || 0) % 180 === 0) - ? g.point(from.x, to.y) - : g.point(to.x, from.y); - return [point, to]; - }, - */ - fallbackRoute: function() { - return null; + // left for reference: + /*// Find an orthogonal route ignoring obstacles. + + var point = ((opt.previousDirAngle || 0) % 180 === 0) + ? new g.Point(from.x, to.y) + : new g.Point(to.x, from.y); + + return [point];*/ }, // if a function is provided, it's used to route the link while dragging an end - // i.e. function(from, to, opts) { return []; } + // i.e. function(from, to, opt) { return []; } draggingRoute: null }; + // HELPER CLASSES // + // Map of obstacles - // Helper structure to identify whether a point lies in an obstacle. + // Helper structure to identify whether a point lies inside an obstacle. function ObstacleMap(opt) { this.map = {}; @@ -14318,9 +21115,9 @@ joint.routers.manhattan = (function(g, _, joint, util) { var opt = this.options; - // source or target element could be excluded from set of obstacles var excludedEnds = util.toArray(opt.excludeEnds).reduce(function(res, item) { + var end = link.get(item); if (end) { var cell = graph.getCell(end.id); @@ -14328,6 +21125,7 @@ joint.routers.manhattan = (function(g, _, joint, util) { res.push(cell); } } + return res; }, []); @@ -14344,11 +21142,11 @@ joint.routers.manhattan = (function(g, _, joint, util) { excludedAncestors = util.union(excludedAncestors, target.getAncestors().map(function(cell) { return cell.id })); } - // builds a map of all elements for quicker obstacle queries (i.e. is a point contained - // in any obstacle?) (a simplified grid search) - // The paper is divided to smaller cells, where each of them holds an information which - // elements belong to it. When we query whether a point is in an obstacle we don't need - // to go through all obstacles, we check only those in a particular cell. + // Builds a map of all elements for quicker obstacle queries (i.e. is a point contained + // in any obstacle?) (a simplified grid search). + // The paper is divided into smaller cells, where each holds information about which + // elements belong to it. When we query whether a point lies inside an obstacle we + // don't need to go through all obstacles, we check only those in a particular cell. var mapGridSize = this.mapGridSize; graph.getElements().reduce(function(map, element) { @@ -14359,19 +21157,20 @@ joint.routers.manhattan = (function(g, _, joint, util) { var isExcluded = isExcludedType || isExcludedEnd || isExcludedAncestor; if (!isExcluded) { - var bBox = element.getBBox().moveAndExpand(opt.paddingBox); + var bbox = element.getBBox().moveAndExpand(opt.paddingBox); - var origin = bBox.origin().snapToGrid(mapGridSize); - var corner = bBox.corner().snapToGrid(mapGridSize); + var origin = bbox.origin().snapToGrid(mapGridSize); + var corner = bbox.corner().snapToGrid(mapGridSize); for (var x = origin.x; x <= corner.x; x += mapGridSize) { for (var y = origin.y; y <= corner.y; y += mapGridSize) { var gridKey = x + '@' + y; map[gridKey] = map[gridKey] || []; - map[gridKey].push(bBox); + map[gridKey].push(bbox); } } } + return map; }, this.map); @@ -14416,154 +21215,363 @@ joint.routers.manhattan = (function(g, _, joint, util) { }; SortedSet.prototype.remove = function(item) { + this.hash[item] = this.CLOSE; }; SortedSet.prototype.isOpen = function(item) { + return this.hash[item] === this.OPEN; }; SortedSet.prototype.isClose = function(item) { + return this.hash[item] === this.CLOSE; }; SortedSet.prototype.isEmpty = function() { + return this.items.length === 0; }; SortedSet.prototype.pop = function() { + var item = this.items.shift(); this.remove(item); return item; }; + // HELPERS // + + // return source bbox + function getSourceBBox(linkView, opt) { + + // expand by padding box + if (opt && opt.paddingBox) return linkView.sourceBBox.clone().moveAndExpand(opt.paddingBox); + + return linkView.sourceBBox.clone(); + } + + // return target bbox + function getTargetBBox(linkView, opt) { + + // expand by padding box + if (opt && opt.paddingBox) return linkView.targetBBox.clone().moveAndExpand(opt.paddingBox); + + return linkView.targetBBox.clone(); + } + + // return source anchor + function getSourceAnchor(linkView, opt) { + + if (linkView.sourceAnchor) return linkView.sourceAnchor; + + // fallback: center of bbox + var sourceBBox = getSourceBBox(linkView, opt); + return sourceBBox.center(); + } + + // return target anchor + function getTargetAnchor(linkView, opt) { + + if (linkView.targetAnchor) return linkView.targetAnchor; + + // fallback: center of bbox + var targetBBox = getTargetBBox(linkView, opt); + return targetBBox.center(); // default + } + + // returns a direction index from start point to end point + // corrects for grid deformation between start and end + function getDirectionAngle(start, end, numDirections, grid, opt) { + + var quadrant = 360 / numDirections; + var angleTheta = start.theta(fixAngleEnd(start, end, grid, opt)); + var normalizedAngle = g.normalizeAngle(angleTheta + (quadrant / 2)); + return quadrant * Math.floor(normalizedAngle / quadrant); + } + + // helper function for getDirectionAngle() + // corrects for grid deformation + // (if a point is one grid steps away from another in both dimensions, + // it is considered to be 45 degrees away, even if the real angle is different) + // this causes visible angle discrepancies if `opt.step` is much larger than `paper.gridSize` + function fixAngleEnd(start, end, grid, opt) { + + var step = opt.step; + + var diffX = end.x - start.x; + var diffY = end.y - start.y; + + var gridStepsX = diffX / grid.x; + var gridStepsY = diffY / grid.y + + var distanceX = gridStepsX * step; + var distanceY = gridStepsY * step; + + return new g.Point(start.x + distanceX, start.y + distanceY); + } + + // return the change in direction between two direction angles + function getDirectionChange(angle1, angle2) { + + var directionChange = Math.abs(angle1 - angle2); + return (directionChange > 180) ? (360 - directionChange) : directionChange; + } + + // fix direction offsets according to current grid + function getGridOffsets(directions, grid, opt) { + + var step = opt.step; + + util.toArray(opt.directions).forEach(function(direction) { + + direction.gridOffsetX = (direction.offsetX / step) * grid.x; + direction.gridOffsetY = (direction.offsetY / step) * grid.y; + }); + } + + // get grid size in x and y dimensions, adapted to source and target positions + function getGrid(step, source, target) { + + return { + source: source.clone(), + x: getGridDimension(target.x - source.x, step), + y: getGridDimension(target.y - source.y, step) + } + } + + // helper function for getGrid() + function getGridDimension(diff, step) { + + // return step if diff = 0 + if (!diff) return step; + + var absDiff = Math.abs(diff); + var numSteps = Math.round(absDiff / step); + + // return absDiff if less than one step apart + if (!numSteps) return absDiff; + + // otherwise, return corrected step + var roundedDiff = numSteps * step; + var remainder = absDiff - roundedDiff; + var stepCorrection = remainder / numSteps; + + return step + stepCorrection; + } + + // return a clone of point snapped to grid + function snapToGrid(point, grid) { + + var source = grid.source; + + var snappedX = g.snapToGrid(point.x - source.x, grid.x) + source.x; + var snappedY = g.snapToGrid(point.y - source.y, grid.y) + source.y; + + return new g.Point(snappedX, snappedY); + } + + // round the point to opt.precision + function round(point, opt) { + + if (!point) return point; + + return point.round(opt.precision); + } + + // return a string representing the point + // string is rounded to nearest int in both dimensions + function getKey(point) { + + return point.clone().round().toString(); + } + + // return a normalized vector from given point + // used to determine the direction of a difference of two points function normalizePoint(point) { - return g.point( + + return new g.Point( point.x === 0 ? 0 : Math.abs(point.x) / point.x, point.y === 0 ? 0 : Math.abs(point.y) / point.y ); } - // reconstructs a route by concating points with their parents - function reconstructRoute(parents, point, startCenter, endCenter) { + // PATHFINDING // + + // reconstructs a route by concatenating points with their parents + function reconstructRoute(parents, points, tailPoint, from, to, opt) { var route = []; - var prevDiff = normalizePoint(endCenter.difference(point)); - var current = point; - var parent; - while ((parent = parents[current])) { + var prevDiff = normalizePoint(to.difference(tailPoint)); - var diff = normalizePoint(current.difference(parent)); + var currentKey = getKey(tailPoint); + var parent = parents[currentKey]; - if (!diff.equals(prevDiff)) { + var point; + while (parent) { - route.unshift(current); + point = round(points[currentKey], opt); + + var diff = normalizePoint(point.difference(round(parent.clone(), opt))); + if (!diff.equals(prevDiff)) { + route.unshift(point); prevDiff = diff; } - current = parent; + currentKey = getKey(parent); + parent = parents[currentKey]; } - var startDiff = normalizePoint(g.point(current).difference(startCenter)); - if (!startDiff.equals(prevDiff)) { - route.unshift(current); + var leadPoint = round(points[currentKey], opt); + + var fromDiff = normalizePoint(leadPoint.difference(from)); + if (!fromDiff.equals(prevDiff)) { + route.unshift(leadPoint); } return route; } - // find points around the rectangle taking given directions in the account - function getRectPoints(bbox, directionList, opt) { + // heuristic method to determine the distance between two points + function estimateCost(from, endPoints) { - var step = opt.step; - var center = bbox.center(); - var keys = util.isObject(opt.directionMap) ? Object.keys(opt.directionMap) : []; - var dirLis = util.toArray(directionList); - return keys.reduce(function(res, key) { + var min = Infinity; + + for (var i = 0, len = endPoints.length; i < len; i++) { + var cost = from.manhattanDistance(endPoints[i]); + if (cost < min) min = cost; + } + + return min; + } - if (dirLis.includes(key)) { + // find points around the bbox taking given directions into account + // lines are drawn from anchor in given directions, intersections recorded + // if anchor is outside bbox, only those directions that intersect get a rect point + // the anchor itself is returned as rect point (representing some directions) + // (since those directions are unobstructed by the bbox) + function getRectPoints(anchor, bbox, directionList, grid, opt) { - var direction = opt.directionMap[key]; + var directionMap = opt.directionMap; - var x = direction.x * bbox.width / 2; - var y = direction.y * bbox.height / 2; + var snappedAnchor = round(snapToGrid(anchor, grid), opt); + var snappedCenter = round(snapToGrid(bbox.center(), grid), opt); + var anchorCenterVector = snappedAnchor.difference(snappedCenter); - var point = center.clone().offset(x, y); + var keys = util.isObject(directionMap) ? Object.keys(directionMap) : []; + var dirList = util.toArray(directionList); + var rectPoints = keys.reduce(function(res, key) { - if (bbox.containsPoint(point)) { + if (dirList.includes(key)) { + var direction = directionMap[key]; - point.offset(direction.x * step, direction.y * step); + // create a line that is guaranteed to intersect the bbox if bbox is in the direction + // even if anchor lies outside of bbox + var endpoint = new g.Point( + snappedAnchor.x + direction.x * (Math.abs(anchorCenterVector.x) + bbox.width), + snappedAnchor.y + direction.y * (Math.abs(anchorCenterVector.y) + bbox.height) + ); + var intersectionLine = new g.Line(anchor, endpoint); + + // get the farther intersection, in case there are two + // (that happens if anchor lies next to bbox) + var intersections = intersectionLine.intersect(bbox) || []; + var numIntersections = intersections.length; + var farthestIntersectionDistance; + var farthestIntersection = null; + for (var i = 0; i < numIntersections; i++) { + var currentIntersection = intersections[i]; + var distance = snappedAnchor.squaredDistance(currentIntersection); + if (farthestIntersectionDistance === undefined || (distance > farthestIntersectionDistance)) { + farthestIntersectionDistance = distance; + farthestIntersection = snapToGrid(currentIntersection, grid); + } } + var point = round(farthestIntersection, opt); + + // if an intersection was found in this direction, it is our rectPoint + if (point) { + // if the rectPoint lies inside the bbox, offset it by one more step + if (bbox.containsPoint(point)) { + round(point.offset(direction.x * grid.x, direction.y * grid.y), opt); + } - res.push(point.snapToGrid(step)); + // then add the point to the result array + res.push(point); + } } - return res; + return res; }, []); - } - // returns a direction index from start point to end point - function getDirectionAngle(start, end, dirLen) { + // if anchor lies outside of bbox, add it to the array of points + if (!bbox.containsPoint(snappedAnchor)) rectPoints.push(snappedAnchor); - var q = 360 / dirLen; - return Math.floor(g.normalizeAngle(start.theta(end) + q / 2) / q) * q; + return rectPoints; } - function getDirectionChange(angle1, angle2) { + // finds the route between two points/rectangles (`from`, `to`) implementing A* algorithm + // rectangles get rect points assigned by getRectPoints() + function findRoute(from, to, map, opt) { - var dirChange = Math.abs(angle1 - angle2); - return dirChange > 180 ? 360 - dirChange : dirChange; - } + // Get grid for this route. - // heurestic method to determine the distance between two points - function estimateCost(from, endPoints) { + var sourceAnchor, targetAnchor; - var min = Infinity; + if (from instanceof g.Rect) { // `from` is sourceBBox + sourceAnchor = getSourceAnchor(this, opt).clone(); + } else { + sourceAnchor = from.clone(); + } - for (var i = 0, len = endPoints.length; i < len; i++) { - var cost = from.manhattanDistance(endPoints[i]); - if (cost < min) min = cost; + if (to instanceof g.Rect) { // `to` is targetBBox + targetAnchor = getTargetAnchor(this, opt).clone(); + } else { + targetAnchor = to.clone(); } - return min; - } + var grid = getGrid(opt.step, sourceAnchor, targetAnchor); - // finds the route between to points/rectangles implementing A* alghoritm - function findRoute(start, end, map, opt) { + // Get pathfinding points. - var step = opt.step; + var start, end; var startPoints, endPoints; - var startCenter, endCenter; // set of points we start pathfinding from - if (start instanceof g.rect) { - startPoints = getRectPoints(start, opt.startDirections, opt); - startCenter = start.center().snapToGrid(step); + if (from instanceof g.Rect) { // `from` is sourceBBox + start = round(snapToGrid(sourceAnchor, grid), opt); + startPoints = getRectPoints(start, from, opt.startDirections, grid, opt); + } else { - startCenter = start.clone().snapToGrid(step); - startPoints = [startCenter]; + start = round(snapToGrid(sourceAnchor, grid), opt); + startPoints = [start]; } // set of points we want the pathfinding to finish at - if (end instanceof g.rect) { - endPoints = getRectPoints(end, opt.endDirections, opt); - endCenter = end.center().snapToGrid(step); + if (to instanceof g.Rect) { // `to` is targetBBox + end = round(snapToGrid(targetAnchor, grid), opt); + endPoints = getRectPoints(targetAnchor, to, opt.endDirections, grid, opt); + } else { - endCenter = end.clone().snapToGrid(step); - endPoints = [endCenter]; + end = round(snapToGrid(targetAnchor, grid), opt); + endPoints = [end]; } - // take into account only accessible end points + // take into account only accessible rect points (those not under obstacles) startPoints = startPoints.filter(map.isPointAccessible, map); endPoints = endPoints.filter(map.isPointAccessible, map); - // Check if there is a accessible end point. - // We would have to use a fallback route otherwise. - if (startPoints.length > 0 && endPoints.length > 0) { + // Check that there is an accessible route point on both sides. + // Otherwise, use fallbackRoute(). + if (startPoints.length > 0 && endPoints.length > 0) { // The set of tentative points to be evaluated, initially containing the start points. + // Rounded to nearest integer for simplicity. var openSet = new SortedSet(); + // Keeps reference to actual points for given elements of the open set. + var points = {}; // Keeps reference to a point that is immediate predecessor of given element. var parents = {}; // Cost from start to a point along best known path. @@ -14572,78 +21580,109 @@ joint.routers.manhattan = (function(g, _, joint, util) { for (var i = 0, n = startPoints.length; i < n; i++) { var point = startPoints[i]; - var key = point.toString(); + var key = getKey(point); openSet.add(key, estimateCost(point, endPoints)); + points[key] = point; costs[key] = 0; } + var previousRouteDirectionAngle = opt.previousDirectionAngle; // undefined for first route + var isPathBeginning = (previousRouteDirectionAngle === undefined); + // directions - var dir, dirChange; - var dirs = opt.directions; - var dirLen = dirs.length; - var loopsRemain = opt.maximumLoops; - var endPointsKeys = util.invoke(endPoints, 'toString'); + var direction, directionChange; + var directions = opt.directions; + getGridOffsets(directions, grid, opt); + + var numDirections = directions.length; + + var endPointsKeys = util.toArray(endPoints).reduce(function(res, endPoint) { + + var key = getKey(endPoint); + res.push(key); + return res; + }, []); // main route finding loop - while (!openSet.isEmpty() && loopsRemain > 0) { + var loopsRemaining = opt.maximumLoops; + while (!openSet.isEmpty() && loopsRemaining > 0) { // remove current from the open list var currentKey = openSet.pop(); - var currentPoint = g.point(currentKey); - var currentDist = costs[currentKey]; - var previousDirAngle = currentDirAngle; - var currentDirAngle = parents[currentKey] - ? getDirectionAngle(parents[currentKey], currentPoint, dirLen) - : opt.previousDirAngle != null ? opt.previousDirAngle : getDirectionAngle(startCenter, currentPoint, dirLen); - - // Check if we reached any endpoint + var currentPoint = points[currentKey]; + var currentParent = parents[currentKey]; + var currentCost = costs[currentKey]; + + var isRouteBeginning = (currentParent === undefined); // undefined for route starts + var isStart = currentPoint.equals(start); // (is source anchor or `from` point) = can leave in any direction + + var previousDirectionAngle; + if (!isRouteBeginning) previousDirectionAngle = getDirectionAngle(currentParent, currentPoint, numDirections, grid, opt); // a vertex on the route + else if (!isPathBeginning) previousDirectionAngle = previousRouteDirectionAngle; // beginning of route on the path + else if (!isStart) previousDirectionAngle = getDirectionAngle(start, currentPoint, numDirections, grid, opt); // beginning of path, start rect point + else previousDirectionAngle = null; // beginning of path, source anchor or `from` point + + // check if we reached any endpoint if (endPointsKeys.indexOf(currentKey) >= 0) { - // We don't want to allow route to enter the end point in opposite direction. - dirChange = getDirectionChange(currentDirAngle, getDirectionAngle(currentPoint, endCenter, dirLen)); - if (currentPoint.equals(endCenter) || dirChange < 180) { - opt.previousDirAngle = currentDirAngle; - return reconstructRoute(parents, currentPoint, startCenter, endCenter); - } + opt.previousDirectionAngle = previousDirectionAngle; + return reconstructRoute(parents, points, currentPoint, start, end, opt); } - // Go over all possible directions and find neighbors. - for (i = 0; i < dirLen; i++) { + // go over all possible directions and find neighbors + for (i = 0; i < numDirections; i++) { + direction = directions[i]; + + var directionAngle = direction.angle; + directionChange = getDirectionChange(previousDirectionAngle, directionAngle); - dir = dirs[i]; - dirChange = getDirectionChange(currentDirAngle, dir.angle); - // if the direction changed rapidly don't use this point - // Note that check is relevant only for points with previousDirAngle i.e. + // if the direction changed rapidly, don't use this point // any direction is allowed for starting points - if (previousDirAngle && dirChange > opt.maxAllowedDirectionChange) { - continue; - } + if (!(isPathBeginning && isStart) && directionChange > opt.maxAllowedDirectionChange) continue; + + var neighborPoint = currentPoint.clone().offset(direction.gridOffsetX, direction.gridOffsetY); + var neighborKey = getKey(neighborPoint); - var neighborPoint = currentPoint.clone().offset(dir.offsetX, dir.offsetY); - var neighborKey = neighborPoint.toString(); // Closed points from the openSet were already evaluated. - if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) { - continue; + if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) continue; + + // We can only enter end points at an acceptable angle. + if (endPointsKeys.indexOf(neighborKey) >= 0) { // neighbor is an end point + round(neighborPoint, opt); // remove rounding errors + + var isNeighborEnd = neighborPoint.equals(end); // (is target anchor or `to` point) = can be entered in any direction + + if (!isNeighborEnd) { + var endDirectionAngle = getDirectionAngle(neighborPoint, end, numDirections, grid, opt); + var endDirectionChange = getDirectionChange(directionAngle, endDirectionAngle); + + if (endDirectionChange > opt.maxAllowedDirectionChange) continue; + } } - // The current direction is ok to proccess. - var costFromStart = currentDist + dir.cost + opt.penalties[dirChange]; + // The current direction is ok. + + var neighborCost = direction.cost; + var neighborPenalty = isStart ? 0 : opt.penalties[directionChange]; // no penalties for start point + var costFromStart = currentCost + neighborCost + neighborPenalty; - if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) { - // neighbor point has not been processed yet or the cost of the path - // from start is lesser than previously calcluated. + if (!openSet.isOpen(neighborKey) || (costFromStart < costs[neighborKey])) { + // neighbor point has not been processed yet + // or the cost of the path from start is lower than previously calculated + + points[neighborKey] = neighborPoint; parents[neighborKey] = currentPoint; costs[neighborKey] = costFromStart; openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints)); } } - loopsRemain--; + loopsRemaining--; } } - // no route found ('to' point wasn't either accessible or finding route took - // way to much calculations) - return opt.fallbackRoute(startCenter, endCenter, opt); + // no route found (`to` point either wasn't accessible or finding route took + // way too much calculation) + return opt.fallbackRoute.call(this, start, end, opt); } // resolve some of the options @@ -14655,33 +21694,35 @@ joint.routers.manhattan = (function(g, _, joint, util) { util.toArray(opt.directions).forEach(function(direction) { - var point1 = g.point(0, 0); - var point2 = g.point(direction.offsetX, direction.offsetY); + var point1 = new g.Point(0, 0); + var point2 = new g.Point(direction.offsetX, direction.offsetY); direction.angle = g.normalizeAngle(point1.theta(point2)); }); } - // initiation of the route finding - function router(vertices, opt) { + // initialization of the route finding + function router(vertices, opt, linkView) { resolveOptions(opt); // enable/disable linkView perpendicular option - this.options.perpendicular = !!opt.perpendicular; + linkView.options.perpendicular = !!opt.perpendicular; + + var sourceBBox = getSourceBBox(linkView, opt); + var targetBBox = getTargetBBox(linkView, opt); - // expand boxes by specific padding - var sourceBBox = g.rect(this.sourceBBox).moveAndExpand(opt.paddingBox); - var targetBBox = g.rect(this.targetBBox).moveAndExpand(opt.paddingBox); + var sourceAnchor = getSourceAnchor(linkView, opt); + //var targetAnchor = getTargetAnchor(linkView, opt); // pathfinding - var map = (new ObstacleMap(opt)).build(this.paper.model, this.model); - var oldVertices = util.toArray(vertices).map(g.point); + var map = (new ObstacleMap(opt)).build(linkView.paper.model, linkView.model); + var oldVertices = util.toArray(vertices).map(g.Point); var newVertices = []; - var tailPoint = sourceBBox.center().snapToGrid(opt.step); + var tailPoint = sourceAnchor; // the origin of first route's grid, does not need snapping - // find a route by concating all partial routes (routes need to go through the vertices) - // startElement -> vertex[1] -> ... -> vertex[n] -> endElement + // find a route by concatenating all partial routes (routes need to pass through vertices) + // source -> vertex[1] -> ... -> vertex[n] -> target for (var i = 0, len = oldVertices.length; i <= len; i++) { var partialRoute = null; @@ -14690,39 +21731,38 @@ joint.routers.manhattan = (function(g, _, joint, util) { var to = oldVertices[i]; if (!to) { + // this is the last iteration + // we ran through all vertices in oldVertices + // 'to' is not a vertex. to = targetBBox; - // 'to' is not a vertex. If the target is a point (i.e. it's not an element), we - // might use dragging route instead of main routing method if that is enabled. - var endingAtPoint = !this.model.get('source').id || !this.model.get('target').id; + // If the target is a point (i.e. it's not an element), we + // should use dragging route instead of main routing method if it has been provided. + var isEndingAtPoint = !linkView.model.get('source').id || !linkView.model.get('target').id; - if (endingAtPoint && util.isFunction(opt.draggingRoute)) { - // Make sure we passing points only (not rects). - var dragFrom = from instanceof g.rect ? from.center() : from; - partialRoute = opt.draggingRoute(dragFrom, to.origin(), opt); + if (isEndingAtPoint && util.isFunction(opt.draggingRoute)) { + // Make sure we are passing points only (not rects). + var dragFrom = (from === sourceBBox) ? sourceAnchor : from; + var dragTo = to.origin(); + + partialRoute = opt.draggingRoute.call(linkView, dragFrom, dragTo, opt); } } // if partial route has not been calculated yet use the main routing method to find one - partialRoute = partialRoute || findRoute(from, to, map, opt); + partialRoute = partialRoute || findRoute.call(linkView, from, to, map, opt); - if (partialRoute === null) { - // The partial route could not be found. - // use orthogonal (do not avoid elements) route instead. - if (!util.isFunction(joint.routers.orthogonal)) { - throw new Error('Manhattan requires the orthogonal router.'); - } - return joint.routers.orthogonal(vertices, opt, this); + if (partialRoute === null) { // the partial route cannot be found + return opt.fallbackRouter(vertices, opt, linkView); } var leadPoint = partialRoute[0]; - if (leadPoint && leadPoint.equals(tailPoint)) { - // remove the first point if the previous partial route had the same point as last - partialRoute.shift(); - } + // remove the first point if the previous partial route had the same point as last + if (leadPoint && leadPoint.equals(tailPoint)) partialRoute.shift(); + // save tailPoint for next iteration tailPoint = partialRoute[partialRoute.length - 1] || tailPoint; Array.prototype.push.apply(newVertices, partialRoute); @@ -14734,49 +21774,54 @@ joint.routers.manhattan = (function(g, _, joint, util) { // public function return function(vertices, opt, linkView) { - return router.call(linkView, vertices, util.assign({}, config, opt)); + return router(vertices, util.assign({}, config, opt), linkView); }; })(g, _, joint, joint.util); joint.routers.metro = (function(util) { - if (!util.isFunction(joint.routers.manhattan)) { + var config = { - throw new Error('Metro requires the manhattan router.'); - } + maxAllowedDirectionChange: 45, - var config = { + // cost of a diagonal step + diagonalCost: function() { - // cost of a diagonal step (calculated if not defined). - diagonalCost: null, + var step = this.step; + return Math.ceil(Math.sqrt(step * step << 1)); + }, // an array of directions to find next points on the route + // different from start/end directions directions: function() { var step = this.step; - var diagonalCost = this.diagonalCost || Math.ceil(Math.sqrt(step * step << 1)); + var cost = this.cost(); + var diagonalCost = this.diagonalCost(); return [ - { offsetX: step , offsetY: 0 , cost: step }, + { offsetX: step , offsetY: 0 , cost: cost }, { offsetX: step , offsetY: step , cost: diagonalCost }, - { offsetX: 0 , offsetY: step , cost: step }, + { offsetX: 0 , offsetY: step , cost: cost }, { offsetX: -step , offsetY: step , cost: diagonalCost }, - { offsetX: -step , offsetY: 0 , cost: step }, + { offsetX: -step , offsetY: 0 , cost: cost }, { offsetX: -step , offsetY: -step , cost: diagonalCost }, - { offsetX: 0 , offsetY: -step , cost: step }, + { offsetX: 0 , offsetY: -step , cost: cost }, { offsetX: step , offsetY: -step , cost: diagonalCost } ]; }, - maxAllowedDirectionChange: 45, - // a simple route used in situations, when main routing method fails - // (exceed loops, inaccessible). - fallbackRoute: function(from, to, opts) { + + // a simple route used in situations when main routing method fails + // (exceed max number of loop iterations, inaccessible) + fallbackRoute: function(from, to, opt) { // Find a route which breaks by 45 degrees ignoring all obstacles. var theta = from.theta(to); + var route = []; + var a = { x: to.x, y: from.y }; var b = { x: from.x, y: to.y }; @@ -14787,25 +21832,40 @@ joint.routers.metro = (function(util) { } var p1 = (theta % 90) < 45 ? a : b; - - var l1 = g.line(from, p1); + var l1 = new g.Line(from, p1); var alpha = 90 * Math.ceil(theta / 90); - var p2 = g.point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); + var p2 = g.Point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); + var l2 = new g.Line(to, p2); + + var intersectionPoint = l1.intersection(l2); + var point = intersectionPoint ? intersectionPoint : to; + + var directionFrom = intersectionPoint ? point : from; + + var quadrant = 360 / opt.directions.length; + var angleTheta = directionFrom.theta(to); + var normalizedAngle = g.normalizeAngle(angleTheta + (quadrant / 2)); + var directionAngle = quadrant * Math.floor(normalizedAngle / quadrant); - var l2 = g.line(to, p2); + opt.previousDirectionAngle = directionAngle; - var point = l1.intersection(l2); + if (point) route.push(point.round()); + route.push(to); - return point ? [point.round(), to] : [to]; + return route; } }; // public function - return function(vertices, opts, linkView) { + return function(vertices, opt, linkView) { + + if (!util.isFunction(joint.routers.manhattan)) { + throw new Error('Metro requires the manhattan router.'); + } - return joint.routers.manhattan(vertices, util.assign({}, config, opts), linkView); + return joint.routers.manhattan(vertices, util.assign({}, config, opt), linkView); }; })(joint.util); @@ -14879,7 +21939,7 @@ joint.routers.oneSide = function(vertices, opt, linkView) { joint.routers.orthogonal = (function(util) { // bearing -> opposite bearing - var opposite = { + var opposites = { N: 'S', S: 'N', E: 'W', @@ -14896,105 +21956,126 @@ joint.routers.orthogonal = (function(util) { // HELPERS // - // simple bearing method (calculates only orthogonal cardinals) - function bearing(from, to) { - if (from.x == to.x) return from.y > to.y ? 'N' : 'S'; - if (from.y == to.y) return from.x > to.x ? 'W' : 'E'; - return null; + // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained + // in the given box + function freeJoin(p1, p2, bbox) { + + var p = new g.Point(p1.x, p2.y); + if (bbox.containsPoint(p)) p = new g.Point(p2.x, p1.y); + // kept for reference + // if (bbox.containsPoint(p)) p = null; + + return p; } // returns either width or height of a bbox based on the given bearing - function boxSize(bbox, brng) { - return bbox[brng == 'W' || brng == 'E' ? 'width' : 'height']; + function getBBoxSize(bbox, bearing) { + + return bbox[(bearing === 'W' || bearing === 'E') ? 'width' : 'height']; } - // expands a box by specific value - function expand(bbox, val) { - return g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val }); + // simple bearing method (calculates only orthogonal cardinals) + function getBearing(from, to) { + + if (from.x === to.x) return (from.y > to.y) ? 'N' : 'S'; + if (from.y === to.y) return (from.x > to.x) ? 'W' : 'E'; + return null; } // transform point to a rect - function pointBox(p) { - return g.rect(p.x, p.y, 0, 0); + function getPointBox(p) { + + return new g.Rect(p.x, p.y, 0, 0); } - // returns a minimal rect which covers the given boxes - function boundary(bbox1, bbox2) { + // return source bbox + function getSourceBBox(linkView, opt) { + + var padding = (opt && opt.elementPadding) || 20; + return linkView.sourceBBox.clone().inflate(padding); + } - var x1 = Math.min(bbox1.x, bbox2.x); - var y1 = Math.min(bbox1.y, bbox2.y); - var x2 = Math.max(bbox1.x + bbox1.width, bbox2.x + bbox2.width); - var y2 = Math.max(bbox1.y + bbox1.height, bbox2.y + bbox2.height); + // return target bbox + function getTargetBBox(linkView, opt) { - return g.rect(x1, y1, x2 - x1, y2 - y1); + var padding = (opt && opt.elementPadding) || 20; + return linkView.targetBBox.clone().inflate(padding); } - // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained - // in the given box - function freeJoin(p1, p2, bbox) { + // return source anchor + function getSourceAnchor(linkView, opt) { - var p = g.point(p1.x, p2.y); - if (bbox.containsPoint(p)) p = g.point(p2.x, p1.y); - // kept for reference - // if (bbox.containsPoint(p)) p = null; - return p; + if (linkView.sourceAnchor) return linkView.sourceAnchor; + + // fallback: center of bbox + var sourceBBox = getSourceBBox(linkView, opt); + return sourceBBox.center(); + } + + // return target anchor + function getTargetAnchor(linkView, opt) { + + if (linkView.targetAnchor) return linkView.targetAnchor; + + // fallback: center of bbox + var targetBBox = getTargetBBox(linkView, opt); + return targetBBox.center(); // default } // PARTIAL ROUTERS // - function vertexVertex(from, to, brng) { + function vertexVertex(from, to, bearing) { - var p1 = g.point(from.x, to.y); - var p2 = g.point(to.x, from.y); - var d1 = bearing(from, p1); - var d2 = bearing(from, p2); - var xBrng = opposite[brng]; + var p1 = new g.Point(from.x, to.y); + var p2 = new g.Point(to.x, from.y); + var d1 = getBearing(from, p1); + var d2 = getBearing(from, p2); + var opposite = opposites[bearing]; - var p = (d1 == brng || (d1 != xBrng && (d2 == xBrng || d2 != brng))) ? p1 : p2; + var p = (d1 === bearing || (d1 !== opposite && (d2 === opposite || d2 !== bearing))) ? p1 : p2; - return { points: [p], direction: bearing(p, to) }; + return { points: [p], direction: getBearing(p, to) }; } function elementVertex(from, to, fromBBox) { var p = freeJoin(from, to, fromBBox); - return { points: [p], direction: bearing(p, to) }; + return { points: [p], direction: getBearing(p, to) }; } - function vertexElement(from, to, toBBox, brng) { + function vertexElement(from, to, toBBox, bearing) { var route = {}; - var pts = [g.point(from.x, to.y), g.point(to.x, from.y)]; - var freePts = pts.filter(function(pt) { return !toBBox.containsPoint(pt); }); - var freeBrngPts = freePts.filter(function(pt) { return bearing(pt, from) != brng; }); + var points = [new g.Point(from.x, to.y), new g.Point(to.x, from.y)]; + var freePoints = points.filter(function(pt) { return !toBBox.containsPoint(pt); }); + var freeBearingPoints = freePoints.filter(function(pt) { return getBearing(pt, from) !== bearing; }); var p; - if (freeBrngPts.length > 0) { + if (freeBearingPoints.length > 0) { + // Try to pick a point which bears the same direction as the previous segment. - // try to pick a point which bears the same direction as the previous segment - p = freeBrngPts.filter(function(pt) { return bearing(from, pt) == brng; }).pop(); - p = p || freeBrngPts[0]; + p = freeBearingPoints.filter(function(pt) { return getBearing(from, pt) === bearing; }).pop(); + p = p || freeBearingPoints[0]; route.points = [p]; - route.direction = bearing(p, to); + route.direction = getBearing(p, to); } else { - // Here we found only points which are either contained in the element or they would create // a link segment going in opposite direction from the previous one. // We take the point inside element and move it outside the element in the direction the // route is going. Now we can join this point with the current end (using freeJoin). - p = util.difference(pts, freePts)[0]; + p = util.difference(points, freePoints)[0]; - var p2 = g.point(to).move(p, -boxSize(toBBox, brng) / 2); + var p2 = (new g.Point(to)).move(p, -getBBoxSize(toBBox, bearing) / 2); var p1 = freeJoin(p2, from, toBBox); route.points = [p1, p2]; - route.direction = bearing(p2, to); + route.direction = getBearing(p2, to); } return route; @@ -15012,9 +22093,9 @@ joint.routers.orthogonal = (function(util) { if (toBBox.containsPoint(p2)) { - var fromBorder = g.point(from).move(p2, -boxSize(fromBBox, bearing(from, p2)) / 2); - var toBorder = g.point(to).move(p1, -boxSize(toBBox, bearing(to, p1)) / 2); - var mid = g.line(fromBorder, toBorder).midpoint(); + var fromBorder = (new g.Point(from)).move(p2, -getBBoxSize(fromBBox, getBearing(from, p2)) / 2); + var toBorder = (new g.Point(to)).move(p1, -getBBoxSize(toBBox, getBearing(to, p1)) / 2); + var mid = (new g.Line(fromBorder, toBorder)).midpoint(); var startRoute = elementVertex(from, mid, fromBBox); var endRoute = vertexVertex(mid, to, startRoute.direction); @@ -15027,79 +22108,91 @@ joint.routers.orthogonal = (function(util) { return route; } - // Finds route for situations where one of end is inside the other. - // Typically the route is conduct outside the outer element first and - // let go back to the inner element. - function insideElement(from, to, fromBBox, toBBox, brng) { + // Finds route for situations where one element is inside the other. + // Typically the route is directed outside the outer element first and + // then back towards the inner element. + function insideElement(from, to, fromBBox, toBBox, bearing) { var route = {}; - var bndry = expand(boundary(fromBBox, toBBox), 1); + var boundary = fromBBox.union(toBBox).inflate(1); // start from the point which is closer to the boundary - var reversed = bndry.center().distance(to) > bndry.center().distance(from); + var reversed = boundary.center().distance(to) > boundary.center().distance(from); var start = reversed ? to : from; var end = reversed ? from : to; var p1, p2, p3; - if (brng) { + if (bearing) { // Points on circle with radius equals 'W + H` are always outside the rectangle // with width W and height H if the center of that circle is the center of that rectangle. - p1 = g.point.fromPolar(bndry.width + bndry.height, radians[brng], start); - p1 = bndry.pointNearestToPoint(p1).move(p1, -1); + p1 = g.Point.fromPolar(boundary.width + boundary.height, radians[bearing], start); + p1 = boundary.pointNearestToPoint(p1).move(p1, -1); + } else { - p1 = bndry.pointNearestToPoint(start).move(start, 1); + p1 = boundary.pointNearestToPoint(start).move(start, 1); } - p2 = freeJoin(p1, end, bndry); + p2 = freeJoin(p1, end, boundary); if (p1.round().equals(p2.round())) { - p2 = g.point.fromPolar(bndry.width + bndry.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); - p2 = bndry.pointNearestToPoint(p2).move(end, 1).round(); - p3 = freeJoin(p1, p2, bndry); + p2 = g.Point.fromPolar(boundary.width + boundary.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); + p2 = boundary.pointNearestToPoint(p2).move(end, 1).round(); + p3 = freeJoin(p1, p2, boundary); route.points = reversed ? [p2, p3, p1] : [p1, p3, p2]; + } else { route.points = reversed ? [p2, p1] : [p1, p2]; } - route.direction = reversed ? bearing(p1, to) : bearing(p2, to); + route.direction = reversed ? getBearing(p1, to) : getBearing(p2, to); return route; } // MAIN ROUTER // - // Return points that one needs to draw a connection through in order to have a orthogonal link + // Return points through which a connection needs to be drawn in order to obtain an orthogonal link // routing from source to target going through `vertices`. - function findOrthogonalRoute(vertices, opt, linkView) { + function router(vertices, opt, linkView) { var padding = opt.elementPadding || 20; - var orthogonalVertices = []; - var sourceBBox = expand(linkView.sourceBBox, padding); - var targetBBox = expand(linkView.targetBBox, padding); + var sourceBBox = getSourceBBox(linkView, opt); + var targetBBox = getTargetBBox(linkView, opt); + + var sourceAnchor = getSourceAnchor(linkView, opt); + var targetAnchor = getTargetAnchor(linkView, opt); + + // if anchor lies outside of bbox, the bbox expands to include it + sourceBBox = sourceBBox.union(getPointBox(sourceAnchor)); + targetBBox = targetBBox.union(getPointBox(targetAnchor)); - vertices = util.toArray(vertices).map(g.point); - vertices.unshift(sourceBBox.center()); - vertices.push(targetBBox.center()); + vertices = util.toArray(vertices).map(g.Point); + vertices.unshift(sourceAnchor); + vertices.push(targetAnchor); - var brng; + var bearing; // bearing of previous route segment + var orthogonalVertices = []; // the array of found orthogonal vertices to be returned for (var i = 0, max = vertices.length - 1; i < max; i++) { var route = null; + var from = vertices[i]; var to = vertices[i + 1]; - var isOrthogonal = !!bearing(from, to); - if (i == 0) { + var isOrthogonal = !!getBearing(from, to); + + if (i === 0) { // source - if (i + 1 == max) { // route source -> target + if (i + 1 === max) { // route source -> target - // Expand one of elements by 1px so we detect also situations when they - // are positioned one next other with no gap between. - if (sourceBBox.intersect(expand(targetBBox, 1))) { + // Expand one of the elements by 1px to detect situations when the two + // elements are positioned next to each other with no gap in between. + if (sourceBBox.intersect(targetBBox.clone().inflate(1))) { route = insideElement(from, to, sourceBBox, targetBBox); + } else if (!isOrthogonal) { route = elementElement(from, to, sourceBBox, targetBBox); } @@ -15107,34 +22200,42 @@ joint.routers.orthogonal = (function(util) { } else { // route source -> vertex if (sourceBBox.containsPoint(to)) { - route = insideElement(from, to, sourceBBox, expand(pointBox(to), padding)); + route = insideElement(from, to, sourceBBox, getPointBox(to).inflate(padding)); + } else if (!isOrthogonal) { route = elementVertex(from, to, sourceBBox); } } - } else if (i + 1 == max) { // route vertex -> target + } else if (i + 1 === max) { // route vertex -> target - var orthogonalLoop = isOrthogonal && bearing(to, from) == brng; + // prevent overlaps with previous line segment + var isOrthogonalLoop = isOrthogonal && getBearing(to, from) === bearing; + + if (targetBBox.containsPoint(from) || isOrthogonalLoop) { + route = insideElement(from, to, getPointBox(from).inflate(padding), targetBBox, bearing); - if (targetBBox.containsPoint(from) || orthogonalLoop) { - route = insideElement(from, to, expand(pointBox(from), padding), targetBBox, brng); } else if (!isOrthogonal) { - route = vertexElement(from, to, targetBBox, brng); + route = vertexElement(from, to, targetBBox, bearing); } } else if (!isOrthogonal) { // route vertex -> vertex - route = vertexVertex(from, to, brng); + route = vertexVertex(from, to, bearing); } + // applicable to all routes: + + // set bearing for next iteration if (route) { Array.prototype.push.apply(orthogonalVertices, route.points); - brng = route.direction; + bearing = route.direction; + } else { // orthogonal route and not looped - brng = bearing(from, to); + bearing = getBearing(from, to); } + // push `to` point to identified orthogonal vertices array if (i + 1 < max) { orthogonalVertices.push(to); } @@ -15143,81 +22244,116 @@ joint.routers.orthogonal = (function(util) { return orthogonalVertices; } - return findOrthogonalRoute; + return router; })(joint.util); -joint.connectors.normal = function(sourcePoint, targetPoint, vertices) { +joint.connectors.normal = function(sourcePoint, targetPoint, route, opt) { - // Construct the `d` attribute of the `` element. - var d = ['M', sourcePoint.x, sourcePoint.y]; + var raw = opt && opt.raw; + var points = [sourcePoint].concat(route).concat([targetPoint]); - joint.util.toArray(vertices).forEach(function(vertex) { + var polyline = new g.Polyline(points); + var path = new g.Path(polyline); - d.push(vertex.x, vertex.y); - }); + return (raw) ? path : path.serialize(); +}; - d.push(targetPoint.x, targetPoint.y); +joint.connectors.rounded = function(sourcePoint, targetPoint, route, opt) { - return d.join(' '); -}; + opt || (opt = {}); -joint.connectors.rounded = function(sourcePoint, targetPoint, vertices, opts) { + var offset = opt.radius || 10; + var raw = opt.raw; + var path = new g.Path(); + var segment; - opts = opts || {}; + segment = g.Path.createSegment('M', sourcePoint); + path.appendSegment(segment); - var offset = opts.radius || 10; + var _13 = 1 / 3; + var _23 = 2 / 3; - var c1, c2, d1, d2, prev, next; + var curr; + var prev, next; + var prevDistance, nextDistance; + var startMove, endMove; + var roundedStart, roundedEnd; + var control1, control2; - // Construct the `d` attribute of the `` element. - var d = ['M', sourcePoint.x, sourcePoint.y]; + for (var index = 0, n = route.length; index < n; index++) { - joint.util.toArray(vertices).forEach(function(vertex, index) { + curr = new g.Point(route[index]); - // the closest vertices - prev = vertices[index - 1] || sourcePoint; - next = vertices[index + 1] || targetPoint; + prev = route[index - 1] || sourcePoint; + next = route[index + 1] || targetPoint; - // a half distance to the closest vertex - d1 = d2 || g.point(vertex).distance(prev) / 2; - d2 = g.point(vertex).distance(next) / 2; + prevDistance = nextDistance || (curr.distance(prev) / 2); + nextDistance = curr.distance(next) / 2; - // control points - c1 = g.point(vertex).move(prev, -Math.min(offset, d1)).round(); - c2 = g.point(vertex).move(next, -Math.min(offset, d2)).round(); + startMove = -Math.min(offset, prevDistance); + endMove = -Math.min(offset, nextDistance); - d.push(c1.x, c1.y, 'S', vertex.x, vertex.y, c2.x, c2.y, 'L'); - }); + roundedStart = curr.clone().move(prev, startMove).round(); + roundedEnd = curr.clone().move(next, endMove).round(); + + control1 = new g.Point((_13 * roundedStart.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedStart.y)); + control2 = new g.Point((_13 * roundedEnd.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedEnd.y)); - d.push(targetPoint.x, targetPoint.y); + segment = g.Path.createSegment('L', roundedStart); + path.appendSegment(segment); - return d.join(' '); + segment = g.Path.createSegment('C', control1, control2, roundedEnd); + path.appendSegment(segment); + } + + segment = g.Path.createSegment('L', targetPoint); + path.appendSegment(segment); + + return (raw) ? path : path.serialize(); }; -joint.connectors.smooth = function(sourcePoint, targetPoint, vertices) { +joint.connectors.smooth = function(sourcePoint, targetPoint, route, opt) { + + var raw = opt && opt.raw; + var path; - var d; + if (route && route.length !== 0) { - if (vertices.length) { + var points = [sourcePoint].concat(route).concat([targetPoint]); + var curves = g.Curve.throughPoints(points); - d = g.bezier.curveThroughPoints([sourcePoint].concat(vertices).concat([targetPoint])); + path = new g.Path(curves); } else { - // if we have no vertices use a default cubic bezier curve, cubic bezier requires - // two control points. The two control points are both defined with X as mid way - // between the source and target points. SourceControlPoint Y is equal to sourcePoint Y - // and targetControlPointY being equal to targetPointY. - var controlPointX = (sourcePoint.x + targetPoint.x) / 2; - - d = [ - 'M', sourcePoint.x, sourcePoint.y, - 'C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, - targetPoint.x, targetPoint.y - ]; + // if we have no route, use a default cubic bezier curve + // cubic bezier requires two control points + // the control points have `x` midway between source and target + // this produces an S-like curve + + path = new g.Path(); + + var segment; + + segment = g.Path.createSegment('M', sourcePoint); + path.appendSegment(segment); + + if ((Math.abs(sourcePoint.x - targetPoint.x)) >= (Math.abs(sourcePoint.y - targetPoint.y))) { + var controlPointX = (sourcePoint.x + targetPoint.x) / 2; + + segment = g.Path.createSegment('C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, targetPoint.x, targetPoint.y); + path.appendSegment(segment); + + } else { + var controlPointY = (sourcePoint.y + targetPoint.y) / 2; + + segment = g.Path.createSegment('C', sourcePoint.x, controlPointY, targetPoint.x, controlPointY, targetPoint.x, targetPoint.y); + path.appendSegment(segment); + + } } - return d.join(' '); + return (raw) ? path : path.serialize(); }; joint.connectors.jumpover = (function(_, g, util) { @@ -15226,6 +22362,7 @@ joint.connectors.jumpover = (function(_, g, util) { var JUMP_SIZE = 5; // available jump types + // first one taken as default var JUMP_TYPES = ['arc', 'gap', 'cubic']; // takes care of math. error for case when jump is too close to end of line @@ -15235,15 +22372,15 @@ joint.connectors.jumpover = (function(_, g, util) { var IGNORED_CONNECTORS = ['smooth']; /** - * Transform start/end and vertices into series of lines + * Transform start/end and route into series of lines * @param {g.point} sourcePoint start point * @param {g.point} targetPoint end point - * @param {g.point[]} vertices optional list of vertices + * @param {g.point[]} route optional list of route * @return {g.line[]} [description] */ - function createLines(sourcePoint, targetPoint, vertices) { + function createLines(sourcePoint, targetPoint, route) { // make a flattened array of all points - var points = [].concat(sourcePoint, vertices, targetPoint); + var points = [].concat(sourcePoint, route, targetPoint); return points.reduce(function(resultLines, point, idx) { // if there is a next point, make a line with it var nextPoint = points[idx + 1]; @@ -15388,60 +22525,102 @@ joint.connectors.jumpover = (function(_, g, util) { * @return {string} */ function buildPath(lines, jumpSize, jumpType) { + + var path = new g.Path(); + var segment; + // first move to the start of a first line - var start = ['M', lines[0].start.x, lines[0].start.y]; + segment = g.Path.createSegment('M', lines[0].start); + path.appendSegment(segment); // make a paths from lines - var paths = util.toArray(lines).reduce(function(res, line) { + joint.util.toArray(lines).forEach(function(line, index) { + if (line.isJump) { - var diff; - if (jumpType === 'arc') { - diff = line.start.difference(line.end); + var angle, diff; + + var control1, control2; + + if (jumpType === 'arc') { // approximates semicircle with 2 curves + angle = -90; // determine rotation of arc based on difference between points - var xAxisRotate = Number(diff.x < 0 && diff.y < 0); - // for a jump line we create an arc instead - res.push('A', jumpSize, jumpSize, 0, 0, xAxisRotate, line.end.x, line.end.y); + diff = line.start.difference(line.end); + // make sure the arc always points up (or right) + var xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0)); + if (xAxisRotate) angle += 180; + + var midpoint = line.midpoint(); + var centerLine = new g.Line(midpoint, line.end).rotate(midpoint, angle); + + var halfLine; + + // first half + halfLine = new g.Line(line.start, midpoint); + + control1 = halfLine.pointAt(2 / 3).rotate(line.start, angle); + control2 = centerLine.pointAt(1 / 3).rotate(centerLine.end, -angle); + + segment = g.Path.createSegment('C', control1, control2, centerLine.end); + path.appendSegment(segment); + + // second half + halfLine = new g.Line(midpoint, line.end); + + control1 = centerLine.pointAt(1 / 3).rotate(centerLine.end, angle); + control2 = halfLine.pointAt(1 / 3).rotate(line.end, -angle); + + segment = g.Path.createSegment('C', control1, control2, line.end); + path.appendSegment(segment); + } else if (jumpType === 'gap') { - res = res.concat(['M', line.end.x, line.end.y]); - } else if (jumpType === 'cubic') { - diff = line.start.difference(line.end); - var angle = line.start.theta(line.end); + segment = g.Path.createSegment('M', line.end); + path.appendSegment(segment); + + } else if (jumpType === 'cubic') { // approximates semicircle with 1 curve + angle = line.start.theta(line.end); + var xOffset = jumpSize * 0.6; var yOffset = jumpSize * 1.35; - // determine rotation of curve based on difference between points - if (diff.x < 0 && diff.y < 0) { - yOffset *= -1; - } - var controlStartPoint = g.point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); - var controlEndPoint = g.point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); - // create a cubic bezier curve - res.push('C', controlStartPoint.x, controlStartPoint.y, controlEndPoint.x, controlEndPoint.y, line.end.x, line.end.y); + + // determine rotation of arc based on difference between points + diff = line.start.difference(line.end); + // make sure the arc always points up (or right) + xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0)); + if (xAxisRotate) yOffset *= -1; + + control1 = g.Point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); + control2 = g.Point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); + + segment = g.Path.createSegment('C', control1, control2, line.end); + path.appendSegment(segment); } + } else { - res.push('L', line.end.x, line.end.y); + segment = g.Path.createSegment('L', line.end); + path.appendSegment(segment); } - return res; - }, start); + }); - return paths.join(' '); + return path; } /** * Actual connector function that will be run on every update. * @param {g.point} sourcePoint start point of this link * @param {g.point} targetPoint end point of this link - * @param {g.point[]} vertices of this link - * @param {object} opts options + * @param {g.point[]} route of this link + * @param {object} opt options * @property {number} size optional size of a jump arc * @return {string} created `D` attribute of SVG path */ - return function(sourcePoint, targetPoint, vertices, opts) { // eslint-disable-line max-params + return function(sourcePoint, targetPoint, route, opt) { // eslint-disable-line max-params setupUpdating(this); - var jumpSize = opts.size || JUMP_SIZE; - var jumpType = opts.jump && ('' + opts.jump).toLowerCase(); - var ignoreConnectors = opts.ignoreConnectors || IGNORED_CONNECTORS; + var raw = opt.raw; + var jumpSize = opt.size || JUMP_SIZE; + var jumpType = opt.jump && ('' + opt.jump).toLowerCase(); + var ignoreConnectors = opt.ignoreConnectors || IGNORED_CONNECTORS; // grab the first jump type as a default if specified one is invalid if (JUMP_TYPES.indexOf(jumpType) === -1) { @@ -15455,7 +22634,7 @@ joint.connectors.jumpover = (function(_, g, util) { // there is just one link, draw it directly if (allLinks.length === 1) { return buildPath( - createLines(sourcePoint, targetPoint, vertices), + createLines(sourcePoint, targetPoint, route), jumpSize, jumpType ); } @@ -15490,7 +22669,7 @@ joint.connectors.jumpover = (function(_, g, util) { var thisLines = createLines( sourcePoint, targetPoint, - vertices + route ); // create lines for all other links @@ -15536,8 +22715,8 @@ joint.connectors.jumpover = (function(_, g, util) { return resultLines; }, []); - - return buildPath(jumpingLines, jumpSize, jumpType); + var path = buildPath(jumpingLines, jumpSize, jumpType); + return (raw) ? path : path.serialize(); }; }(_, g, joint.util)); @@ -15985,147 +23164,1435 @@ joint.highlighters.addClass = { } }; -joint.highlighters.opacity = { +joint.highlighters.opacity = { + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + */ + highlight: function(cellView, magnetEl) { + + V(magnetEl).addClass(joint.util.addClassNamePrefix('highlight-opacity')); + }, + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + */ + unhighlight: function(cellView, magnetEl) { + + V(magnetEl).removeClass(joint.util.addClassNamePrefix('highlight-opacity')); + } +}; + +joint.highlighters.stroke = { + + defaultOptions: { + padding: 3, + rx: 0, + ry: 0, + attrs: { + 'stroke-width': 3, + stroke: '#FEB663' + } + }, + + _views: {}, + + getHighlighterId: function(magnetEl, opt) { + + return magnetEl.id + JSON.stringify(opt); + }, + + removeHighlighter: function(id) { + if (this._views[id]) { + this._views[id].remove(); + this._views[id] = null; + } + }, + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt + */ + highlight: function(cellView, magnetEl, opt) { + + var id = this.getHighlighterId(magnetEl, opt); + + // Only highlight once. + if (this._views[id]) return; + + var options = joint.util.defaults(opt || {}, this.defaultOptions); + + var magnetVel = V(magnetEl); + var magnetBBox; + + try { + + var pathData = magnetVel.convertToPathData(); + + } catch (error) { + + // Failed to get path data from magnet element. + // Draw a rectangle around the entire cell view instead. + magnetBBox = magnetVel.bbox(true/* without transforms */); + pathData = V.rectToPath(joint.util.assign({}, options, magnetBBox)); + } + + var highlightVel = V('path').attr({ + d: pathData, + 'pointer-events': 'none', + 'vector-effect': 'non-scaling-stroke', + 'fill': 'none' + }).attr(options.attrs); + + var highlightMatrix = magnetVel.getTransformToElement(cellView.el); + + // Add padding to the highlight element. + var padding = options.padding; + if (padding) { + + magnetBBox || (magnetBBox = magnetVel.bbox(true)); + + var cx = magnetBBox.x + (magnetBBox.width / 2); + var cy = magnetBBox.y + (magnetBBox.height / 2); + + magnetBBox = V.transformRect(magnetBBox, highlightMatrix); + + var width = Math.max(magnetBBox.width, 1); + var height = Math.max(magnetBBox.height, 1); + var sx = (width + padding) / width; + var sy = (height + padding) / height; + + var paddingMatrix = V.createSVGMatrix({ + a: sx, + b: 0, + c: 0, + d: sy, + e: cx - sx * cx, + f: cy - sy * cy + }); + + highlightMatrix = highlightMatrix.multiply(paddingMatrix); + } + + highlightVel.transform(highlightMatrix); + + // joint.mvc.View will handle the theme class name and joint class name prefix. + var highlightView = this._views[id] = new joint.mvc.View({ + svgElement: true, + className: 'highlight-stroke', + el: highlightVel.node + }); + + // Remove the highlight view when the cell is removed from the graph. + var removeHandler = this.removeHighlighter.bind(this, id); + var cell = cellView.model; + highlightView.listenTo(cell, 'remove', removeHandler); + highlightView.listenTo(cell.graph, 'reset', removeHandler); + + cellView.vel.append(highlightVel); + }, + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt + */ + unhighlight: function(cellView, magnetEl, opt) { + + this.removeHighlighter(this.getHighlighterId(magnetEl, opt)); + } +}; + +(function(joint, util) { + + function bboxWrapper(method) { + + return function(view, magnet, ref, opt) { + + var rotate = !!opt.rotate; + var bbox = (rotate) ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet); + var anchor = bbox[method](); + + var dx = opt.dx; + if (dx) { + var dxPercentage = util.isPercentage(dx); + dx = parseFloat(dx); + if (isFinite(dx)) { + if (dxPercentage) { + dx /= 100; + dx *= bbox.width; + } + anchor.x += dx; + } + } + + var dy = opt.dy; + if (dy) { + var dyPercentage = util.isPercentage(dy); + dy = parseFloat(dy); + if (isFinite(dy)) { + if (dyPercentage) { + dy /= 100; + dy *= bbox.height; + } + anchor.y += dy; + } + } + + return (rotate) ? anchor.rotate(view.model.getBBox().center(), -view.model.angle()) : anchor; + } + } + + function resolveRefAsBBoxCenter(fn) { + + return function(view, magnet, ref, opt) { + + if (ref instanceof Element) { + var refView = this.paper.findView(ref); + var refPoint = refView.getNodeBBox(ref).center(); + + return fn.call(this, view, magnet, refPoint, opt) + } + + return fn.apply(this, arguments); + } + } + + function perpendicular(view, magnet, refPoint, opt) { + + var angle = view.model.angle(); + var bbox = view.getNodeBBox(magnet); + var anchor = bbox.center(); + var topLeft = bbox.origin(); + var bottomRight = bbox.corner(); + + var padding = opt.padding + if (!isFinite(padding)) padding = 0; + + if ((topLeft.y + padding) <= refPoint.y && refPoint.y <= (bottomRight.y - padding)) { + var dy = (refPoint.y - anchor.y); + anchor.x += (angle === 0 || angle === 180) ? 0 : dy * 1 / Math.tan(g.toRad(angle)); + anchor.y += dy; + } else if ((topLeft.x + padding) <= refPoint.x && refPoint.x <= (bottomRight.x - padding)) { + var dx = (refPoint.x - anchor.x); + anchor.y += (angle === 90 || angle === 270) ? 0 : dx * Math.tan(g.toRad(angle)); + anchor.x += dx; + } + + return anchor; + } + + function midSide(view, magnet, refPoint, opt) { + + var rotate = !!opt.rotate; + var bbox, angle, center; + if (rotate) { + bbox = view.getNodeUnrotatedBBox(magnet); + center = view.model.getBBox().center(); + angle = view.model.angle(); + } else { + bbox = view.getNodeBBox(magnet); + } + + var padding = opt.padding; + if (isFinite(padding)) bbox.inflate(padding); + + if (rotate) refPoint.rotate(center, angle); + + var side = bbox.sideNearestToPoint(refPoint); + var anchor; + switch (side) { + case 'left': anchor = bbox.leftMiddle(); break; + case 'right': anchor = bbox.rightMiddle(); break; + case 'top': anchor = bbox.topMiddle(); break; + case 'bottom': anchor = bbox.bottomMiddle(); break; + } + + return (rotate) ? anchor.rotate(center, -angle) : anchor; + } + + // Can find anchor from model, when there is no selector or the link end + // is connected to a port + function modelCenter(view, magnet) { + + var model = view.model; + var bbox = model.getBBox(); + var center = bbox.center(); + var angle = model.angle(); + + var portId = view.findAttribute('port', magnet); + if (portId) { + portGroup = model.portProp(portId, 'group'); + var portsPositions = model.getPortsPositions(portGroup); + var anchor = new g.Point(portsPositions[portId]).offset(bbox.origin()); + anchor.rotate(center, -angle); + return anchor; + } + + return center; + } + + joint.anchors = { + center: bboxWrapper('center'), + top: bboxWrapper('topMiddle'), + bottom: bboxWrapper('bottomMiddle'), + left: bboxWrapper('leftMiddle'), + right: bboxWrapper('rightMiddle'), + topLeft: bboxWrapper('origin'), + topRight: bboxWrapper('topRight'), + bottomLeft: bboxWrapper('bottomLeft'), + bottomRight: bboxWrapper('corner'), + perpendicular: resolveRefAsBBoxCenter(perpendicular), + midSide: resolveRefAsBBoxCenter(midSide), + modelCenter: modelCenter + } + +})(joint, joint.util); + +(function(joint, util, g, V) { + + function closestIntersection(intersections, refPoint) { + + if (intersections.length === 1) return intersections[0]; + return util.sortBy(intersections, function(i) { return i.squaredDistance(refPoint) })[0]; + } + + function offset(p1, p2, offset) { + + if (!isFinite(offset)) return p1; + var length = p1.distance(p2); + if (offset === 0 && length > 0) return p1; + return p1.move(p2, -Math.min(offset, length - 1)); + } + + function stroke(magnet) { + + var stroke = magnet.getAttribute('stroke-width'); + if (stroke === null) return 0; + return parseFloat(stroke) || 0; + } + + // Connection Points + + function anchor(line, view, magnet, opt) { + + return offset(line.end, line.start, opt.offset); + } + + function bboxIntersection(line, view, magnet, opt) { + + var bbox = view.getNodeBBox(magnet); + if (opt.stroke) bbox.inflate(stroke(magnet) / 2); + var intersections = line.intersect(bbox); + var cp = (intersections) + ? closestIntersection(intersections, line.start) + : line.end; + return offset(cp, line.start, opt.offset); + } + + function rectangleIntersection(line, view, magnet, opt) { + + var angle = view.model.angle(); + if (angle === 0) { + return bboxIntersection(line, view, magnet, opt); + } + + var bboxWORotation = view.getNodeUnrotatedBBox(magnet); + if (opt.stroke) bboxWORotation.inflate(stroke(magnet) / 2); + var center = bboxWORotation.center(); + var lineWORotation = line.clone().rotate(center, angle); + var intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation); + var cp = (intersections) + ? closestIntersection(intersections, lineWORotation.start).rotate(center, -angle) + : line.end; + return offset(cp, line.start, opt.offset); + } + + var BNDR_SUBDIVISIONS = 'segmentSubdivisons'; + var BNDR_SHAPE_BBOX = 'shapeBBox'; + + function boundaryIntersection(line, view, magnet, opt) { + + var node, intersection; + var selector = opt.selector; + var anchor = line.end; + + if (typeof selector === 'string') { + node = view.findBySelector(selector)[0]; + } else if (Array.isArray(selector)) { + node = util.getByPath(magnet, selector); + } else { + // Find the closest non-group descendant + node = magnet; + do { + var tagName = node.tagName.toUpperCase(); + if (tagName === 'G') { + node = node.firstChild; + } else if (tagName === 'TITLE') { + node = node.nextSibling; + } else break; + } while (node) + } + + if (!(node instanceof Element)) return anchor; + + var localShape = view.getNodeShape(node); + var magnetMatrix = view.getNodeMatrix(node); + var translateMatrix = view.getRootTranslateMatrix(); + var rotateMatrix = view.getRootRotateMatrix(); + var targetMatrix = translateMatrix.multiply(rotateMatrix).multiply(magnetMatrix); + var localMatrix = targetMatrix.inverse(); + var localLine = V.transformLine(line, localMatrix); + var localRef = localLine.start.clone(); + var data = view.getNodeData(node); + + if (opt.insideout === false) { + if (!data[BNDR_SHAPE_BBOX]) data[BNDR_SHAPE_BBOX] = localShape.bbox(); + var localBBox = data[BNDR_SHAPE_BBOX]; + if (localBBox.containsPoint(localRef)) return anchor; + } + + // Caching segment subdivisions for paths + var pathOpt + if (localShape instanceof g.Path) { + var precision = opt.precision || 2; + if (!data[BNDR_SUBDIVISIONS]) data[BNDR_SUBDIVISIONS] = localShape.getSegmentSubdivisions({ precision: precision }); + segmentSubdivisions = data[BNDR_SUBDIVISIONS]; + pathOpt = { + precision: precision, + segmentSubdivisions: data[BNDR_SUBDIVISIONS] + } + } + + if (opt.extrapolate === true) localLine.setLength(1e6); + + intersection = localLine.intersect(localShape, pathOpt); + if (intersection) { + // More than one intersection + if (V.isArray(intersection)) intersection = closestIntersection(intersection, localRef); + } else if (opt.sticky === true) { + // No intersection, find the closest point instead + if (localShape instanceof g.Rect) { + intersection = localShape.pointNearestToPoint(localRef); + } else if (localShape instanceof g.Ellipse) { + intersection = localShape.intersectionWithLineFromCenterToPoint(localRef); + } else { + intersection = localShape.closestPoint(localRef, pathOpt); + } + } + + var cp = (intersection) ? V.transformPoint(intersection, targetMatrix) : anchor; + var cpOffset = opt.offset || 0; + if (opt.stroke) cpOffset += stroke(node) / 2; + + return offset(cp, line.start, cpOffset); + } + + joint.connectionPoints = { + anchor: anchor, + bbox: bboxIntersection, + rectangle: rectangleIntersection, + boundary: boundaryIntersection + } + +})(joint, joint.util, g, V); + +(function(joint, util) { + + function abs2rel(value, max) { + + if (max === 0) return '0%'; + return Math.round(value / max * 100) + '%'; + } + + function pin(relative) { + + return function(end, view, magnet, coords) { + + var angle = view.model.angle(); + var bbox = view.getNodeUnrotatedBBox(magnet); + var origin = view.model.getBBox().center(); + coords.rotate(origin, angle); + var dx = coords.x - bbox.x; + var dy = coords.y - bbox.y; + + if (relative) { + dx = abs2rel(dx, bbox.width); + dy = abs2rel(dy, bbox.height); + } + + end.anchor = { + name: 'topLeft', + args: { + dx: dx, + dy: dy, + rotate: true + } + }; + + return end; + } + } - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - */ - highlight: function(cellView, magnetEl) { + joint.connectionStrategies = { + useDefaults: util.noop, + pinAbsolute: pin(false), + pinRelative: pin(true) + } - V(magnetEl).addClass(joint.util.addClassNamePrefix('highlight-opacity')); - }, +})(joint, joint.util); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - */ - unhighlight: function(cellView, magnetEl) { +(function(joint, util, V, g) { - V(magnetEl).removeClass(joint.util.addClassNamePrefix('highlight-opacity')); + function getAnchor(coords, view, magnet) { + // take advantage of an existing logic inside of the + // pin relative connection strategy + var end = joint.connectionStrategies.pinRelative.call( + this.paper, + {}, + view, + magnet, + coords, + this.model + ); + return end.anchor; } -}; -joint.highlighters.stroke = { + function snapAnchor(coords, view, magnet, type, relatedView, toolView) { + var snapRadius = toolView.options.snapRadius; + var isSource = (type === 'source'); + var refIndex = (isSource ? 0 : -1); + var ref = this.model.vertex(refIndex) || this.getEndAnchor(isSource ? 'target' : 'source'); + if (ref) { + if (Math.abs(ref.x - coords.x) < snapRadius) coords.x = ref.x; + if (Math.abs(ref.y - coords.y) < snapRadius) coords.y = ref.y; + } + return coords; + } - defaultOptions: { - padding: 3, - rx: 0, - ry: 0, - attrs: { - 'stroke-width': 3, - stroke: '#FEB663' + var ToolView = joint.dia.ToolView; + + // Vertex Handles + var VertexHandle = joint.mvc.View.extend({ + tagName: 'circle', + svgElement: true, + className: 'marker-vertex', + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown', + dblclick: 'onDoubleClick' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + attributes: { + 'r': 6, + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move' + }, + position: function(x, y) { + this.vel.attr({ cx: x, cy: y }); + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + this.options.paper.undelegateEvents(); + this.delegateDocumentEvents(null, evt.data); + this.trigger('will-change'); + }, + onPointerMove: function(evt) { + this.trigger('changing', this, evt); + }, + onDoubleClick: function(evt) { + this.trigger('remove', this, evt); + }, + onPointerUp: function(evt) { + this.trigger('changed', this, evt); + this.undelegateDocumentEvents(); + this.options.paper.delegateEvents(); } - }, + }); - _views: {}, + var Vertices = ToolView.extend({ + name: 'vertices', + options: { + handleClass: VertexHandle, + snapRadius: 20, + redundancyRemoval: true, + vertexAdding: true, + }, + children: [{ + tagName: 'path', + selector: 'connection', + className: 'joint-vertices-path', + attributes: { + 'fill': 'none', + 'stroke': 'transparent', + 'stroke-width': 10, + 'cursor': 'cell' + } + }], + handles: null, + events: { + 'mousedown .joint-vertices-path': 'onPathPointerDown' + }, + onRender: function() { + this.resetHandles(); + if (this.options.vertexAdding) { + this.renderChildren(); + this.updatePath(); + } + var relatedView = this.relatedView; + var vertices = relatedView.model.vertices(); + for (var i = 0, n = vertices.length; i < n; i++) { + var vertex = vertices[i]; + var handle = new (this.options.handleClass)({ index: i, paper: this.paper }); + handle.render(); + handle.position(vertex.x, vertex.y); + this.simulateRelatedView(handle.el); + handle.vel.appendTo(this.el); + this.handles.push(handle); + this.startHandleListening(handle); + } + return this; + }, + update: function() { + this.render(); + return this; + }, + updatePath: function() { + var connection = this.childNodes.connection; + if (connection) connection.setAttribute('d', this.relatedView.getConnection().serialize()); + }, + startHandleListening: function(handle) { + var relatedView = this.relatedView; + if (relatedView.can('vertexMove')) { + this.listenTo(handle, 'will-change', this.onHandleWillChange); + this.listenTo(handle, 'changing', this.onHandleChanging); + this.listenTo(handle, 'changed', this.onHandleChanged); + } + if (relatedView.can('vertexRemove')) { + this.listenTo(handle, 'remove', this.onHandleRemove); + } + }, + resetHandles: function() { + var handles = this.handles; + this.handles = []; + this.stopListening(); + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + handles[i].remove(); + } + }, + getNeighborPoints: function(index) { + var linkView = this.relatedView; + var vertices = linkView.model.vertices(); + var prev = (index > 0) ? vertices[index - 1] : linkView.sourceAnchor; + var next = (index < vertices.length - 1) ? vertices[index + 1] : linkView.targetAnchor; + return { + prev: new g.Point(prev), + next: new g.Point(next) + } + }, + onHandleWillChange: function(handle, evt) { + this.focus(); + this.relatedView.model.startBatch('vertex-move', { ui: true, tool: this.cid }); + }, + onHandleChanging: function(handle, evt) { + var relatedView = this.relatedView; + var paper = relatedView.paper; + var index = handle.options.index; + var vertex = paper.snapToGrid(evt.clientX, evt.clientY).toJSON(); + this.snapVertex(vertex, index); + relatedView.model.vertex(index, vertex, { ui: true, tool: this.cid }); + handle.position(vertex.x, vertex.y); + }, + snapVertex: function(vertex, index) { + var snapRadius = this.options.snapRadius; + if (snapRadius > 0) { + var neighbors = this.getNeighborPoints(index); + var prev = neighbors.prev; + var next = neighbors.next; + if (Math.abs(vertex.x - prev.x) < snapRadius) { + vertex.x = prev.x; + } else if (Math.abs(vertex.x - next.x) < snapRadius) { + vertex.x = next.x; + } + if (Math.abs(vertex.y - prev.y) < snapRadius) { + vertex.y = neighbors.prev.y; + } else if (Math.abs(vertex.y - next.y) < snapRadius) { + vertex.y = next.y; + } + } + }, + onHandleChanged: function(handle, evt) { + if (this.options.vertexAdding) this.updatePath(); + if (!this.options.redundancyRemoval) return; + var linkView = this.relatedView; + var verticesRemoved = linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + if (verticesRemoved) this.render(); + this.blur(); + linkView.model.stopBatch('vertex-move', { ui: true, tool: this.cid }); + if (this.eventData(evt).vertexAdded) { + linkView.model.stopBatch('vertex-add', { ui: true, tool: this.cid }); + } + }, + onHandleRemove: function(handle) { + var index = handle.options.index; + this.relatedView.model.removeVertex(index, { ui: true }); + }, + onPathPointerDown: function(evt) { + evt.stopPropagation(); + var vertex = this.paper.snapToGrid(evt.clientX, evt.clientY).toJSON(); + var relatedView = this.relatedView; + relatedView.model.startBatch('vertex-add', { ui: true, tool: this.cid }); + var index = relatedView.getVertexIndex(vertex.x, vertex.y); + this.snapVertex(vertex, index); + relatedView.model.insertVertex(index, vertex, { ui: true, tool: this.cid }); + this.render(); + var handle = this.handles[index]; + this.eventData(evt, { vertexAdded: true }); + handle.onPointerDown(evt); + }, + onRemove: function() { + this.resetHandles(); + } + }, { + VertexHandle: VertexHandle // keep as class property + }); - getHighlighterId: function(magnetEl, opt) { + var SegmentHandle = joint.mvc.View.extend({ + tagName: 'g', + svgElement: true, + className: 'marker-segment', + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + children: [{ + tagName: 'line', + selector: 'line', + attributes: { + 'stroke': '#33334F', + 'stroke-width': 2, + 'fill': 'none', + 'pointer-events': 'none' + } + }, { + tagName: 'rect', + selector: 'handle', + attributes: { + 'width': 20, + 'height': 8, + 'x': -10, + 'y': -4, + 'rx': 4, + 'ry': 4, + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2 + } + }], + onRender: function() { + this.renderChildren(); + }, + position: function(x, y, angle, view) { - return magnetEl.id + JSON.stringify(opt); - }, + var matrix = V.createSVGMatrix().translate(x, y).rotate(angle); + var handle = this.childNodes.handle; + handle.setAttribute('transform', V.matrixToTransformString(matrix)); + handle.setAttribute('cursor', (angle % 180 === 0) ? 'row-resize' : 'col-resize'); - removeHighlighter: function(id) { - if (this._views[id]) { - this._views[id].remove(); - this._views[id] = null; + var viewPoint = view.getClosestPoint(new g.Point(x, y)); + var line = this.childNodes.line; + line.setAttribute('x1', x); + line.setAttribute('y1', y); + line.setAttribute('x2', viewPoint.x); + line.setAttribute('y2', viewPoint.y); + }, + onPointerDown: function(evt) { + this.trigger('change:start', this, evt); + evt.stopPropagation(); + this.options.paper.undelegateEvents(); + this.delegateDocumentEvents(null, evt.data); + }, + onPointerMove: function(evt) { + this.trigger('changing', this, evt); + }, + onPointerUp: function(evt) { + this.undelegateDocumentEvents(); + this.options.paper.delegateEvents(); + this.trigger('change:end', this, evt); + }, + show: function() { + this.el.style.display = ''; + }, + hide: function() { + this.el.style.display = 'none'; } - }, + }); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - highlight: function(cellView, magnetEl, opt) { + var Segments = ToolView.extend({ + name: 'segments', + precision: .5, + options: { + handleClass: SegmentHandle, + segmentLengthThreshold: 40, + redundancyRemoval: true, + anchor: getAnchor, + snapRadius: 10, + snapHandle: true + }, + handles: null, + onRender: function() { + this.resetHandles(); + var relatedView = this.relatedView; + var vertices = relatedView.model.vertices(); + vertices.unshift(relatedView.sourcePoint); + vertices.push(relatedView.targetPoint); + for (var i = 0, n = vertices.length; i < n - 1; i++) { + var vertex = vertices[i]; + var nextVertex = vertices[i + 1]; + var handle = this.renderHandle(vertex, nextVertex); + this.simulateRelatedView(handle.el); + this.handles.push(handle); + handle.options.index = i; + } + return this; + }, + renderHandle: function(vertex, nextVertex) { + var handle = new (this.options.handleClass)({ paper: this.paper }); + handle.render(); + this.updateHandle(handle, vertex, nextVertex); + handle.vel.appendTo(this.el); + this.startHandleListening(handle); + return handle; + }, + update: function() { + this.render(); + return this; + }, + startHandleListening: function(handle) { + this.listenTo(handle, 'change:start', this.onHandleChangeStart); + this.listenTo(handle, 'changing', this.onHandleChanging); + this.listenTo(handle, 'change:end', this.onHandleChangeEnd); + }, + resetHandles: function() { + var handles = this.handles; + this.handles = []; + this.stopListening(); + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + handles[i].remove(); + } + }, + shiftHandleIndexes: function(value) { + var handles = this.handles; + for (var i = 0, n = handles.length; i < n; i++) handles[i].options.index += value; + }, + resetAnchor: function(type, anchor) { + var relatedModel = this.relatedView.model; + if (anchor) { + relatedModel.prop([type, 'anchor'], anchor, { + rewrite: true, + ui: true, + tool: this.cid + }); + } else { + relatedModel.removeProp([type, 'anchor'], { + ui: true, + tool: this.cid + }); + } + }, + snapHandle: function(handle, position, data) { + + var index = handle.options.index; + var linkView = this.relatedView; + var link = linkView.model; + var vertices = link.vertices(); + var axis = handle.options.axis; + var prev = vertices[index - 2] || data.sourceAnchor; + var next = vertices[index + 1] || data.targetAnchor; + var snapRadius = this.options.snapRadius; + if (Math.abs(position[axis] - prev[axis]) < snapRadius) { + position[axis] = prev[axis]; + } else if (Math.abs(position[axis] - next[axis]) < snapRadius) { + position[axis] = next[axis]; + } + return position; + }, + + onHandleChanging: function(handle, evt) { + + var data = this.eventData(evt); + var relatedView = this.relatedView; + var paper = relatedView.paper; + var index = handle.options.index - 1; + var coords = paper.snapToGrid(evt.clientX, evt.clientY); + var position = this.snapHandle(handle, coords.clone(), data); + var axis = handle.options.axis; + var offset = (this.options.snapHandle) ? 0 : (coords[axis] - position[axis]); + var link = relatedView.model; + var vertices = util.cloneDeep(link.vertices()); + var vertex = vertices[index]; + var nextVertex = vertices[index + 1]; + var anchorFn = this.options.anchor; + if (typeof anchorFn !== 'function') anchorFn = null; + + // First Segment + var sourceView = relatedView.sourceView; + var sourceBBox = relatedView.sourceBBox; + var changeSourceAnchor = false; + var deleteSourceAnchor = false; + if (!vertex) { + vertex = relatedView.sourceAnchor.toJSON(); + vertex[axis] = position[axis]; + if (sourceBBox.containsPoint(vertex)) { + vertex[axis] = position[axis]; + changeSourceAnchor = true; + } else { + // we left the area of the source magnet for the first time + vertices.unshift(vertex); + this.shiftHandleIndexes(1); + delateSourceAnchor = true; + } + } else if (index === 0) { + if (sourceBBox.containsPoint(vertex)) { + vertices.shift(); + this.shiftHandleIndexes(-1); + changeSourceAnchor = true; + } else { + vertex[axis] = position[axis]; + deleteSourceAnchor = true; + } + } else { + vertex[axis] = position[axis]; + } - var id = this.getHighlighterId(magnetEl, opt); + if (anchorFn && sourceView) { + if (changeSourceAnchor) { + var sourceAnchorPosition = data.sourceAnchor.clone(); + sourceAnchorPosition[axis] = position[axis]; + var sourceAnchor = anchorFn.call(relatedView, sourceAnchorPosition, sourceView, relatedView.sourceMagnet || sourceView.el, 'source', relatedView); + this.resetAnchor('source', sourceAnchor); + } + if (deleteSourceAnchor) { + this.resetAnchor('source', data.sourceAnchorDef); + } + } - // Only highlight once. - if (this._views[id]) return; + // Last segment + var targetView = relatedView.targetView; + var targetBBox = relatedView.targetBBox; + var changeTargetAnchor = false; + var deleteTargetAnchor = false; + if (!nextVertex) { + nextVertex = relatedView.targetAnchor.toJSON(); + nextVertex[axis] = position[axis]; + if (targetBBox.containsPoint(nextVertex)) { + changeTargetAnchor = true; + } else { + // we left the area of the target magnet for the first time + vertices.push(nextVertex); + deleteTargetAnchor = true; + } + } else if (index === vertices.length - 2) { + if (targetBBox.containsPoint(nextVertex)) { + vertices.pop(); + changeTargetAnchor = true; + } else { + nextVertex[axis] = position[axis]; + deleteTargetAnchor = true; + } + } else { + nextVertex[axis] = position[axis]; + } - var options = joint.util.defaults(opt || {}, this.defaultOptions); + if (anchorFn && targetView) { + if (changeTargetAnchor) { + var targetAnchorPosition = data.targetAnchor.clone(); + targetAnchorPosition[axis] = position[axis]; + var targetAnchor = anchorFn.call(relatedView, targetAnchorPosition, targetView, relatedView.targetMagnet || targetView.el, 'target', relatedView); + this.resetAnchor('target', targetAnchor); + } + if (deleteTargetAnchor) { + this.resetAnchor('target', data.targetAnchorDef); + } + } - var magnetVel = V(magnetEl); - var magnetBBox; + link.vertices(vertices, { ui: true, tool: this.cid }); + this.updateHandle(handle, vertex, nextVertex, offset); + }, + onHandleChangeStart: function(handle, evt) { + var index = handle.options.index; + var handles = this.handles; + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + if (i !== index) handles[i].hide() + } + this.focus(); + var relatedView = this.relatedView; + var relatedModel = relatedView.model; + this.eventData(evt, { + sourceAnchor: relatedView.sourceAnchor.clone(), + targetAnchor: relatedView.targetAnchor.clone(), + sourceAnchorDef: util.clone(relatedModel.prop(['source', 'anchor'])), + targetAnchorDef: util.clone(relatedModel.prop(['target', 'anchor'])) + }); + relatedView.model.startBatch('segment-move', { ui: true, tool: this.cid }); + }, + onHandleChangeEnd: function(handle) { + var linkView = this.relatedView; + if (this.options.redundancyRemoval) { + linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + } + this.render(); + this.blur(); + linkView.model.stopBatch('segment-move', { ui: true, tool: this.cid }); + }, + updateHandle: function(handle, vertex, nextVertex, offset) { + var vertical = Math.abs(vertex.x - nextVertex.x) < this.precision; + var horizontal = Math.abs(vertex.y - nextVertex.y) < this.precision; + if (vertical || horizontal) { + var segmentLine = new g.Line(vertex, nextVertex); + var length = segmentLine.length(); + if (length < this.options.segmentLengthThreshold) { + handle.hide(); + } else { + var position = segmentLine.midpoint() + var axis = (vertical) ? 'x' : 'y'; + position[axis] += offset || 0; + var angle = segmentLine.vector().vectorAngle(new g.Point(1, 0)); + handle.position(position.x, position.y, angle, this.relatedView); + handle.show(); + handle.options.axis = axis; + } + } else { + handle.hide(); + } + }, + onRemove: function() { + this.resetHandles(); + } + }, { + SegmentHandle: SegmentHandle // keep as class property + }); - try { + // End Markers + var Arrowhead = ToolView.extend({ + tagName: 'path', + xAxisVector: new g.Point(1, 0), + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + onRender: function() { + this.update() + }, + update: function() { + var ratio = this.ratio; + var view = this.relatedView; + var tangent = view.getTangentAtRatio(ratio); + var position, angle; + if (tangent) { + position = tangent.start; + angle = tangent.vector().vectorAngle(this.xAxisVector) || 0; + } else { + position = view.getPointAtRatio(ratio); + angle = 0; + } + var matrix = V.createSVGMatrix().translate(position.x, position.y).rotate(angle); + this.vel.transform(matrix, { absolute: true }); + return this; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + var relatedView = this.relatedView; + relatedView.model.startBatch('arrowhead-move', { ui: true, tool: this.cid }); + if (relatedView.can('arrowheadMove')) { + relatedView.startArrowheadMove(this.arrowheadType); + this.delegateDocumentEvents(); + relatedView.paper.undelegateEvents(); + } + this.focus(); + this.el.style.pointerEvents = 'none'; + }, + onPointerMove: function(evt) { + var coords = this.paper.snapToGrid(evt.clientX, evt.clientY); + this.relatedView.pointermove(evt, coords.x, coords.y); + }, + onPointerUp: function(evt) { + this.undelegateDocumentEvents(); + var relatedView = this.relatedView; + var paper = relatedView.paper; + var coords = paper.snapToGrid(evt.clientX, evt.clientY); + relatedView.pointerup(evt, coords.x, coords.y); + paper.delegateEvents(); + this.blur(); + this.el.style.pointerEvents = ''; + relatedView.model.stopBatch('arrowhead-move', { ui: true, tool: this.cid }); + } + }); - var pathData = magnetVel.convertToPathData(); + var TargetArrowhead = Arrowhead.extend({ + name: 'target-arrowhead', + ratio: 1, + arrowheadType: 'target', + attributes: { + 'd': 'M -10 -8 10 0 -10 8 Z', + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move', + 'class': 'target-arrowhead' + } + }); - } catch (error) { + var SourceArrowhead = Arrowhead.extend({ + name: 'source-arrowhead', + ratio: 0, + arrowheadType: 'source', + attributes: { + 'd': 'M 10 -8 -10 0 10 8 Z', + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move', + 'class': 'source-arrowhead' + } + }); - // Failed to get path data from magnet element. - // Draw a rectangle around the entire cell view instead. - magnetBBox = magnetVel.bbox(true/* without transforms */); - pathData = V.rectToPath(joint.util.assign({}, options, magnetBBox)); + var Button = ToolView.extend({ + name: 'button', + events: { + 'mousedown': 'onPointerDown', + 'touchstart': 'onPointerDown' + }, + options: { + distance: 0, + offset: 0, + rotate: false + }, + onRender: function() { + this.renderChildren(this.options.markup); + this.update() + }, + update: function() { + var tangent, position, angle; + var distance = this.options.distance || 0; + if (util.isPercentage(distance)) { + tangent = this.relatedView.getTangentAtRatio(parseFloat(distance) / 100); + } else { + tangent = this.relatedView.getTangentAtLength(distance) + } + if (tangent) { + position = tangent.start; + angle = tangent.vector().vectorAngle(new g.Point(1,0)) || 0; + } else { + position = this.relatedView.getConnection().start; + angle = 0; + } + var matrix = V.createSVGMatrix() + .translate(position.x, position.y) + .rotate(angle) + .translate(0, this.options.offset || 0); + if (!this.options.rotate) matrix = matrix.rotate(-angle); + this.vel.transform(matrix, { absolute: true }); + return this; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + var actionFn = this.options.action; + if (typeof actionFn === 'function') { + actionFn.call(this.relatedView, evt, this.relatedView); + } } + }); - var highlightVel = V('path').attr({ - d: pathData, - 'pointer-events': 'none', - 'vector-effect': 'non-scaling-stroke', - 'fill': 'none' - }).attr(options.attrs); - var highlightMatrix = magnetVel.getTransformToElement(cellView.el); + var Remove = Button.extend({ + children: [{ + tagName: 'circle', + selector: 'button', + attributes: { + 'r': 7, + 'fill': '#FF1D00', + 'cursor': 'pointer' + } + }, { + tagName: 'path', + selector: 'icon', + attributes: { + 'd': 'M -3 -3 3 3 M -3 3 3 -3', + 'fill': 'none', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'pointer-events': 'none' + } + }], + options: { + distance: 60, + offset: 0, + action: function(evt) { + this.model.remove({ ui: true, tool: this.cid }); + } + } + }); - // Add padding to the highlight element. - var padding = options.padding; - if (padding) { + var Boundary = ToolView.extend({ + name: 'boundary', + tagName: 'rect', + options: { + padding: 10 + }, + attributes: { + 'fill': 'none', + 'stroke': '#33334F', + 'stroke-width': .5, + 'stroke-dasharray': '5, 5', + 'pointer-events': 'none' + }, + onRender: function() { + this.update(); + }, + update: function() { + var padding = this.options.padding; + if (!isFinite(padding)) padding = 0; + var bbox = this.relatedView.getConnection().bbox().inflate(padding); + this.vel.attr(bbox.toJSON()); + return this; + } + }); - magnetBBox || (magnetBBox = magnetVel.bbox(true)); + var Anchor = ToolView.extend({ + tagName: 'g', + type: null, + children: [{ + tagName: 'circle', + selector: 'anchor', + attributes: { + 'cursor': 'pointer' + } + }, { + tagName: 'rect', + selector: 'area', + attributes: { + 'pointer-events': 'none', + 'fill': 'none', + 'stroke': '#33334F', + 'stroke-dasharray': '2,4', + 'rx': 5, + 'ry': 5 + } + }], + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown', + dblclick: 'onPointerDblClick' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + options: { + snap: snapAnchor, + anchor: getAnchor, + customAnchorAttributes: { + 'stroke-width': 4, + 'stroke': '#33334F', + 'fill': '#FFFFFF', + 'r': 5 + }, + defaultAnchorAttributes: { + 'stroke-width': 2, + 'stroke': '#FFFFFF', + 'fill': '#33334F', + 'r': 6 + }, + areaPadding: 6, + snapRadius: 10, + restrictArea: true, + redundancyRemoval: true + }, + onRender: function() { + this.renderChildren(); + this.toggleArea(false); + this.update(); + }, + update: function() { + var type = this.type; + var relatedView = this.relatedView; + var view = relatedView.getEndView(type); + if (view) { + this.updateAnchor(); + this.updateArea(); + this.el.style.display = ''; + } else { + this.el.style.display = 'none'; + } + return this; + }, + updateAnchor: function() { + var childNodes = this.childNodes; + if (!childNodes) return; + var anchorNode = childNodes.anchor; + if (!anchorNode) return; + var relatedView = this.relatedView; + var type = this.type; + var position = relatedView.getEndAnchor(type); + var options = this.options; + var customAnchor = relatedView.model.prop([type, 'anchor']); + anchorNode.setAttribute('transform', 'translate(' + position.x + ',' + position.y + ')'); + var anchorAttributes = (customAnchor) ? options.customAnchorAttributes : options.defaultAnchorAttributes; + for (var attrName in anchorAttributes) { + anchorNode.setAttribute(attrName, anchorAttributes[attrName]); + } + }, + updateArea: function() { + var childNodes = this.childNodes; + if (!childNodes) return; + var areaNode = childNodes.area; + if (!areaNode) return; + var relatedView = this.relatedView; + var type = this.type; + var view = relatedView.getEndView(type); + var magnet = relatedView.getEndMagnet(type); + var padding = this.options.areaPadding; + if (!isFinite(padding)) padding = 0; + var bbox = view.getNodeUnrotatedBBox(magnet).inflate(padding); + var angle = view.model.angle(); + areaNode.setAttribute('x', -bbox.width / 2); + areaNode.setAttribute('y', -bbox.height / 2); + areaNode.setAttribute('width', bbox.width); + areaNode.setAttribute('height', bbox.height); + var origin = view.model.getBBox().center(); + var center = bbox.center().rotate(origin, -angle) + areaNode.setAttribute('transform', 'translate(' + center.x + ',' + center.y + ') rotate(' + angle +')'); + }, + toggleArea: function(visible) { + this.childNodes.area.style.display = (visible) ? '' : 'none'; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + this.paper.undelegateEvents(); + this.delegateDocumentEvents(); + this.focus(); + this.toggleArea(this.options.restrictArea); + this.relatedView.model.startBatch('anchor-move', { ui: true, tool: this.cid }); + }, + resetAnchor: function(anchor) { + var type = this.type; + var relatedModel = this.relatedView.model; + if (anchor) { + relatedModel.prop([type, 'anchor'], anchor, { + rewrite: true, + ui: true, + tool: this.cid + }); + } else { + relatedModel.removeProp([type, 'anchor'], { + ui: true, + tool: this.cid + }); + } + }, + onPointerMove: function(evt) { - var cx = magnetBBox.x + (magnetBBox.width / 2); - var cy = magnetBBox.y + (magnetBBox.height / 2); + var relatedView = this.relatedView; + var type = this.type; + var view = relatedView.getEndView(type); + var magnet = relatedView.getEndMagnet(type); - magnetBBox = V.transformRect(magnetBBox, highlightMatrix); + var coords = this.paper.clientToLocalPoint(evt.clientX, evt.clientY); + var snapFn = this.options.snap; + if (typeof snapFn === 'function') { + coords = snapFn.call(relatedView, coords, view, magnet, type, relatedView, this); + coords = new g.Point(coords); + } - var width = Math.max(magnetBBox.width, 1); - var height = Math.max(magnetBBox.height, 1); - var sx = (width + padding) / width; - var sy = (height + padding) / height; + if (this.options.restrictArea) { + // snap coords within node bbox + var bbox = view.getNodeUnrotatedBBox(magnet); + var angle = view.model.angle(); + var origin = view.model.getBBox().center(); + var rotatedCoords = coords.clone().rotate(origin, angle); + if (!bbox.containsPoint(rotatedCoords)) { + coords = bbox.pointNearestToPoint(rotatedCoords).rotate(origin, -angle); + } + } - var paddingMatrix = V.createSVGMatrix({ - a: sx, - b: 0, - c: 0, - d: sy, - e: cx - sx * cx, - f: cy - sy * cy - }); + var anchor; + var anchorFn = this.options.anchor; + if (typeof anchorFn === 'function') { + anchor = anchorFn.call(relatedView, coords, view, magnet, type, relatedView); + } - highlightMatrix = highlightMatrix.multiply(paddingMatrix); - } + this.resetAnchor(anchor); + this.update(); + }, - highlightVel.transform(highlightMatrix); + onPointerUp: function(evt) { + this.paper.delegateEvents(); + this.undelegateDocumentEvents(); + this.blur(); + this.toggleArea(false); + var linkView = this.relatedView; + if (this.options.redundancyRemoval) linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + linkView.model.stopBatch('anchor-move', { ui: true, tool: this.cid }); + }, - // joint.mvc.View will handle the theme class name and joint class name prefix. - var highlightView = this._views[id] = new joint.mvc.View({ - svgElement: true, - className: 'highlight-stroke', - el: highlightVel.node - }); + onPointerDblClick: function() { + this.resetAnchor(); + this.update(); + } + }); - // Remove the highlight view when the cell is removed from the graph. - var removeHandler = this.removeHighlighter.bind(this, id); - var cell = cellView.model; - highlightView.listenTo(cell, 'remove', removeHandler); - highlightView.listenTo(cell.graph, 'reset', removeHandler); + var SourceAnchor = Anchor.extend({ + name: 'source-anchor', + type: 'source' + }); - cellView.vel.append(highlightVel); - }, + var TargetAnchor = Anchor.extend({ + name: 'target-anchor', + type: 'target' + }); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - unhighlight: function(cellView, magnetEl, opt) { + // Export + joint.linkTools = { + Vertices: Vertices, + Segments: Segments, + SourceArrowhead: SourceArrowhead, + TargetArrowhead: TargetArrowhead, + SourceAnchor: SourceAnchor, + TargetAnchor: TargetAnchor, + Button: Button, + Remove: Remove, + Boundary: Boundary + }; - this.removeHighlighter(this.getHighlighterId(magnetEl, opt)); - } -}; +})(joint, joint.util, V, g); joint.dia.Element.define('erd.Entity', { size: { width: 150, height: 60 }, @@ -16828,7 +25295,7 @@ joint.shapes.basic.Generic.define('uml.Class', { }); -joint.shapes.uml.ClassView = joint.dia.ElementView.extend({}, { +joint.shapes.uml.ClassView = joint.dia.ElementView.extend({ initialize: function() { @@ -17259,7 +25726,8 @@ joint.layout.DirectedGraph = { graph = graphOrCells; } else { // Reset cells in dry mode so the graph reference is not stored on the cells. - graph = (new joint.dia.Graph()).resetCells(graphOrCells, { dry: true }); + // `sort: false` to prevent elements to change their order based on the z-index + graph = (new joint.dia.Graph()).resetCells(graphOrCells, { dry: true, sort: false }); } // This is not needed anymore. diff --git a/dist/joint.layout.DirectedGraph.js b/dist/joint.layout.DirectedGraph.js index bcd9563a2..39c2e7964 100644 --- a/dist/joint.layout.DirectedGraph.js +++ b/dist/joint.layout.DirectedGraph.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -110,7 +110,8 @@ joint.layout.DirectedGraph = { graph = graphOrCells; } else { // Reset cells in dry mode so the graph reference is not stored on the cells. - graph = (new joint.dia.Graph()).resetCells(graphOrCells, { dry: true }); + // `sort: false` to prevent elements to change their order based on the z-index + graph = (new joint.dia.Graph()).resetCells(graphOrCells, { dry: true, sort: false }); } // This is not needed anymore. diff --git a/dist/joint.layout.DirectedGraph.min.js b/dist/joint.layout.DirectedGraph.min.js index f044898c2..fdc6d827b 100644 --- a/dist/joint.layout.DirectedGraph.min.js +++ b/dist/joint.layout.DirectedGraph.min.js @@ -1,8 +1,8 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - 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/. */ -if("object"==typeof exports)var graphlib=require("graphlib"),dagre=require("dagre");graphlib=graphlib||"undefined"!=typeof window&&window.graphlib,dagre=dagre||"undefined"!=typeof window&&window.dagre,joint.layout.DirectedGraph={exportElement:function(a){return a.size()},exportLink:function(a){var b=a.get("labelSize")||{},c={minLen:a.get("minLen")||1,weight:a.get("weight")||1,labelpos:a.get("labelPosition")||"c",labeloffset:a.get("labelOffset")||0,width:b.width||0,height:b.height||0};return c},importElement:function(a,b,c){var d=this.getCell(b),e=c.node(b);a.setPosition?a.setPosition(d,e):d.set("position",{x:e.x-e.width/2,y:e.y-e.height/2})},importLink:function(a,b,c){var d=this.getCell(b.name),e=c.edge(b),f=e.points||[];if((a.setVertices||a.setLinkVertices)&&(joint.util.isFunction(a.setVertices)?a.setVertices(d,f):d.set("vertices",f.slice(1,f.length-1))),a.setLabels&&"x"in e&&"y"in e){var h={x:e.x,y:e.y};if(joint.util.isFunction(a.setLabels))a.setLabels(d,h,f);else{var i=g.Polyline(f),j=i.closestPointLength(h),k=i.pointAtLength(j),l=j/i.length();d.label(0,{position:{distance:l,offset:g.Point(h).difference(k).toJSON()}})}}},layout:function(a,b){var c;c=a instanceof joint.dia.Graph?a:(new joint.dia.Graph).resetCells(a,{dry:!0}),a=null,b=joint.util.defaults(b||{},{resizeClusters:!0,clusterPadding:10,exportElement:this.exportElement,exportLink:this.exportLink});var d=c.toGraphLib({directed:!0,multigraph:!0,compound:!0,setNodeLabel:b.exportElement,setEdgeLabel:b.exportLink,setEdgeName:function(a){return a.id}}),e={},f=b.marginX||0,h=b.marginY||0;if(b.rankDir&&(e.rankdir=b.rankDir),b.align&&(e.align=b.align),b.nodeSep&&(e.nodesep=b.nodeSep),b.edgeSep&&(e.edgesep=b.edgeSep),b.rankSep&&(e.ranksep=b.rankSep),b.ranker&&(e.ranker=b.ranker),f&&(e.marginx=f),h&&(e.marginy=h),d.setGraph(e),dagre.layout(d,{debugTiming:!!b.debugTiming}),c.startBatch("layout"),c.fromGraphLib(d,{importNode:this.importElement.bind(c,b),importEdge:this.importLink.bind(c,b)}),b.resizeClusters){var i=d.nodes().filter(function(a){return d.children(a).length>0}).map(c.getCell.bind(c)).sort(function(a,b){return b.getAncestors().length-a.getAncestors().length});joint.util.invoke(i,"fitEmbeds",{padding:b.clusterPadding})}c.stopBatch("layout");var j=d.graph();return g.Rect(f,h,Math.abs(j.width-2*f),Math.abs(j.height-2*h))},fromGraphLib:function(a,b){b=b||{};var c=b.importNode||joint.util.noop,d=b.importEdge||joint.util.noop,e=this instanceof joint.dia.Graph?this:new joint.dia.Graph;return a.nodes().forEach(function(d){c.call(e,d,a,e,b)}),a.edges().forEach(function(c){d.call(e,c,a,e,b)}),e},toGraphLib:function(a,b){b=b||{};for(var c=joint.util.pick(b,"directed","compound","multigraph"),d=new graphlib.Graph(c),e=b.setNodeLabel||joint.util.noop,f=b.setEdgeLabel||joint.util.noop,g=b.setEdgeName||joint.util.noop,h=a.get("cells"),i=0,j=h.length;i0}).map(c.getCell.bind(c)).sort(function(a,b){return b.getAncestors().length-a.getAncestors().length});joint.util.invoke(i,"fitEmbeds",{padding:b.clusterPadding})}c.stopBatch("layout");var j=d.graph();return g.Rect(f,h,Math.abs(j.width-2*f),Math.abs(j.height-2*h))},fromGraphLib:function(a,b){b=b||{};var c=b.importNode||joint.util.noop,d=b.importEdge||joint.util.noop,e=this instanceof joint.dia.Graph?this:new joint.dia.Graph;return a.nodes().forEach(function(d){c.call(e,d,a,e,b)}),a.edges().forEach(function(c){d.call(e,c,a,e,b)}),e},toGraphLib:function(a,b){b=b||{};for(var c=joint.util.pick(b,"directed","compound","multigraph"),d=new graphlib.Graph(c),e=b.setNodeLabel||joint.util.noop,f=b.setEdgeLabel||joint.util.noop,g=b.setEdgeName||joint.util.noop,h=a.get("cells"),i=0,j=h.length;isvg{position:absolute;top:0;left:0;right:0;bottom:0}[magnet=true]:not(.joint-element){cursor:crosshair}.marker-arrowheads,.marker-vertices{cursor:move;opacity:0}[magnet=true]:not(.joint-element):hover{opacity:.7}.joint-element{cursor:move}.joint-element *{user-drag:none}.joint-paper{position:relative}.joint-highlight-opacity{opacity:.3}.joint-link .connection,.joint-link .connection-wrap{fill:none}.marker-arrowheads{cursor:-webkit-grab;cursor:-moz-grab}.link-tools{opacity:0;cursor:pointer}.link-tools .tool-options{display:none}.joint-link:hover .link-tools,.joint-link:hover .marker-arrowheads,.joint-link:hover .marker-vertices{opacity:1}.marker-vertex-remove{cursor:pointer;opacity:.1}.marker-vertex-group:hover .marker-vertex-remove{opacity:1}.marker-vertex-remove-area{opacity:.1;cursor:pointer}.marker-vertex-group:hover .marker-vertex-remove-area{opacity:1}.joint-element .fobj{overflow:hidden}.joint-element .fobj body{background-color:transparent;margin:0;position:static}.joint-element .fobj div{text-align:center;vertical-align:middle;display:table-cell;padding:0 5px}.joint-paper.joint-theme-dark{background-color:#18191b}.joint-link.joint-theme-dark .connection-wrap{stroke:#8F8FF3;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-dark .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-dark .connection{stroke-linejoin:round}.joint-link.joint-theme-dark .link-tools .tool-remove circle{fill:#F33636}.joint-link.joint-theme-dark .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-dark .link-tools [event="link:options"] circle{fill:green}.joint-link.joint-theme-dark .marker-vertex{fill:#5652DB}.joint-link.joint-theme-dark .marker-vertex:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-arrowhead{fill:#5652DB}.joint-link.joint-theme-dark .marker-arrowhead:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-vertex-remove-area{fill:green;stroke:#006400}.joint-link.joint-theme-dark .marker-vertex-remove{fill:#fff;stroke:#fff}.joint-paper.joint-theme-default{background-color:#FFF}.joint-link.joint-theme-default .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-default .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-default .connection{stroke-linejoin:round}.joint-link.joint-theme-default .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-default .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-default .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-default .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-default .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-default .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-default .marker-vertex-remove{fill:#FFF}@font-face{font-family:lato-light;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAHhgABMAAAAA3HwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcaLe9KEdERUYAAAHEAAAAHgAAACABFgAER1BPUwAAAeQAAAo1AAARwtKX0BJHU1VCAAAMHAAAACwAAAAwuP+4/k9TLzIAAAxIAAAAWQAAAGDX0nerY21hcAAADKQAAAGJAAAB4hcJdWJjdnQgAAAOMAAAADoAAAA6DvoItmZwZ20AAA5sAAABsQAAAmVTtC+nZ2FzcAAAECAAAAAIAAAACAAAABBnbHlmAAAQKAAAXMoAAK3EsE/AsWhlYWQAAGz0AAAAMgAAADYOCCHIaGhlYQAAbSgAAAAgAAAAJA9hCBNobXR4AABtSAAAAkEAAAOkn9Zh6WxvY2EAAG+MAAAByAAAAdTkvg14bWF4cAAAcVQAAAAgAAAAIAIGAetuYW1lAABxdAAABDAAAAxGYqFiYXBvc3QAAHWkAAAB7wAAAtpTFoINcHJlcAAAd5QAAADBAAABOUVnCXh3ZWJmAAB4WAAAAAYAAAAGuclXKQAAAAEAAAAAzD2izwAAAADJKrAQAAAAANNPakh42mNgZGBg4ANiCQYQYGJgBMIXQMwC5jEAAA5CARsAAHjafddrjFTlHcfxP+KCAl1XbKLhRWnqUmpp1Yba4GXV1ktXK21dby0erZumiWmFZLuNMaQQElgWJ00mtNxRQMXLcntz3GUIjsYcNiEmE5PNhoFl2GQgzKvJvOnLJk4/M4DiGzL57v/szJzn/P6/53ee80zMiIg5cXc8GNc9+vhTz0bna/3/WBUL4nrvR7MZrc+vPp7xt7/8fVXc0Dpqc31c1643xIyu/e1vvhpTMTWjHlPX/XXmbXi3o7tjbNY/O7pnvTv7ldm7bvh9R/eNKzq658Sc385+Zea7c9+avWvens7bZtQ7xjq/uOl6r+fVLZ1fXP5vuqur6983benqao0587aO7tbf9tHYN6/W+N+8XKf9mreno7s1zpVXe7z26+rjS695e2be1hq3pfvS39b/7XcejTnNvuhqdsTNzZ6Yr97i/+7ml7FIXawuwVLcg/tiWdyPHi4+rD7W/Dx+3RyJXjyBZ/AcVhlrNdZivXE2YAgbMYxNeBM5Y27FNmzHDuzEbuxzjfeMvx/v4wN8iI8wggOucxCHcBhHkGIUYziKAo7hODJjnlDHjXuKrjKm9HsO046rOI+Fui/rvKzzss7LOi/rsqbLmi5ruqzpskZ9mfoy9WXqy9SXqS9TX6auRl2Nuhp1Nepq1NWoq1FXo65GXY26GnU16srU1WJJzKJnLjrbczJIzTg149SMUzNOzXgsa/bGfbi/mY+e5uvxsOMVzXXxYrMUL6krnbvKuYPqanWNulbNOXcrtmE7dmAndmOfcTJ1XD3lu2Wcdt4ZnEWl7dMgnwb5NBgX/f8DanskqEJxD8U9kjQoRYNSVJGgymWlWyitxQPNk9Qm8WBzkuItVPZQ2ENdKyUVKalISUVKKlJSkZKKlFQoS6hKqOmhpjVrgxT1UNRj9lpKeuKVmCWPc5p7Y67aia7mI/zbQs0j1OyN7zVHYyFul97u5gR1e/k6wdeJuLP5Gm8neDsh05vN9mazvdlsb44nm9X4TfONeNq5fXjGe8+qz6nPqy80t8cfqPyj4xXN6Ugcv6S+3CzESjpW0TCovuHz1Y7XOF6rrnf9DRjCRgxjE95Ejo6t2Ibt2IGd2I33XHc/3scH+BAfYQQHcBCHcBhHkOJj1x5Vx3AUBRzDcXzisyI+xWfIXOOE90/RWMZpes9gio9nVXPK9UdkYYssbJGFLXHRe92y8KUZqMrCl/Edee5UuyRqPm7x/iIsaw7Jw4QsVGXhiCyksjARv/T9fqx0ziDWYL3vbMAQNmIYm/Am9jl3HKd97wymXOOsWsE5xxfVn1HUR00fJX2yUInbvdvt7MVYgju9lqr3tJXl4l5n3sf/+5sZdQOU7TWnBfNpLo2xyhiD6mp1jbpWzTl3K7ZhO3ZgJ3bjLeO9jT3Y277HBvhbpXyAvxX+VnTQp4M+6vuo7+Nrha8VvlZ00Rc3Ut7vyv2u2u+K/c7sd2a/b/b7Zr9v9sddnM9xu5fbvdzOyXsm75m8L+R8TsbvkOtUrlO5TuU5k+dMnlN5zuQ5ledMjjNZzbif436O+znu57if436O+zk5S+UslbNUzlI5S+UslbNMzlI5S+UslbNUzlI5S+Usk7NMzjI5y2QsNWu9ZqvX/TqHO11Wr/m4xfEirMcGDGEjhrEJb2LK987hp9w5+a05vTKfv25e0OsFvV5wD0/o84IeL7hXC+Z03Fo5bl7HOXuSsyc5e/Kac3nAuQdxCIdxBClGMYajKOAYjqM1zyfUU8YtYxpVnMevYtZXEzEXneiKe3SxMOart+upW64XYwmW4h4sa74gmX2S+bpkLpPMPh1O63Bah9O6m9bdtM7e0dkRnb0TK429yriD6mp1jbpWzfl8K7ZhO3ZgJ3Zjn7EPGOcgDuEwjiDFKMZwFAUcw3Fkzjuhjjv3lPHLOO1aZzClp7NqBeccT/usivO46L07zPywmb/VzN9q5ofN/LCs9lmHSzqs6rCqw6oOqzqsSsWwVAxLxbBUDEvFsFQMS8WwtbFkbSxZG0vWxpK1sWRtLFkbS7qq6qqqq6quqrqq6qqqq6quqrqq6qqqq6quWnNXlbJbpYwuczJpTibNyaQ5mTQnk+ZkwopR5eckPyf5OcnPSX5O8nOSn5NWgKoVoGoFqFoBqryajGe+vldv/tb9mrhfE1caat+vi9UluLO51BWHXHEoHvvqfzzp5kk3T7o9l+51Hyfu44Q/3e7jhEfd7uPEc+kh93IiEb0SMeC59Gep6PVcGpKKXvd4IhW9EtF7zXs95/tbsQ3bsQM7sRvv0bMf7+MDfIiPMIIDdBzEIRzGEaT42HVH1TEcRQHHcByf+KyIT/EZMtc44f1TNJZxZb2YRhXn8fDlJ3/xqid/nrM1zuY5W7QC/pCjRU7ul6pRDtY5WOdgnYO7OVfnWp1jZy4/sWvtJ/Zq9dLTusahIoeKHCpyqMihIoeKHCpK3ajUjUrdqNSNSt2o1I1K3SgX6lyoc6HOhToX6lyoc6DOgToH6hyoc6DOgbpu67qt6bZ21ZM3f9WTN6/7mu5ruq+1n7wvc2ABBwY4sIADCzjwOgcSDrzOgQHZystWvu1Ea3VZ5L0rK8ylfF1aZS7tfRLuJNxJuPOCfOXlK8+lRL7ynErkK8+tf8lXXr52ydeIfK2Tr10cXMDBhIMLZCzPxYSLC7iYcHGAiwNcHODiABcHuDjAxYFrrkrX3vMkHE44nHA44XDC4UTO8lxOuJxwOeFywuWEy4mc5eUsL2d5OctfXsESziect9Ok9wym+HdWreCc42mfVXEeF733Ey6nl10tcLTA0QI3C9wscLLEyRInS9wrca7EtTLHJjjVWptT7qScSXVf0H1B9wXdF3Rf0H1B9wUdlnRY0mFJhyUdlnRY0l1JdyXdlXRX0l1JdyXdFHRT0k2qm5TqlOqU6lQ6ZrXuFHRihQS92PwvNTX7m6K9TdG+pmhPUrQnKdqTFO1JivYhxfiuM0ecOWJvV3P2iOfRZs+jumfRZvu3mtEaUpAZrWEv1xpxxIgjRhwx4ogRR4w4YsQRI47ETXK7XGaXU7W8ndlWXlc6HsQanMYZXJqH5eZheXseLqrz+ZvxN+NvaxfT2sFkvMp4lfEq41XGq4xXrV1JxquMVxmvMl5lvGrtQrKY59rrXHtd+5lzrWfIlO+cw/fdbYWvz7rF8aL2fDfoadDToKdBT0PiCxJfkPiCxBckviDxBYlvzWuD1gatDVobtDZobdDaoLVBa4PWBq0NWhu0Nr5WcP3Xu6UrO6EZ8So/5+qm047iZv54asWiWBw/ih/b594Vd8fS+Lln8C+sGff6LX9/POC30IPxkDX0sXg8nogn46n4XTwdfZ5Rz8bzsSJejCReij+ZlVUxYF5Wm5e1sT42xFBsDE/eyMV/Ymtsi+2xI3bGW/F27Im9fr2/E+/F/ng/PogP46PwWz0OxeE4Eh/HaIzF0SjEsTgen8cJv8hPRdlcn7FbOGuOz8V0VON8XPw/fppwigAAAHjaY2BkYGDgYtBh0GNgcnHzCWHgy0ksyWOQYGABijP8/w8kECwgAACeygdreNpjYGYRZtRhYGVgYZ3FaszAwCgPoZkvMrgxMXAwM/EzMzExsTAzMTcwMKx3YEjwYoCCksoAHyDF+5uJrfBfIQMDuwbjUgWgASA55t+sK4GUAgMTABvCDMIAAAB42mNgYGBmgGAZBkYGELgD5DGC+SwMB4C0DoMCkMUDZPEy1DH8ZwxmrGA6xnRHgUtBREFKQU5BSUFNQV/BSiFeYY2ikuqf30z//4PN4QXqW8AYBFXNoCCgIKEgA1VtCVfNCFTN/P/r/yf/D/8v/O/7j+Hv6wcnHhx+cODB/gd7Hux8sPHBigctDyzuH771ivUZ1IVEA0Y2iNfAbCYgwYSugIGBhZWNnYOTi5uHl49fQFBIWERUTFxCUkpaRlZOXkFRSVlFVU1dQ1NLW0dXT9/A0MjYxNTM3MLSytrG1s7ewdHJ2cXVzd3D08vbx9fPPyAwKDgkNCw8IjIqOiY2Lj4hMYmhvaOrZ8rM+UsWL12+bMWqNavXrtuwfuOmLdu2bt+5Y++effsZilPTsu5VLirMeVqezdA5m6GEgSGjAuy63FqGlbubUvJB7Ly6+8nNbTMOH7l2/fadGzd3MRw6yvDk4aPnLxiqbt1laO1t6eueMHFS/7TpDFPnzpvDcOx4EVBTNRADAEXYio8AAAAAAAP7BakAVwA+AEMASQBNAFEAUwBbAF8AtABhAEgATQBVAFsAYQBoAGwAtQBPAEAAZQBZADsAYwURAAB42l1Ru05bQRDdDQ8DgcTYIDnaFLOZkMZ7oQUJxNWNYmQ7heUIaTdykYtxAR9AgUQN2q8ZoKGkSJsGIRdIfEI+IRIza4iiNDs7s3POmTNLypGqd+lrz1PnJJDC3QbNNv1OSLWzAPek6+uNjLSDB1psZvTKdfv+Cwab0ZQ7agDlPW8pDxlNO4FatKf+0fwKhvv8H/M7GLQ00/TUOgnpIQTmm3FLg+8ZzbrLD/qC1eFiMDCkmKbiLj+mUv63NOdqy7C1kdG8gzMR+ck0QFNrbQSa/tQh1fNxFEuQy6axNpiYsv4kE8GFyXRVU7XM+NrBXbKz6GCDKs2BB9jDVnkMHg4PJhTStyTKLA0R9mKrxAgRkxwKOeXcyf6kQPlIEsa8SUo744a1BsaR18CgNk+z/zybTW1vHcL4WRzBd78ZSzr4yIbaGBFiO2IpgAlEQkZV+YYaz70sBuRS+89AlIDl8Y9/nQi07thEPJe1dQ4xVgh6ftvc8suKu1a5zotCd2+qaqjSKc37Xs6+xwOeHgvDQWPBm8/7/kqB+jwsrjRoDgRDejd6/6K16oirvBc+sifTv7FaAAAAAAEAAf//AA942sR9B2Ab15H2vl0sOha76ABJgCgESIIESIAECPYqik2kSFEiqS5Rnaq2bMndlnvNJU7c27nKjpNdkO7lZPtK2uXSLOfuklxyyd0f3O9c7DgXRxIJ/fPeAiRFSy73N9kktoDYeTPzZr6ZN29A0VQnRdGT7CjFUCoqIiEq2phWKdjfxSQl+7PGNEPDISUx+DKLL6dVysLZxjTC1+OCVyjxCt5OujgbQPdmd7Kjp5/rVPw9BR9JvX/2Q3ScPU4JlIdaQaWNFBWWWH0mbaapMBKLoyJ1UtJaM/hn2qql1GHJZMiIpqhYEJescOSKSV4UlqwmwSQZ2VSKksysYBJdqarqZE0zHY+5aauFo/2+oFmIC3Ck8keY9zmnz2r2u4xGl99cmohtpBkl0wE/9GD+qsXn4hJMHd0792JkeHRDKrVhdBjT+zLzOp0AerWUlaqiYIBUWNTHZ1R6SqMIi6YYEm2EZobPiAwv6YA2js9IdhSmqqoxCSoOATGhkoXDl0c1NGfieBp5ckeM4ioUzr77kGCxCA/NHxF+jVGUYjU8P0HVoyEqHQN+iSXxtBHokHhzPD5To4gZDeFp1pOsC9jjUo0yMx2oqIwH7LEZrYrcUrpT9fiWFm7pBJMTbiGxISqWnZRKjJl0SZk2PN1a4tPAB/OSGQZgM2akRhQWE65Xmx/7ww8pa1grxiKcqD8hRdSnWJE/8WrzbX+YItdNcB3+LIyvm3jJqT4lxvhpNqY3w4PJbx3+LUb4aSHCm/Ezpt0lTrjuIb8D+LcY5qcrwib5bZXkbfAh8fwfJskVeE8dfs90Kv/OenydodL6cAT+oVYrq9TpeRih2xMIV1RGYvFkXao+cr5/YqsLy6cRtaC42ZtM2OPmZtSAGK85HrNaVExcpQz5GThWeRmQWW1N0uxlOBRGZjgr8Zq9YzTzL6uyc0pF+T+NK5ym8GZUvTlcjMb/XcmWvbHqf3jY7H9tKufMaCz7D2OsUwhveo0TUAJVr8r+A/oNq9Xy6K6QD6GHzZZsA/obj1qR3Q7n2YOuymy9IKgU6L7sVrsJ/a2hHt1FwSx8MHtK4VceoxqoZdRK6m+ptBVrIkyKdk1GDIJAh6Mif1JqFDJiIy/VgRRrOBB3TZ06PLOSo4pBWUMxsYaX+uFWRMhII7KAW/5j9hksSIUYAkm6Tkht7CnRdoKdtrbZgMshfrog5AKmB/FvsY2fbsfXGWra5gq1Eba/aLW5CoJt7QuclRpBCKIyJenq4FWbklbWwGt3SuwXRH9KjJgkrxtmblV1C0rAhFXYzRGmFiZvC8IyULmRXaX0+yJ0iHGzeDIbEeZ8MoLMFjdtN3MMaob3w/0HC/SCpjBU2z2R8i67fkdr7c57tmiQ0Vii3/Fgm13L68taN3a4q7aM99cVN+5/fKceGQ0l+mPvjFau2J4qWnHxihBKDl+zprJm9f7m50uNNl9pwMXQt9lqR46u7z62s4X5Omf+vmqg1S94y4Ls3EtGX1nt8g1NYw9e0s3+1GD+s3KS+X3L2taIha5VVA9sOfPXbN3aI12d69srzBTFUuNnf89+m32FMlMhsB2dMJe/TKVLYQanW7HZ62Uz6QqQYprFk9nPZmZWJVpZQ1haBYdOIzl0shkkjhMLYzFmRAsvuUF+WjjU8lI1HHbBYRcvDcJhA0zbCXh1WwRT2siWplIpabALjhOtlSlsKVf1gtFsqIbLficcaakUWE3zOVYzQieBx/FYM40Z7PdxtJkIBSn96DPeOB4dPtDSsn+kqnrVvuaWA8PRwUDTcCQy0hIItIxEIsNNgTKFUWnius783mCjV1atPNAK745Wj+xvajm4smpFoHk4GhlpCgSa4N0jzQHFwMQtayORtbdMjN+MX28eHzzQ7fN1HxgcPNDj8/UcODPJ3qPWnt5lQmMTt6yLRNbhd05EIhPwzv3Lvd7l+wcHDy33+ZYfAju69+wH7GGQRSs1TF1HpeNYCo1YCstUmbQBC8ANB24D2ELKbdOALxohXG8Dn9PGS2rgqx/mlh9MHByawNqDtSvHcwms/Sp4dfoF04yBbVy2ImBPiSZB7EuJ5aZ0qDpJeO9eBrcpdXUS35a5Dgpdm+OpXYk1PhiKMJiTVovNDlxPYsZzSIWdRhRxzGKmJ1EwxDF7a9dd3dvTU7P5xpGuy9YmaU7vMKg5RuVvHG9s2ra8dPVa9K1IUk3r9Sm6qwVVrzU5+B9F9l37lZUDX71k+dbGzYfrl199YH0oW65kO/f2l6GLem/cP1Y4fP/Y8ssm4tGhXSlGwRp0BV3N4WDXhrpV949lm3of7TMYN31vffZdtfHvayfaAvGtf7Fl8PBgyNswWI3+nlUVDW0+CK6LQth3IgPxnX7Zc+bcJhJ1eZ9JfvRLneW8h1zkF+HzvpH9kEbKAsoJMwqJLvIZBvj7AvnvMUvtNrDeSuCgCR8ZUYT5hrttajBsUF12xRWXq7jw4FSbm77hyL/+8tdHC1RGre5vsmv//d+ya/9apzWqXUf/9Ze/gudMZj9EL5HnJOTnaE+KVGzGIJtRAy+xsgrgB0sGLcwwWm0HKYusIDLYrtlrkglTbQ0dCoZqWpCbwVNGFQpOqi+//IqjKsSFV0y1FxW1T60Ic7/Q6v4aPflv/46e/BudllMXHP31L//1yJFf/fLXR1wqzMOrmHvoNHuKqqWSlFgSndHoKRXmYCIqlpyU1LFYbCZA6JK09lhMSgJFgRLBNM1yxWWgaZgvSTtY1AhqQnGrRalqBpdnBz6DmfUgVSiCQm5UhPy1NYkkh4woBFoHihm6quAt3sKpVbWsWm/l33KdMBaYTC7+Lec7RqtBiS/rbMYTrrc4l9ns4tiByEGt2WR2m/75n0xus2DRHIgc0GhpRqM+ED2oEQRTgfDP/yQUCEZBs7/ygFrDMFo10ZED1CuKasVfUjqYlyIVFVVxCSkzIhtLUwjjEkqrCacRhQ8Rg6elnoiDjkkasHyKWFqjxfc0KnibVoMPtZQGpCKrRK0XlMpr9Qp+4QB6eQi9ku0eom/pQ9/PxvqyVegHsp4ezM6hIPUNqoCKU2knNgqMHsxuIVYwkQPIC3gU/xQBc5UUuDIbTGjGSXwchp3gxGw5EWM2NjNJosYHq0srqmxlKb9RrVRoi4udCqVRE6xaE4g3VpePjazwGtVaVqvQlibbSmg6LtOynU7QHfQt4PF9mB8S0mTwDxIVUYlC4RnGimcQ1kB5fNbt6Od0YmQE/+0UYOsyGIdAlS1C1vkDhFH0ArrGSI/6BGieOhcpnwuP4Rlnz5x9lv5H9keUmjJSIhNFoiYqacknqVAC/ASMnKWvNJaWz12v9gqrlXTwNGWxUATL9p39UDGe84edOQqdmkzO/6mBwlLZ0xkWPJ05I5XlfFoO75/ju0zNCKhHJquFxjyPoE+4pb6Vd7w+NfXGHcPDd7y5Z+r1O1ZOdh66d9Wqew915l/pd99E9hfHx1/MZt58M5vBR8j+pnTqkeXLHzkliacf6el55DTm7yxg8RD7TYqnAIkrMfUqFaD+GLFt05wSqUE/haioBtNmyKQZNVZHhgXNVDP4UK0EzTTBaBg16A6CsSAODnR4JIjoKehrTRJ8rS80ix7vQ01zVjTAZN/SwrRRNKFDpx/q71fc4w9lfwNmAFHXAz1h4GeMWk+lKUxPpTaT9mBuGrHKxKOiS+ZmeSztsmASXDA5MG+12E4YMlIN5jHmLevBvK0E7ZYU5WDKjMI0a3MFiLOKY63OYS7MUuKr/KFmJq84KvBWcW/MVoSu12nQfzbtGqioHb+4teui8Xq91kMr6Wr9wOH7xkfuuagjtvpQc7be2x2gD/IWv86hRv/VfPjSK7qHLukPlPfubAog9fovT9ZUbf7y1uHbr72sJVutVpv5FJkb15/9QBGF8S6nbqfSnXi8HGgP14kHxoFxSMeIImkAPTk6Y3n01BMVK09KpcCFUlmnkiAbdxL/kdsB3HDzorn4pCC1ADt64XZpJfCAUQMP3MI0F2vsxGZUcoCkJKoFrjoFsTEl+k3p8krs2rGBxQbAg9zsvN7VnsusKFrEKzfKI6jrQ3q9zsKqlbZA7cDOjnW3rY+Ub3nskg1f2lQdX31Rc9dFYw2c2q1iY4b+w/ePj3zlQGvFwM6mRx9ffuXxySue3N2Atgis1mgxJesbIoVNGy9Jdlw0XL2Mjgztbx842Osr69nZkmMnxkbdh1bXG92v3TF+7/7m9j3Xw3xsA/05yj4H+myjeqm0DmMi4qYNgg4ZwiITlwyg4GqILuxRUXcSwl1JC8gHjK8D640up8WCAQ6olIgEsIx5XbYowwjMrhfceRK0OpFso3+6BmkMxt+NzY0aBWYzvZdm0G+Zd2Y7EjpDdhN61KBL0H8SSi1E1veCrBWAHaLUP1HpMJa1msmk7VjARdrMjNcUtgOF5rjkVWfEYqCwKioaTkpBEGJ1LnSd+yOJbEQ7BDYQ0UhFmlOc6D7xquFXb92Ib7BicURyF6nhGiuZbXDTekK08tMWq9kcflX7lRO/gnfpQD+mPe5iczgNv4tvLb7VrwRVSKXhXfBCzVhtbosnIgegGqvNXuQ2WzzFiwNNBFSB8jiceIaZYOqnKSZINEeOfxaZK6UqZMas83sZYtjmwfa9hVqLITY41b3qy3uaIuvv2lR/fU/rIfq2AvfcH9d0XVZ38OsXNwzd/OKOxr2bhg6WGj0l7sT2ezauOLa+BpvG68othdkiwdh68aMbLnrh6g5rIIrt8W3A4yrgcSFEJ2DRHJjLPnUmrcQ6wFU4lDCFOCVMoWpilotgChXxUghEbwY2x+A1VARQQ8c5VGSOVPjw2Mw6eVZgmyF7BNW5Y1lqoW9bvRXdJvhXZ4eKa22NT29Z//Ch1u4rpV3bnjnSvjG+7oaRsTsma2s2HRuauHNLDfr70ZM30BbH3PfKewPN3U0HHt665amjHW2XS2Mrb9maTG6+cXDkxvXxlq1Xy/70BtDxHpJvci3ScMmoJf4w5wSxHwVoRMJMlEiCzt7A/LVKObdTXWhvpx8ymGbf0PHs7pYKwaU5/TPeynoKrDz+fIa6HHhYBjYpBJH5IPUmlfYTOwyxBEnR9CkzM21JvxF0tS4utangqUOEmbI9Ehux5dHCsTYqNcomCvPVbchMW9wxNYQncHFZFBtxaaWs18Lzb1+J1ZcTWV7sOCGl7KdEJwTsdSknCcxZZ6qDqOMM66yTD0lQvqwRZGX0VyaJrJLYyrnBi0p9bXBk0abmoxKmdhEmUMno9byR4ZLzyyOrLu5q2drur9/7wOZND+xt8HduaVl20arosiue37nzG5cvm6zdcsvIyM1bEsv2Hmtqun5qWTQ4dNmqkcuGSsLDRwYGjo6E0dVDV65r4k2tY3uaB26aTKUmb+5vmhprNRmb1105tO7uncnkzrvX91wyGo2OXtKz8er+4uL+q+md9XtHY7HRqYbmqaHKyqEprNsiyD0GcnGDdwTdNlP5ODuizsy4AmYcXLtUspMEcXiAzR6eQA1tzi2WeTCMtrvMhF+RAOi2lrKnlsbMKgSGDkdrBH98gkli1+XHJzc9dnGrPdJenr3e6B9DX/fUWBuObxq/Z2/z5tj4Vf1rbtlQFV93Vd/QjRsTCuX6Rw63tx15envdju1TTXM/dtCrwwOB9uUNU/dNDl0zHm3cdKRpEKZ1fN01BFPdDZhvmPkF6LefqlxAfaI3Ktkx5gsQEIsNtzUjFpIXqeR8yE849/Ru42IgmDz3bEnWdGwJSiR0AaaW6aqkOnIW3Ap0GaMyFo1ERdNJiSqGmMUBlGnJixQFvjtM8+kLSrKGwbU4PpGmCJovBLqX0K08PwZnrj6H5DnqUzH5E8jIPKEYBD9JmWsRsRRKFYToOHB6gqH0/Nx3fKVhD50wGugHytGtHTpek/1XQavhs79UC7oOzI9n0X8yp5jLSD7dJSN7CHMA1LNYCdVRSTNviRD8PMsMzkrMIPrPvj7U2t9P6IB/RgWS6UAEkiVwpIaCTQhZEdIb6WRxmSUgzH27gKGQsUNnUqFiXsNyauTmbB3ZS8qBDt/ZD+kfwLwopeqpKSpdh+US0ecwuBdj8IaoaD4pmTic4Zi2m+IcTAWQUFlUiltJ1qMQTxKBpIglkxlPEm+kDic94oLIp8RCAOrE1XkjcI/SmoJyxmMeAimMyB8CG6PIzxGAu0vE6yvsGtlSv/yqTXVVvav7amh9B1vdM9pTHe7dVNu5pTOkMqpf5FzeRZEKGy6Ml9rDQxctX3FgtK2u3vfMN9nylsamgcmu5Jomj78ioD8zcB493X9WryxlR6gV1Gbq25TYG5Va2Ey6pRfDw5ZOgIfGqGiNS2FFRlwVE9dHJQ+bEWtBbBhabiG2ox5YVc9LLmDHIMSkgzzG+DNBOVsQ5KUqzC8uI22V7XdT5vffku33OC9OnJD8ylOi7wQ17fOPTxC7PX9EsINpUDC9yFo9tS2964GRUlUQT4/2bjI9jC0ksSqth2nygpZymarqc+klUyKwiJ8h2TjJht1mZzjQ4nPsFMIpE5siHktgMOtBSoXfFwjSJfl0kzmCsKT2H/khsj9yy+xbFzfsvG1wYi2d+otVqVV1Be3XvHZJYlNwvV5vD1a76vcMV2197tfX3D77xoGL/w5pvnrvme0qHafkL8q+/8zx7M/+8Ur0nqWssaxksKfFNuys8a+7Z1c9HXsOlbx32ejx008eePn6no3jG0dLuzYk13zz9jGTKftQtM9dWefVNR36y8l7//VrPVPvZD967IXs+69sXNbOcsH+4anvo4o1Zd1xt7N13yhqUqn7jn4NyxcMIusC/28AjFshR0mAa2WYq+EogLmSBs9AexRj2lxEZsZBD4qTXBSD8/5+sxfBVAMoY6RX7qJXruTM7HNzdc8qLMYP6VuyP1VxahWnYo+fXmM0oCeza3UCzdE/EyqdTpwJxjjhPfBHXwM6LJSHKqf25OI1K8QvBI+UQ9BS7CHkFGNywkSzrGaMbQGTkqSj0ZyZVhmdAAqCcD0YlVQQHFfAjaAVaNaDOnjwgTElFgtwKpabRBUeiOBdEnqUeGMJIneIN4kKBP3e99BjV7xwaX1p/97u515pv/LFi7NfRlN/9U7Nli+tzX4FNUzetTb86lvZv2OPV2+8dU1qz0S7yfXNv1j3lR2JVU9+tWtff9lAfNWeui/fQ+zl1Wc/YCMkLo1T6Qgep1ubszAW7bzLdVqIn6Uki1swzWgpQ7DsXN2VVwEUckY0p4cYSXrkXCiir97xOmIfHjx2cFtVsdqkKapoXn2w+/pfPDIx/sBPrlhx2faxMKtValVllbuvumfintMzk/S7TyL+r/fYK9rDEb21OFhsXXv8w6/e/+HT46COIYVSVVE1kCza9TYyEdsAMmMfAJnpKSdVl5OYgclJzMlk5nOQIA6DvHSmssjpSMmJY6J59ucTFCXe/JTzvkfzD2Rf3LbtxewD2Qn01LGf4mTET49lJ9jjk29k//j0M9k/vjE5uvqJ39137++eWE34inWoAejRUd05ajR5ahRMZoZVE/1hMWF6QpjGLKfISPpMowNrRsfkXFkuQSYnx+Sf95jJOSV92dyN9Gn2+Jq5F0fnnlhDnfNcDdUqP3fhmWqWPFONn6k9zzMhKs89ULfkgfLj7p6bwg97ZM3cdmped7aC7tRQ+6l0FdEdZkF3ZkrKqjByK8GOqjavRqKTl/zA/DAE9v4wfq6/FJ6YwDl7J1hLga3C2dmwIBm02GqWgMKJ4ZRkKSMOyuA8j97Np+JziocD2SbkFbDqgWG8evsbyPD0yO1Hd1UVagSN2tiw9Wu77/jNo2PjD//LjX2X7d5Ylf0PHY++lDh8w33rHspmX91Ov/sMEt7eZatoK680KpSV1aGJZz685/6Pjk8YPRUF6CZOk5qbCzaUWnPqJ/OdrSXybslZLpVsuUQ2PsNoCecZ1by0dWYcmos6sloBMiD2IS9nvCgfx/G48N5u5rZdu2YPs8fn1tFPnF5DvzjXKz9vDn5th+cxlHeRnHHqkWTr4dPwDzv/iXO7sMWT/3bt2Q/o78LfuiAOkiNJHZMBWkQljnAoiCoF8lkFZJnSDJ9TiKeJDqdTmZSoFEQFzqWSVY/5mFhewQcrvJZmEK3nNK5AxL3iyrHI7qb9j01GNhq4IqOGU6lV1dse2Ml8a7b+slevbuUIPX8C3vnY5ygflcrxzpbjnQF455V5h7XITwbnI7yTApgmxgs0mVLyGOXFFrIERnLmduIUUIQJI+FPO1ebixwWPb2cL7SOzt1kdpttPoF+cLTAZph7QGe2e53rwU1sZrScjh7nublLLKBbLuvccgCKh3SCjp1blpMz83vgHZv3UBKTm9dIVOZ5n2aofDpRUi0I1freTloEMYjj8zqj3A+f5cnPVVHIjdsYz9dXeAQS7OBMpAA4DtdTmCDYEdU4I4kzgOrClDx8wArIZgehEA6A+uDsZBj5QshmFd5bzgkaerlRrzRo6JRa4HrWK+b+hivgXca5Fxn2uNIwyxd5eS/H/N6gPL1G8eOColl9QQHzX+6CM5WL9duUt66iLkerBmg1E1pNAsGceP1NB7RaiI/GNCqNi2gMYlXx58iKA1nMs8y6mIObHQY6VPozDk+h4sTpNRbFf3gKzjRi237V2Q/ZXy/NRee9lF+7kIu2LOSiLf+7ueirtr2UvRes/uQkWP375l7atmf0gZPXHnvvvlWr7nvv2LUnHxil330arMTuXe9kfw8e4Pdv7wJrIDxz3wfPjI0988F99374zPj4Mx9i+kG/FfuIb7JT7Yutsh2QhM5A9FuHk8AOMgw9dlExUS97KRamnxNz0o69FCt7qWIFAQdeJ5oHBX9Cl1BnEdN9w19dmv0D4jbds7vu+9/N/oE9/i//sPHRi1vnXqYfrN1wTf/TMzKWvir7ltIDPMX5pMF8PinP0wrtQiLJMp9IwjydTySxVoeRBNs+B5BlTYkVQlprpFJL2YuDbjILP4vNFcOHe9HRMYtPn/1u211Dn8nxfW89fm0ku1fHoRUFhefnfJ73Pwfe28G6rM1prkHWXMkH7Lc5CPttqnnzYgf2O2KiXVYkzP4AViQ7aI9JKy8cCjjJbCP1EqJPyAslF+Pa8mYHhZETxRfkc/DMn1NT92xymtFHa3mHLlsllJa/Obvpvl113307+zF7/O3XRm7Z2a41uubugPiwz26aO0j/PLL6aP8DX5XtxfjZD5h3QWZN1D4q3YAlpgXbo20gK2k4p16ER1UK10qL8LVSP16Ea46KjpNSpSEjVvKSEYaSMGSkFnitdJBVMdEovKC1FJXEGnBcmDCJxTC6Ui12t47iBHG3udqPnNyU+dBEpVT5ZCmC61XmwpfxIj2vKSqr79vavPqmDdUt26+75bodzcndD00enO51agRD+fKpwcFLV5Y37yB3mi/9+v67/uH5SqMjUB5w1Exc0T2wtb0ynBi+YkPPjTubu3ujAgpGQpUrttf1buqMVCaGj4yvfezSzm0yTwIg31tAviqIkck6jyxaisGLPThYF5UnsRDTrBKzhMVsUrL4UInXHhciebzuGFBsyzI72aHx8dMiO0Q+/ztnf8+a4fOdVJJKW0luWyvbe5GL50ElmHxcUAb+W+LNuaVmhkyL3Fq5ZYmTjNDf2dV08KmdO5+8qHFn313fvfrq793ZT5cx18xeu+2b1/Usv1bcBsfXHPnB/WNj9/8A04FjIyfQwWN/z+NxUrKDxKtY2D1QEsXnYKw55wsSOWfoN45ADIT+02zQmdDvWLNxeO7ZDexxo+HMimhtslKR1gkADcBSU5Tqx/CMEPVzKh3Cz/AUB+PxOHmUxLnjcWxpsV3FsfHbH79/guTsqQgnKniR4iXGcYqFQynkOPVq4+/e30VuB3HV2QlJy58SdSdefcf3fiqf0OdE7wnJrD0lmk682lTxuyr5ugfXNvHY6Tl18HEumIe6UwwFGq7Q6kxmp8tbslAbhlp5Kn/d7Sn2lgRD5ysfk6gQYEuVzS/bp3gMJ4TmfWXMds4p8qNgSAlmS1jjVqN9Sg3L6lTofoWFK8JsvF+lY1m1Cu1lbNxQtm5DdpVaqdRkR9azxwvPjFuiLlfUonhaJwB7xy2VLmeEnIFPzTgLC51n7LLeAq8Vr5B8fnDB99N5tSqKYuNDSTT2niob8Z4aRMSap1IjWxmSCfcLtD6r38FxLHqZUbPouJLTTWZ1tGYHJ7DZpEKbbVWZ9fT/oN/Wa+ZuVBvV9ISam+ucMwMmeMDIzV2nETBNLqApTeLeqlwWlsqDEaucaALltuUySQSBUPJBXuUWMxGmk2steHf0MGdVq60celhp5tbNZXazxw2GuR2OCps97KDv0xlnn597ll6Nn38JPP9pEv+7c9gKcClZ4ZADJS6K7RdFFjmTyIsXAlTIa71Ez9w/e7HCzs3uZB4Omk2sak3AZjk9uwZ/5jQ4w1NKAT4zSjJ5ajYjqqISYsnn4cmr5jNpNcFragOJunIPMecXxuJ4sXQaLTNxP/4xZ8r+QeUJGIRT23hDCYXO/vnss/TJ/Bo7tXiNncFahmWkLi810leWCl41+6PgqazZiunaB3Sl83QZohIDdCnhT3N0KQAGAF0KPaZLgenS5Omy1yQwvJNDHO8+HlPFo87s6xkDr3yA5wJ/xnUxP2DizLcIXsvX81CkGoVYRXN0AZzll7TlBIqcOMFZlB+g9U1owzKdif1Yw7Esp/kTyxuYOH3J3K2cFr0peAS+WMi2q3lZn6nsb5nQ2QjEI3ZcayBRbAb/kFoIOQqxgo1lQrP/+COCo8cUT6KvgC/TgF8majaj1FNGXC1DQtMZ1koZFPlI1EzWbDGBYxucDv2jSb1Jzb7Cmf6o0mIfvw/84hqFHuxWkrqBShfg2eSN51Z32EzagiiSOUpryLq6htOEZ9i434IDcExi3aJVHoxwRDYmuXD9Mi8VGTN4MqbwWjNmlpASY0Kas2BDIhaZRDdMgjhenqHcqZSkYclb5Hx9Ert9kjGNotyimoCPlxSHQZS6r+ehj5+/7EjvjuWVRotOGBL3D1++sizkUXHlIxO7mmu29kU2+JK9pQ1bR3sDf/Hjm1s/bts3XK3Yc8e9ZdVl5qKh4ZrNt47O7Sy6rqy90u5u3dob76uyuyItJUirCDSPEhwknv1IwYKeWkAfVlJpDvOIiksO4IoSs6dYlRFRNLcGgau3JVqIkXQWrqTRGMhKhFRkxWiew3C6GNBDWiMwqRy0F/AYTbkYMARhedI9D358SpW4pTN94LUf1R96cs/u++uUjCNYf+e6iZvXRp55aNsTbeyP5i6d2Jmdy84eeOvO4ZGVV7p+MdbdfuTpyV+f3Lme6NfE2Y+YvQodRF1Ncl2mVACks5h0AQ4E4tIFPQY8lWQINiA5gpVcKAAoo6aK/fPFfAS7yFnWxXmD+WwVPdF8+Ln9Wx9IOVmtWhtoGG8du3l9LL7u2FDv1tagzqAucCyf2FW/+bGL2lD28InbBloSflZd6C1oPvzUjqknDzX6y/xar6c2ZF124zvA+3Gg/Rs53q+h0iY5eiK8JwPwAO81i3mP2Y5BhJqLxSRdjvcFmPesCfROJ4hGnEHEEqDUxkXLXDY7ia2iBG3TZosNJ4kFOR88Dryf2nFP3ZaES6HtfOHgaz+aJLxvuGti4qa1UXQGs36gh153OlLw6LoppEAKzH3ataa77cjTWIewDF4EGZSAf5ik0l4sBUt+EBXKzEyQ8+KMT1AxHz4YDbjiWTTmIgg+F0EYgXLW4sWTSCtIzkKsUBwuhaXwcUoMCgCtFy8kKf3eT4op6c0FERMth5/bu/rLU40Gbs6T2HLb6oGD/ZU6g6rAuXLrodTOr1/eMUk/Wjl8aNnglWvraNO+V27sbzj01B47b7no+UsavOU+LK2gbfnt3/7J8HUT1bF11xKd88Cgr2Rfg9c2Kl2IpQZwrygu2ZUwV2IYd6lVGUmHRwvBeiGpdCuAAdti6YJCrI8FToCY3hzEjC+GzcQyFCEZdoaCnucrhy9aVtzqZJBZX+6JjTb5UF/2pc1fcjPTpdeuuX6sQqeN4pxG+66Bq3pm9zFf0tJyrnogez3zM7B99dQQNYni4LexMDYpM9N28yZ1WHIpMmIiKrUCyX1RqQI0LRyDQEdajQ3fNiKjBj4jNvCSUgc2jicr3StxHoiDaB487kqBmMW1OAaCQzcvdcFhtZBJV3fhMVY7YIzbZUj4pw9OPCkvl/Tz4vITUrn6lBg5wU6HyyPm8KunzCc24SqN6Up8Cm+Z7ulfbg6n4XRRrQZcw7UaL/SXV0aW9+RQ3ov95eGFU3mxZW2pYGrVMGabX5doXb0JBy9uQSwATeprBU2qbsDBKISlOGXlB6tVCmerBUlXAq8u0zTnXrmWWATwp7nq3vkiX5vdiwtS89U/IbIEozzP2roixDFLl9YHdq+PN/LeiKdnZc2mm4Y7DlYituj+InftxhtWji0PVzdtv+7G67Y1tx55dtfUY/uSayLj165acePWVHzV3iNHa0LtVa6Wku7tbe3buwIly7a3tm3vLplaebhYaK+3RSNlfPltG3ovXR0tdvtctC60Odl7ZDRa4Oz0VERtSpU5MtLZcslEoqJvS0flQJ3X3zJWU9XgNQBANZbGGhkqtbGzpKRzQ738ulH23U+BIv0d2Ccr1ZXDovq47BWEnFewzVsmmvgEHOnoDWTrjGSwkjASDK2cH1zwBsTjCbL9F57a3P3CwVXXrApvOXbT5Nc7weJfvmZH7eSd43OH6dvuenzHxJwC25j7gaBB9gXKDDiimUpb5msBjPpM2opwms1xzsYjC9l4ZDeQLIlkn8/3fLJaHgdi93POYrPJ6+B5h9dk8jq5ss3shMnn5Dinz2Qqxq/Fp19mzsyyFH3277M35mgJ4ayuk6SbgAwtwnAdMJsGMFuMZJ80JzE/pu0aCwfzxConn/QaIMbpJ8QwpPAMzPFConQpfXEWGdRu18jQZk/j2mZ39KWltGYfrNarJ0YUV545VjvREdQqv7OEcpClCLJ8E2Tpns+lWuJpHRA8wxRROpxIZWWReggX3USkUjHJpRaB/Pj5XGrifKlUBHhY3FLFOXl0r85hXp1t1pp1vF2PfjrK2fTZVUKRO8r+aPZitRFdrzNmR7UmpdpumMvqDOg7Jm4uS/TtHfgVABoZsKwyjZigXOYaBIl/FjLX72xmf3Q6ktNT9ocEA+zLxQcOP0SnCEYny8QUl0pBY4tieRBQYcALHGIFT3I4fsP8pgCHjA6kCook1cQAdjhgJkQDKRo04RQIjr1YQz5z6SF1gTZ7bmk8p9jcOSpeW6DQuDsG1lQduMFh6li9rbb/6GjllmuP1G7pq9h86cGRO5PMGddXyrviBddd1LKuqSi25UvrsPp/7cHgwEX9+Ojuh7eOzWbzcxLGaqcGcjziciNV44lpVs2nC+3yGO1ycofLT4TcwIwCCdTM1HzykAzlE7MTk77slUMLExQovW9sz5IJKmOZ00DXObnYPAbwq85bF2z49FzsZ2xVabn0+X37nr+kpeUS/Hppy2R07c1r18rbTPBrFGWPvHVrb++tbx05cuLWnp5bTxzZ/uThlpbDT27f9hT+s6ewXXkqey/QrQcbF6DGqbSQp5uwVIOJ94Lm4ACuZB4BszYZAbtz1i6INzNSctLMLUgagVRO4FUrvUUpozCBRCrnQGEnOgcIP1VrEJAG8NfrP2w48OTUznuT9XetxQDs6Ye3PdmavZfdqjM+tG4qOytj4b6+rJHuHlsug+FdG/BYxmEs34CxYDw5LuNJAibxNF9AlNxSRMlhIF8AiNKQQ5TcPKI0yFpyXkSZJOGmcCFEueuBpAYVJbZ0Tu/PI8rkl9cuIMqhgUOu0w/RRRM75xFlwaoegihzc5r+PYzFga29nBmfl4hFlwEbyhefiMo10k4yGpi6JEDDJstIVhfs86sLMusXMpNYs+MCj9TVTxyJrPBzjKC0+6qLL747wpzhTO9dcbvZ3MEjjVZ9101zu/JrYwwL+t1I/ZBK15N1WyUEjvUkcFRowulCTFkIroUIxAv5cMjRFBXtYG0AH1XIfK4VMlKzDIren3zHIoMiMy8KJ6So85RYfQJOpk1mAXBQlJ+uilYDDoLfi3AQ3CQ4SDCZo1XVORx0zhlBQRU4L61UgAw5YVpTGMA1JWKtSfL4sHKGNDiNa/fU5tK4i9brzsnj+j+Zx13rYPU6Q2nz+q62LW2+6qFtU9uGqqNrrlyx/ktNNpVRV1I/2pRc1xqAO3vgTtXaG0anHpjyqTXeoDfQPBKJd0S93lDDaGtisr+yNukD9+Qqru0OVbVWFntLG1c3dRxaVd1JeF579gP6QXYT5aMOydG7HNIVkJDOpgnjLUieuKQmsDut1uXr80nG3k08r6iKpfVufEOPN6G4Sd7EjQvo9bzEcBmcksAugMHLyTRwRifki9Vqk2Q7KVnoztkeHGFgh1eL0yy133Aigz6CWrMnrMG4u6Q25ODVBaEjbTsu/rLOyDwb1KO9Gi57ec/cQHljyGxzWbXhcM2hI/TLBhjb7aBP32DOyHbcgPUbJ9YkZc70iNp43o6D18NJZA1ojTFG7A224xqG1LiIelyvRUlImfPRJKssT8aFiC9C37712I1bv961JVGENN2vHBq9elUYHaBvmzt81xPbJ+jsLFtwz9huMOpULt/HfA9oM+Gcsonk+1Au35fPEFGmCyb4/K5+zqRAQ1ody+o0aJg16Xuzw6uZM0bt7M8c5TZbhY0J6DhAUvhZdvDd/wAIr5z6M5Uux/6sME4eJ3EFOK8cjuLyGDxf3tG+f2w+r8ySvLLCcIqFQ6nccOrVt3/4u5Q8nXy86DkhCcpTouXEq43Z9x+S88eF8GcOXizkJTve6OyAUFp96tV3yt8vJiXiAsw7wQLzzsdPF/s85vC0F/9Ow8VFsw/uwIvoTVGtOgUrmCx2h6fY64sszjwbqdydgkJPcfk5N/PTExhYjtdo/amlLASjGsuv1+LKa7wgKiff8KKtvZczMwipNApWr0YmlbXUrkIGo1ahUSNaXbA8+9xyXpX9LatmGDWb/XeluXOB7WE7E7bbZ9+NhG0VdibgnGVtTIPRY4T/Z//GllszYW4DuRfM5575eJpGueWEwihO+eRzz9bFuefEeVLPAXQg+/B6nHoOKzhkZ3ntRPZBdGg9zjx/l9Vm31PxOlqD/qDXZIcEC7pVY8ia5/4gaNDbFmN2o8aIdQP82feBHhvBg7IKitboQqEXZb2gFpJ93vYhI2jiGqVWweqUaIQ16/rmXlRaTMtmCFt+aywW+GKecei4029wJnQnPKMfeLACnrko15xPhZEqzwvkmvuN9DVzX6F/aZw7Rh8KCVZm80CZTZj9ywHM17bsH9AZpUAtR4cosT4q1bAZUjwKIbgtKvG5DS4tELu0gheO8hmpMBKLpVuipIARacLTndEWCGZUHfG4VA63PWG4XU72zJSnwJYJMbzrhWyYeOOjdfJW8NaIGAZd46WI5pQY5qUOzalX31r1kYZMIW1E9ETw9uNCuOnhJRW+WfxHA5kJWn5arVXBBNDg3zBhposK8Xxw49+vNs/+8XHytgg/XREJw/VK/BueNN3W2gGn7fh3Go4Xpo3YnkrDu/BRRSoNn7boljuVhufgI0AarbxKrdEWFrk9eO9/a1t7x9JVG/SSWlPkrqic36uen081oJXleG8PBCIlKdFmknTFZHbV5kAj9moNiKTuc8m9RbXx+BQv+BTN11jiP2kLNJTbzHZzqGeqs86k9lUsr3Gb7CZnebLInSh3wqG7ZnmFT22q65zqCcEbbeWN9JYWW3nKW7dnz5765j0rKsI6vSc1HKvfP7UnGWyJFquUxVXNwcTU3n31seGUR68LVwzubknB2+t8deV4HiJ99l40DvrCyFXG8yGQMUN+5BAIgX1H+oHsvaqjf75JxkxT2T/QJUTPrqPE5fLaQV1USoKe+aNSKKdnEJJqC0HP2kGRIm2gSO1ky2V7HehZU7tGTZpfYD03OEHdmuBd1c3wLq6JbNFaDuoWXFC3b390j6xuzogIonDyUjVoVIQo1qtvRT/6K6JuhojYFsHldc1ws42XtPim4Y8XET0y8NM6gxYUR49/v9r84R93k+tOftrlLITrBfi3WM1PR6sjcFqFf7/6VtlHPydva+anW5rb4Hor/p2GP1mkXAWpNLwdH0VTaXjbolutqbQe7/tNiTqsd1qd3uB0FRRGAEY1t7S2fVLvdHpXQbSqpfVcvasDPyxx7aB3SQH7Y79JclSmUrnlmEWql9uTgU9BAYNN89tpSP7Sukglw2iK1/gqemrcZpvZWZ5wY12DQ3dNT4VPw9d17ukNWWwWe3l9IFBfbofDUO9UR92vZUVL7d8LitZcVaxUFUdbSxJTU/sa8oq2Yk9zamrP7hRWNNBSUDhQu1TznsEKoj93odcVFnoOrO1qCuyspFVn0layNdeKEZMrKrFwhXWRBXNeM9/rxWMktUg4zOSNci2S0YNDCCvGmi4t9nSOxTEdAZrxXGBHNtjd5W0eT9Xu272tItgcdgwWN0+kavbt2VYRagw7EHq9bvPystLq0oLqztK6zd34sBAOSS8amCvHAZdzVCHY7jSDDbVenwFvhVdLyTqeNYN/pgvUOCFUaMD3REucZGStMRLEFRQCiXoGU6uHQ9Ei733CpC6kZJJxMBWC//1E6aIuNPNNaDYyz5cmOJevFO7VzS2b7z8TmZN75jyenWPOKLJUlKqnbpL3UoglcakWAjJ7LF1LKh5rCzVynIZXARIqnDAmpfwwiCogtkpuVhAE1FpbfFIQw3HJDsdBXlLK1eliAudnbXCgi5HK/mCCRPeSHaPDEhhdohZwP0cJxfNrHov6dXCI9Osg6QycSs+37GCSuZYdj7dd9fJhHTJyJfrxWxMOVmPy1Q2nKgZ2dpXq1GqF07FsYk+DfH/LXx5u2VS19pqhyg1fnqxB2Yv+6tZB+kcGy5/UDVEfq3a4C9jZa2l/qVfBFrtjQTv9Hm7F0X/Da5dOPnKoTcVcybRe/ATWyS6KUkyxLwPXLpI7PkiVTEY+ADea1uHcm0uTmaEUcZ0hLBbH8eqiWCIzLnUSR4QhvC8olg6l8nFZOhXChykKF7am4powZhYlVeIOJ+UpyaUAbeDNsvMgi6r5Dg+Li0oFeY+fQLbjx+UTvGVU6DILxxO7Htm54tLxVltIYxA4S7RlrHno0uEy9B+CIVvT22oPO5ig0zrr8bfHi+ibvEYrqtz4xJHOYNtYtZ0VipuiBbUbb1yZ/XGpzpT99torKhSKMmNRh6GsYagWrZD1CVEQNm+ASD9JraAwIiqDMCgOU1Qpr1wWn5QCoAkBnuSzOC5DFivxFqiXaLVgcRX5daROK14GV9Q6coWW1SJpl6PlpJ1UmytVdlVIbuqgCpFceCKpWpKNeTz2cORAW8uByMOxh0rC5SUPxx+OHGyB80diD5eUl5WwFX3bU6ntfRX5V0V5/GF4Y+Ch+EO5P4yTNz6cP/95altvRUXvNnh3f0VF/3bQhTWgC+3scaqYuliuTMvXusy4ChyUvJUUr2tYYzNuD7lgjEtuuCCAOnhxuRPePYXzYqZY2u7AOmC3gmHjY2mHHZ85XHgvcUzy4USZg1TNALLwLJTPEIyZT4B6reQ/XJBbS/5bs7LAgLaoOVYjoC24nCa7Ak1mb0GXZm/ZLL/A5eOuuTWWgOAL0cd1xtnvNx5pzB5FN8ELqUtb5PtVME7i/dVk+5cihp2/qIxJKrCxmnkMwMg4YACQAFMw+2+K9Uzh7G/kGrc7z17GXEP2Wq+jHqHkuWJTZtI2EinbBBhsNCo1wJUGAjUbEtimrycGp4fPTCt7sMUsADTQw+NeQ1IALpYHRuBiK1xsjWIwipsrbMg3VYilxB5BTIDjNYl14GOFVr3OzHhC0YauwaHxCZyDGDGRMjlbg2B6QcmVx4YmcrYosWiZZWnmQTm/4zoYSp6brADjpAB9lRdd0J0bdtV1L8pGBBpGm1Ib2gLxVXv271kVX70q2UUyEg822VmDzhBq3bCsZWuHv3bswMX7xxJrSrsmtmyP9LSUNI+s21Sxtp/+58GrgsFt/cmtA5WJhN/g9LiKE8tLo8vqotWp7k0to1cFQpPdJGNR51ervcFiX/NIVc2KxupYbffavvL2RCRc4fJuaY4sT1WWl9pDm7FcShU/pKPsEYivS6gaCu9O8sXJhj9HDL9IjC0GChuMiogsZ2CcbiGL7Bm8WgpyN52bG0WBJeelBkcRRDZ2jrMX87zbgVYaHO75C4LbwZp8HnziEXi33WCwF517Ctq35uwflEVgdwvAY63DPY9IjZtXkUmrcFFGWEEFFOGZsX6ryhCWxkCF+sewCvWvxCjSqlKHZ2rbyb1abI+ITs0UytupCuXtVN1CRuzmcfJ0hpO7n2A1CnaDObJ6VeHa+tExYqCa+gXTi1xhsIrqHsUK1C6I9bLzUuDiQ7wZDW8xWZofti822osX9BO5rf5yYmRN7aabnnh9+/Y3nrxpYyKx8aYnX9+x7Y0nbtpU27j75Y/vuOPUK7t3v/LnO+/4+OXdH3Rd/uy22vH+do9DxWl9DeuXjd42mUhsvn5wzVVJvY7V0MWNT16y5anD7fS7297EH4E/+s1t29/IH7+x/c5Tr+7e/eqpO+889dqePa+dumP7s5d18kXlhT5dgacgse2u8XVf2lpTDngaPmt5x9Fn5Xm8lxmmO0AWQdCWq6m0Bc9jjWJx2Yroi85UEJGIsegMS47ymytC4AVCcqMpFuN+B7gCvK0ihON4TgDkWi3AR/nwqqjDJBblNoFLToBsYkyQqKLFFSzm81Sw2HAByyfbG9VyaG944z1Ty/oqGssKdUaVoXpv1449Xp2O1bpiiZaArzlauMziDTt8qViF7esPML8raY8V0zUrVtqdds5eHbl0W/Zqtb7LEXAaTMGGisJSl87o9FvuZJcRvjxC3UJ/h3mYzKMglZsxMy4rpQY+FMdIaYEL4aJks6Mo10in1my32S0qBm/+NMORES25hBd4H/nYzSP1awaNVv+aCgluDp+rXsfnr6sEN23g0DFea9Trsz+xaNWW7I91BqOWR9ef97Icmz2D1jKn6J9QLFWV3zma746j0Mh7BBSkm1JaQfqMKKj5PQK4A45feIZZuYq+pS97E4qAGzxnfi6jBqknLzBDu7rJLOwCrNTVjT+4qwrUpTE2Uz1IblSz+e3sS6bnMjDt3TFxGS/14bw1nNWeM1lXwtW+ZWDErd6wqo3sHa0VIKoSgyaxEXSou0swzcC0pcitQUGs/RyTlhTVyeZ+SbV0AnQujD7/bEVfnXvo0euP6C0aFBjWGpXZ/6l2FRy894qj+44+9bnn59zzzG2XHN1+TFCZjdmbVFq0Q8dl96MfTa7fsBpkamFpmJddC31+2IxcQLjQ50d9Tp8fC5h9uoPsJV7PjNF/y75K1svaqfn2cXhvNel4klst4xZWy7j/ndWy9VUjB1vbDo5UwWtb24GRqp6SltXV1WuaS0qaV8eqV7eUKG5pOTASjY7sxx3d4G37W/BV8q7VbSUlbatlW3SAGlZUKx6CMRupjYv2QOOQBaCnqImlFaTmSsHhYEZBYkUV1nA+KnInMX4xGHE/krSBw/cMDKijNpbmDCS9gONMQDqCvLtd3ki90P6JeWu2Jd8Carivj97Uhx7NburLbkMP4Dm2lbmf7lFeRVVSvYSyMuCnJSpq45irBQp5x7r2pFTMZdLa4vk+U1EM/stI15wgmDyLIClZ3D0HV7zLIUDLfOMcucfbfOEeaWxI+uYUoa1KzQdFsaDNUVpb1NJrVVloA+Pmrt5YOdTgdYbr3T8xl1qR08nc71ALqo+KUvVN3kCt39STMiPEbtlVEOurLlvW1uh5j2UdYWIzJpm/oPtgPC3USgrCGckAUNYenXHIhr4EMH4Ub2pGgMRE00mxICYlABpWgaK05TeGpClFghh2QYynpOISGGRBldzwhlhuD3IzizreoPlRqhaqExehrwg96VGoWLWRYRSWksZIeWuZzRbtS65fZy+tcbf1mpRmFe/krlpfuSJV3NPcNxhsH6tuGkl5FSsMNK1Wq/XlJUUFFbVOX23QGqMHWv1xH9/eaEGMYssuV1VnRee4RVjdWT1Y5/HUdGEe/ETxJC3k60EVuXrVC9aDknZ7uEr1J4/pnI5NP1cLBsWTfzRx2TmtSrbDt+M1UuYMVYRXSM1yTQvIe37VRSwAxO0mk88lkLIW1zlrLx7sU+T+YaKGZHz0pvkVGIm3pS60BhMMAROxn1y8FLP8Gzsnbw6yTLXFkX2HrVu8HDOxYbCnYqIkK9kI3cmzTYpfQexjxrU4xFroNfLqFplteo6UAiOs7xzpqCca+BlKdoVUFOfecLsoDZ+RrPOd9iBq9ZPthH4Bm4yWi5/ZTf/bv6/JimO7jl/comgbvmFDfNWp3yodp37L3JWavAXTcRz9GR2hvwV0RDBynWH1lAXcjPxCHg9C0VrJRfll8QMXWajjfGGJxRYqFITCkM1SUsjTG+bPgoU8D54DP++m7N3op+A1i6ijFMhmRk2UP60mi4Bq0k0OpCWcnDHJ3ssk9+/F7W89ub36sd91yjlKIcKJ/AmFZHKd4kTzCWqaF0xmktyDcD+/VV/A2aoCbF7VBaQlUq45FIGOpGNpMr4QjdykVWlZobDMXVPvirWXhpvdazcWxrrKyoeyf1Wk1xl0lSGX12Zgb9nCNzd6qn1mB4zpPrBTHcqjYEF7KHD8Myp5QjO4AzMelgrl7KWaJH0v0IRMWNSEDNMYF+JWb21cSOLJG7rvpw33ZK/4S8VX1Gqdmn39jbmRWIwuC16rRFpix8eZQfoJ9iWQo2fe/xQpiP+x5woXF/qVuuR+pSSz51rwP0X2T/E/NtlngzEZLx2YWtY51V9a2j/VuWxqoHTFnn27p6Z279ujONZ9cGU4vPJgd/718PXXH774hhtkXzMD+O6XgO8sVBkgPCSWk0BYG5sJyo41jOMFmItpJW9NkWqqZA1etMUdNZhgbU0LMluZULBk0cVQ/uKM6nUlXqBUvq4yuT/+2C0ghfo1+QpAPvnStE6PKnUGBcvpUIXOwGv47JVc9gpeI1zoBqZbQcFEYb/MPg/ydVKl4I0el3fmiP7czkhLXAryuHxB9MZnymThF8XSZUEs27JCTXhGpeSRIbygGMRzfZo24BXiAOh7eWzGn4NxMdKJJachYkBIuwrKsCvwk/1HUlmQtNzGu3YrU0v0BzfzyC+j+UsQvmMJI6u/1usjjcCSt/y08WvZK7F2aXSqx5i41mUJz35XV2hCZ9CuzmuFA63ZaQfdjkoYxYevz6ue5kyUvUEwn77UxJ1Cv856S/hvfYsvQWscRXLNKubbVI5v3dRjVNolr0FKHWwmz7mZsloX3phXBji3rJYwLEIY5lrCsOWfi2FSPbwhQKo4Ai6YVD3nsGzaGqttJUFohwu3WmoF9pUJaU+sPtc07kI88y4FDaoLgIZzGHmAqdE6rTIj6QGl+kOAE1Y7hhN9FqWVttIO7hqAE/U+gBOen5jLLMjlvAB/nWqeYIxmjDGE9hYzomnFlp0uDDK6W5sAZCidYayro0RX01Qb1UdNAKJ7jUq3Y66PxtOVmOPL4lKxIiONtRN9HYnPrJVZPBhLryUR/9oVwH5DU3slCAUAyozDjg9zIAWJm6JiwUmRj0kx3IwG56fr4CDGS6tBW9fFZkZlbV0RkzYD61fXwWzuH1iL9XRUELuB82vHQBr9KbFJEDem8pimLodpalNisSldUh5LfS5MU46X0s+Haj5d20fnMY+5pClS3lIOmKc/sX6tDTBPS79ZBbZDazIS1FPn7W3qW1GCUc+qOl9mYWYI6A9LZgZzXQ4SlQWLCsO1LoBEFoBEbf64V+hJWEBgzJZdzmqMiczCmo7qwZTbXds5+/iFphBIK3s7/Y8KHVjLBmoTlY7itZCUPgNIUbLjbfKNS3dja7jMtF1dzoWlGmtGaoIr5bgnP2sE7qoFXM6mMU3bS6IpMgdSdlw0pC4szpVHNytaUNyOQ7mFEnxbvgb/3E7TwXB1z+r+GlrXoYQD0gOopntze4lWo1G4SJ+g7qs31SEf5/JZFlZX2lbsG6yPJ/xPf4MNNyUS3Rs7kmONxYGKgEpZWhgvdZQPHlLUfqIfECP3i1FZSL+Y4k/tGOON4lzvZ3eMQfMbjT6td0z2Py922rn/6NEL2vO3kaHDGsOPFer/OzQyBPyycOnTaBzLcE7HRdl3tSb9+WlE7T82aH6uYvM0Kj8mNIY+lUZ59+fn4GMybifxE5zi5aVPJTU7++G6D/vUFtVxWkGrnlWZ1Rei+HvfY9kbYMKwN7ALdP+C0B2jDl6Qbgwo7HHJC2FiNCoVwksgRjrb2E/OxGS7FCNeYqZEznnglnKBmGB6AZnoQnM5mRW5IUtRL8wcD1n6vZCA5lc/E8mFxU/lp7Yj+jdzScLnb07VFoYrUdLkT/h9TfWJwnAFfQFeDPibI05vibeuItAYcXmD3vowwSQyT+YIT8qpRmrswlwJRnGfw0IwHJFYvoTRa82IXp4grriVlDBKYRjwNG1C5sVsuLDklwDEEnl5NX/6qXrwkcHu5nk5Q83jDDV6ttrHux0Gg8PNC3B+AV6c4D34PfhvbAaDzc37YovOqAW+qEpzfEl8mrYEozMR2fnVRGcKc/4tSbQlLGtLmKRZZ7yytuAvcKjGTb2ASYXBc9gk1URAW7z2z6Et50PUn8atLxVGmv3+lkhhYaTFD8pQmGivibe3x2vaL8ClB/2NYacz3OgPNIQdjnBDAL8bfggGP/s7ilL+hvTetFNfodL63P7AxU2LREtshjPpkbwAx6lwl4oZVq2fb2TkiOKSRRyLnbj24zOkIsQSETURHFooCk6JGl7Sw4uCn2YVGnN4Wo1/w81pgwV/+YgZ/2ZeUrBqjd5gtpz79R9+vAxnzv0AC5VwAfioMjPFzHuzb/bSR+a+MkA/Oqepn3s4Y3CjFrpySm3RzXdHQm9lx100x/QVRO2kd1H2btL3apC6lEr34dFG4ue0LwKJz7TLQWg7aUDc3oSjtaHFjYzwTqiYkXT7lLqceDuShXVHosn63j6iBe1J0IL6lNgniLHUf6t31sImpGBoSXQaoT9/U60dV9y9xp6PWAvOjWVLbs88te6zu21F+5NuNJCPbs2Lg95L1AfeQmoq34dL0QD+TkdZP7vzle2zOl/ZP9H5asFDL+qBNVe+yCHnBK6y5Hzw/wOa5j3yYpp+s9gD54hShnNOd4FX4Hd1VOFn01X0WXS5z0PXEi+8mLy6TzrdeSKX+FmZzjmg00NVUzs+nVLcNaoyLgngVvzgVmIXJJuYA5zCAZdj4/EWJKnUSha+458cyad7lcXjin62E8mP8/hn+g2awl/s8DjojgY8RxGV1uJqBB3p9sSRHLPBnMn3C5jXTLxUr5rXyMSunCqe+jZpwUVTb8EHr/t8nzmvWfgz31rQKP2uvCqdejfX2IsG7aboEdAnnmRSyB6XtIl8rhWnziRLrn2DRcBfg4F0ci7FvFRLcFrTulQ7Htx1rlrMPxb0Q4/HA/qB9+yV4V5WZNce+dIjYxRXP+E174JYLrGzeKkb99qx86RDeTHAjfB5M4iYHvO5AtcvFfKHu4bOlfInhHtqByZYefw8Mo4BNvhxrrfKjtyeJgG0myHJMtBuRBkZuegIAXh0w0h8UdFI9vsKZrzfLC0YyWaFYk04bRTwoRGvcAg82SGpsWRwz7tcMyyNXa44OqfZoFcwL7QbxEof+zktPDD30uTkS9n7536/Gz197D3cdPC9Y9lx9HB2C/1GO/3sQu9B+o25e/PtB+eea8/1Q6wFbGyiItQVn+jYhbEf+PAiGE04KjlYuS17dHHcaAaAE5HhToTMzhzcwfAw3+ELrx8WY4TjCKZSi3p9SeEivABRdoGuX+YLAOQl3cBOfQom/kSfMGXifICYkXuHwVzD62/V2Mqep3tY7Hzdw+K5NbhpI1taSbz5F2wgtuCpPruVGCqcNxefq6sY87Ts3P6/jm/eNn2O8Z1cMF2fa4D0m/OOMjdGsGt4jHUXGGPqfGOsXzTG8H9vjEts4+cYavlS0/k5B3yO01007l+QcXdQx84zblz8WBqXYiyp0qrE7Y5hHncu5kUpzNwOeeZ28FItnCXks8QCnzCOre2ACMbo9FeyDedySmqFSFiqav7cPLvA7P4crOu54Iz/fDz89vlsgCLHxznCxwZqgNp9Pk5CgNcTlyrBU7UAC1csYaEUs5JsJq627YTDzgXm4a9za4xhJXP62f+Wkn06uPkcfPN+Fub5fEal8TPxEKIeok4rGMUGwIKUWYOSGmTXIJUGPYSuyt6UQEfRpYnszejKmux12WtRFF2NjiazN6Ijyewt2WO16MrstbJe383+mn0fvG0llaI2UGkblkZ1XhpleD7Xy60+QQA+npQxCcDqBnj14UVZd0pMCC+pWZuT8wQjuPBEwFu3KamsWjC9RHGC06MuSeXDrFyVKymAtuUFEQypyN6hII647Uje0Wqe36orG+0r3h09pDdZ647vOIS5f8l3R240+ITKN/Yf3bN5DT3b89JezP//2f3N7VgeY0M5Pne23ccbf7Ml++sZwuzm+hmBp85uQSWvPXFmlYKtbwZuz/XUJDDzH/xoFcYgpM8c2HEn5cddWT/ZaS5wvk5zJblOc2mry5NDc+ftNreATc/Td+7jBd9zoQ507FbZ3/zfpnPBp5yHTiQtciIXolRxWd5x5GgFv+Gkys9Pa/h8tFYs0Fr06bQu8Q3nI1n5CWdwYcKXOAAmR/8c0F9JtVDrPjkCsSwqNsQlDxit6hgpD1kYDl7LDVjnC8MTcJhYGGRbrkZcsqo/TW0+3TKdZ8Bzn2mJLjj+P3+G9aHl/nSgexbK/ckOdZ75DnXFn79D3UIu/fy96poXx/Dna1vHvDuPUxb6vHIgsb5FfV5nDEYSHRs0mRnGKbcz1sx3JOeAZNoYi4kcj0soSCdouS25cb4t+QVavu5E3Pl7vmZ/Lnd9zf4zOkq6vk5j2/29sx8o2tjXqF7q8hx1xZTcuQkgg6TEBbx9hKReQ0bslb+Zlnyjs1xVWiBkpnUF1eqw1AIhQkuUhAD4K2rr8HeVlvlT+Ks0JWUnvLYAlLAVV9Q2En/YWYG/eajAH5K/oWzRt5coFm04X1LwrVj8rRNW4XsdR57esubmddGqnlU9Vb667r5lKV/NumsHd3y1ycZyOkOweW1r48Y2b+PEronG6r7VfdVFrbv6eq7enFSgHU8eaqwZ2R5v2diTqmsMlsRK3L7y5tHGZRevinTW5fast6yq6hquDcX722K9LY1do/XFvW3hiok7Ns0imIukxxz57qAk1UbdfZ4uc3X462E/q9Vc+2e2mus4p9XcDGfx1zVhB3ehZnNSHQBcsekLN51bcAlfuP3cjvkmfF+sEZ3i5lzLvs/Fz8b/T/xsxPys++L8nK9J+8L8/PV8EdsX4ydzcb7kLc/P44Sfy6kHzsPP1OfhZ89n8rP3HH6+gPlZ3zbPUNEliA3nZWvqv8tW7GWj+Ct0EfGyX5i7Vf+y5hftvP5RJUsr6cdYTvMFmXzF7Kz+aYVaoaSfZlWLdPdWwusR6t0v3HESW9m6uNQOdncoKjXBhS7w3qsWsx5M78yIHKeNLBbE9DJXTB2e6ZJvdUVnlslHC/IZXSSfOkHkUlLXCER2Fn9lkwavSkhFMeFCqj/UDldaV6S+uJQuEPN9YWElLKE6n78pUVNQUYkazcGk39dYV1MQrqS/oNSeLWmLunwhX11VSWu0wFfqa4iQdUBZdkeI7Hqp9dTbX1x63VFxIi41AegaArFtWCw2vPWuHZBW+zkyG8Uyk/rhej/Ix7p4Nm1cJK0UlpbYbpIqsSvtFySLBu/MMElDE3KZzP+RZqOftafoC4ss+VmbkL6g5H716VuW5mX4cyLDPmrNeWfgKMZdTfL63afLc2awm2syhGcGcyu9Y0vnYb88xfp5aRjO2uWz9guYx/Gl00/sN4n+lDgszFgqm7o1nzEDRwfhSnvdf38Gnm8Z+QuL9NbCqtZAoLWqqEh+LWzIry1/QYevKGmucDormktKGiudzsrGknhbW37NmdhRpVGhp9qpYZiJIpVuxlJMxKXlMMvKYqTdn1gQJ4vy47G0xjovvZFAs9UQFlfEpREF7gaVn4YdIIsOXhqQJRMAmDoSwxEQ/tL3Yj5DplsHRb4yRBwQ0py1GReYBUySA7+uEtIFZaSMvtgkRapxSjuwHNdCwTHZ0iiIxbhUSjLN73JfEFCu7s9mn68783uXdCzFXwO/WG5NcBXle5guFpLOyAqDz+299m571Ss3DtywpU7Lza2rnrh6Rc/2ZSEtp3Y6+tbtrL3x7SrLmv3/q7dzD46quuP4fe4z+7jZZ7J5bTbJ5r3Ze5MseUMChIQkBBLAPARDERGCgBgEX4hCK0lFKyhi29FSFehUu3fJjNba6YBV207/cqa0U1un49ROM+NMy1inLUjo+Z1z95l9JNX2D2DvJsy9v98595zfOef3+3wfWoaaxLeluG1YXHn/iATNx5xgtlf07GzvPTgs0prOAyMBrvvJFyrESr0GNdmxe+99vO3g6/c6zAdem2pxlxfrCgF++uQ3102uzC9cuWtd03opp2bzkfXH+YquMdqweXqr1HjHCWDwzp/GDN5u6igV6oK2KpNklyophjfo8802k9evGRedNjfA8fmaMJsXjvxwIpppDidjttnh+FzgXWVen9jZhdcNzT5SatolQLn20ji+dLqTczYj4Lf2h5M5Y3fkiasrKgdzdSodn51XkV/f4vJ3lpeOnNrVlIb72zLIrU96TH5Y1X/8J9DvMUcXxb7A0cX17hGSrp8JE9wScbotKXC6rQpOd5a3uv2g1pAGqCv7YZRpXAJYN7pIWBJidyayQFgUbJflo+uC1L5p+N/6pgF841+Cb+hIwL8k39DqSLS/KOfQ12LqWsL+uYj9syLOP2JK/3Sm8E9XrH/qM/hHXKp/FkTuS3LTcGLUvjhn/Ts+WOcUfx3C/uqiNlHT6bnVsIc2JMmNKLjrQbPK5gTPAby6xYZxyXBmMoA+DkT9eRukAbWgUcrqroaTAFnnhfraL0u3zhSxLcmvY5mitUX5mdmSPkhjKBSI0VtwPZeBqlRyHGCvDkMqI4kOBpLoIFN6BU8an0ThiYwj7RMK7/9GL4bzKnXBFP2HhHtwKe/B6SNlPuEXF+7xYuR1tE9EashujJG7MLc+hRvh3AAr1ajkVMCeXiibjkmsMMQlVmix3iedrdyPTXwR8GZrYv8+NcG9Ftt5bwwphrK3PkN2XsccATvJr8A7n1aa5FeUkfyKPJJfEUUJgHiUMtFCfoU7kl/BJPQfeJzEPmZI6CbvTNRkQAvc0MPzJn6L22ns1j/Yv/MvIv/1ArtHhPevVY21sjFrjWw6BtCzBsywMw0KwzXK3uKKAFq86vnc0nIRxwSgjB2ianRx2s6OWtqLtYU7YDMek0s6YKs34MBl3gtlsQME7jLWuv/VXY17dtzmNj29/4KgzjradmKtTkBNMj47+B0Lb7xvxe51VS33yVO3f/+B1RNNE492j57YIrGm1tHDA6NPjNfSH2x7/bG1ec2jbT/+V9/pfI1Ol7W3uM7MmIysnbMa28SZAo1Gb9hR9/C59w89+ZdXRjofkvdufW5H4+pjP7u/fucGqW3PM6QvEwb3NOWgJOpkCuIvnFc4JblYNRes8+HkDeDf1CdQgFFjz0pkkSKZ4eQlRt42TAhuiBKC5VIJ4qp8CzkgV0DBch2gAYpqm1Ijg1Ot+ReihL0pF/XJIMPch0mX7mjuw+xhRQfOTw3H0IfLI3MfRhCLyRDEaRIe5HKY3GoWUV8dHZ8yc4m/HRm9MhKK2U0kAkpnY/WXtLEabCxfhI3RwGYR7GVHZPjMaCTTGYlkwnZeVHI6Yu2siLezKZmdaRI75IrF2rkgQMls7vbEUTuz0b0J24cR26cT8zpiKNrhvA5VsrwOw+LyOgxLyuvI4KoU73pmj+1K+e5ndt2hFHt4xH+HsP+aY/M5Yj0Y8AV7ST7H8mg+B3FdRXw+xyr0cVXUaRnyOdI7KlOsltlhuzMFaJn99qMMO2jQB/dRH3N+DjTuLShWq6VAz0CdNRcGPbh9siNrDp/mc1eDVlHOskGIAdOJwrigY8+Cy4S4q33s5ZuXY/l5sZ+ZE2vXzr9ZvsycU2KxenJMAZaOuSDvxyXOwHXgeqlGaqOSH+ILbzSUw0FlANcI54uy24ArVqBkR0CtB2eW9W5AnfF2p7GglIyC5T6SFuIs0JQ0xu0fBBQsnqL0oSYoPDo2J8ROGpiM+KOnlo3orRbp6bbl0ISv3DNk8Aje6dXdW+tEhqs93D82vcX31Mj02PTtvg2kqcTa+03Gy6uuHIb2Wr9PML+16leP7brQwrxRVbvi4Pl5d/fyqVd3/HwKxwGYF43GfwflhhP/eGK0k1H46BgbXZwCG+1RsNEhixMSGBLQ0VBOmZ8aIB2d4JKgpN+NzmjJoNLcufA6PoMdeV+FHXkC4XcntyM6iSVDYq+IzlrJDGFPxqy5w7aAhmj5Qlty4mypSGFLZdQWVxJbctLasmCiSmLSyQUzU1LDnoufjVjFtkPItkqqDXh7SRnlQa8v2CzJ+WiAqBOxpGjUSqCUF9twnhakzjTYMEEoxnbQGsWkKYsKzTogirIolHmmoTSJE57NOHYmdcqNjOMlQxjVqD9DFSdaa7qYKC0do6rD1ZsKqjroEoKO1MBqNtI7U6OrhUgfTQ6x5o5EO6mib8F/gFnuir4biNoSonUBlrbAKivkZcsGfTeLKEJqh0vRd4PXzZUd0XcrsMfou1kS9d0SRS0mVob2pRC0UDffPDh6d1jbbbB/XhOvZ8Eqvj2EV7et1EAsAxwS1ZtIkaKPFCk644oU65UiRbeiQlwlyBo7PH4mZDiToXelbpefZupkKZrr0wy9DHSuP9PcjfpYEVVPPaEojtkkuYydC1pEgnU0hivU6ti5WVN2HmxbmaA8iDDg3FbsGUDA2KtEEdZ6wMA0YrivERiYWSL6IGircE6lDmpZebw/lQ2YCAfoxYQodxUMUcZsZZeKZLAyjph6HLeA96iSyDmPvfznma3nZ/aUsSPhkpwvzpftmTm/dfqTl8d2989cmTp4ebqvb/rywakrM/1KwqR//NgwvTFcqrdp+NhY3c4rtPnC2WvnR0bOXzv7/LWLo6MXr5HYWfUIp6dEajXq56epUC14CcXKy9RQY0KwugZJ7kSX/eJst70WXNQN26AbsIsk5BKJnD3A7ki3CBskayDTyTyH4ZdtaD0s1wIZyo46E3JFcE12yOAqbyL5TUWg5yTbl6GomiryVEk4maQbJIOCnUqPU0ILRSko+UEQnSx65MNbfiMt+87deer9KuuaOx7o7f/615bpTTdv948dGVh15+pKfZbG5ewbv6tx+r3aql88v/2lfS3bKzce2Tj8yHBlJfoLfaxkVcydFWt3tvdODYskCvnuzMrJgcqYg5/wtt7zz518KUkUaQmf+7Ak7051k7Ki+a+ZGorPvIMQsVGSc9EbWk1ovLarcqENk6ItOBMPJ5BBzO23kT35xSbnpc8+TJ6xt4ga4mR5fNzQInKf3dxrTAPeC6yJaqoKCodEwEQkBQWXHVFX1TaFK6xi5m934mQdv/UH9/Jyv2MCaI3oovqooMUHtbg6FJc7fTgFwSCCTgPc0EUWfS6c2hlm9oFkp8EF77YFOqsTk7nt8WTu+IVc6i2apNsxNLWDaWS6GOgdFKwGdtB/ZBqHhoif/tufnWGq2beZKaIhSxYi8CdGQxb+yxm2lKnu6SG/z7+f+ff5OuX3j3PNdAP/OerHzVQw2zfLZlE6jmziooFBb5oL6XGBoh64MZR51mSlJORN2NnVk0NjigBsYVtRDaKAZH+xlj4+0J6nUXmlEt603G7lfjN4qs2i0qhV9XcFWjs0WqPK5e0nNu7namk3/1f0DG34GbKiz8BflU2muaDJPKvFNw5qfSEtrivTAr4OHsMEextZ5DECQDwhm56E3uwt208eocNhHejIU3PrNCppZ6ClQ6MxqnO9fd7B060WFTzD/HXaTc1+6WdwZH6GTxY+QrYK5jrUFkwPbosKtBZFTxH0SkqDBJ2RUsFUbRLUk1zZIvTzIpwWUORCP7eZZ0usVL2CjFLaTLaZUPdnIZemSAh6U7ZhaeaGpa39HXBZDwamamdvisZnoO2Zetz2FdTusM3E+UE3sTm9/+EICud1I7NzS+DbXBuwzXMLtMRkpW0gC88LeQ0gYJOir5SGv/SmbDzagi49PG1uR9ft+Sk6lCZpL8P2zl9n6nE/+//a6/iK7E3aebXJezToeZTSy9hH2G/hmsugETPz1ISZp4bXy4IHbK0Nf0n+wSJLdX6oAIqZ2ehS34bJh/Zu8Pk27G1v27PBx2xr3wvMzns62ibh20myhzN56xpvp16nBMpDNQAvEO+CuSUJnwjJjgpRJF/xsJXTGFt8iyYoOQ+2dAgdqxbNzAHC4ozn+ZSmvZw05hTbojs79OemnGKrpSTHbM7xWNH1PzHnJ3K9Lo7hU57mioyVL1In6Hcx99dNhd1nslFGDmf3QP0w6L+hKDU58DeR7psC50vuNYvu9SFm0MG9bGECnYBvh8c9gSj/paLPLQDNXUoDj6OpolvXuGn+DbTaOUaFeqCRmrVzIROE9oUotKfoHpOhKuiTZIqbC9aLs1oN/qJCAiI05tesw2+PbgCF+dWWObmkAbV2Nc6/qfbDS1JdBmDWagxmhXdJI8qDeIXajIbDFSvRUrwQ9EmtTqUcGY7NAp4GiYStSmINplKoieqBymbFwrjoIwZvcdGzam/R92iGO3fBPH7yrf2de7cOlRVxOq3G7hFXjbWMv3Bfn4nZaRJuhliaZgSzad5i6D1wdrxjW29Daa5Wpy0r3bTzwTX3vT29ych0t1rL7aK/9Ru/fXbQUdNVXcKrbYVlhbblD795uFCfXSfZvbbCLOHI5aMrnGXVZTk6j68/kD949qOn8JjTy47zpShGU6N34gCJ0mStTSJ+ZMUwixnAihqHiBZDVAHkJaEgVnVV5o1odYXRjDyLnKfC3lSB83hS9OwxYgVROGJzkFALKpucHkAl5pNCmgYC28SEY4fF0aioy3mEAOqanmIv6xB66Y9/vYY+3azTqT/S89rf81pdy3L+TxohS9B8ouL3tLbe/BsjoD/9nGZ+psBspKc03M1L9Hs18w+aaYF+vGq+GfoQDAI32BtoJPDGaCcqMkIQisJAQ/5R4iG/4Bbgv8DBMta3Zh/lf4n+3aqsNh2SInFti0pcqxLlra0ihJtwpuwwzIUVFSiidC07UdgZ0giYLSBrQGRP35Sgfu0B9WtVPu1WmKQgfx3YdWaiuMfJ0QZ9dfG5ILNx27yJqF9v3nLm7qYsnV+nfvUHw1+Uss+E1a/J81/i36GKQY28kMLLkZABWlxAMbJghmefzc0v1JDa/VxsExYNLMTGgPhtjhgqKMRigXmgCWGWzTCsGObwsGguQMboNValDCxsBEhIoecm28OxIt4NO85u86ztbrP1TgQe8PcfHqqmvfMfEju6Rl/Yv5xXcdf7+H2Mpm7s6GBXRMj7P61y/VcAAHjaY2BkYGBgZOo//7DZK57f5iuDPAcDCFz2z/KA0f/P/mvhyGTXAHI5GJhAogBrnAx3AAB42mNgZGBg1/gXzcDA8eL/2f/PODIZgCIo4CUAogoHhnjabZNfSJNRGMaf7/z5VjD6A6bQjctWClFgEV1LiVR2FTHnMCjXruY/hCCCRdCwUApyYEWyZDUsKKUspJuI6MYKuggGIl5Eky4WXgQjarGe92uLJX7w4znnPd855z3vc44q4AhqPmcUUCkU1CrmTQZd5K7bhLC9ij7nLeZVDE9IVB9AgmODTgpDahoxalwtln8xdpyUyJUKbeQWGSVJcpHMOitICWzfJ49MxnFUEU3uTQzYZmy2AeTsPVxy65AzL8k4+yX2/cipKH7rKURsB4qmATlfO3ISd88wp1coilo/x/YhbB4jaJexIGv68thq3nlst1twnud4ppbKP6j9zOGj3s2zh9Clv7B/GrM6g25q2NSjW42j0WzECXMSWeZ9x/lc/qBXvXO8cXuQlTgJmw4q5+i9yOpBRNQiDjI+pvPcM48GPYOgFp1EJ/dtUzHHT41z/xtSf6k92xnSXtGQ/GMUrjO3FneY/Rn06QTSHJuWOV4shDodRI94oh6gl0QZ+yR72004pAJ4yP4I47dVifklMGef4prHC5xi7fd4dV8HX2/5m3jh+VADffCR12Qb8bud2F/1YS3Ma9LzRbyoQbwQz8wU3kvd18MdoIoX9f/D2u8kaWelXCDfzVFE/vmwFtal0h6rRbwQz0Q3fGWuy/yHObFWO0izTgG+FqCq6izfyAJp/Qvy1H7qOY7xHVTh2hO8FxN8F0l5I5V3kiSiQ7zvu+xlxGWuuoA0mZN1mWfAPscx/ZPtw7xzI2j8AyV25OAAAAB42mNgYNCBwxaGI4wnmBYxZ7AosXix1LEcYTVhLWPdw3qLjYdNi62L7RK7F/snDgeOT5wpnFO4EriucCtwt3Gv4D7F/YanhDeFdwWfHF8T3yl+Nn4b/kP8vwQkBBIEtgncETQSLBC8ICQl1Cf0RbhOeJ3wJxEVkVuiKqIpon2i+0RviXGJOYlFiTWIC4kXiV+QMJFYI/FPSkEqTWqNNJt0hHSJ9CsZM5lJMj9k42SXySXInZOXkQ9SkFBIUJilcETxjuIPZQnlIiA8ppKk8k41Q/WWGoPaGXU59ScaBRrHNN5pvNPcoHlOS0urQuuBdpJ2l/YzHS2dJJ0zuny6Cbp79CL0hfR/GNQYnDNUMKwxYjOaZKxkPMvEzWSCyR1TA9N1pjfMWMwczBaYc5n3mf+zKLB4YznByswqwuqRtZl1j/UbmxKbI7YitpvsouyZ7Hc4THOscIpxNnG+4ZLm8s21z83LrcZtndsH9wD3Rx4lHs88ozxveFV4S3lneD/z8fLZ4Cvnu8mPyS/B74l/WYBBwJaAV4FWOKBHYFhgSmBN4JTAa0ESQVFBV4J9go8E/wnJAcJFIbdCboW2hf4JkwmrCXsEAOI0m6EAAQAAAOkAZQAFAAAAAAACAAEAAgAWAAABAAGCAAAAAHja1VbNbuNkFL1OO5BJSwUIzYLFyKpYtFJJU9RBqKwQaMRI/GkG0SWT2E5iNYkzsd1MEQsegSUPwBKxYsWCNT9bNrwDj8CCc8+9jpOmw0yRWKAo9vX33d/znXttEbkV7MiGBJs3RYJtEZcDeQVPJjdkJwhd3pD7QdvlTXkt+MrlG/J+8K3Lz8H2T5efl4eNymdTOo2HLt+U242vXW7d+LHxvctb0mkOXd6WuPmNyy8EXzb/cnlHjluPXX5Rmq3vXH5JWq0fXP5ZbrV+cvkX6bR+d/lX2dnadPk32d562eQ/NuTVrdvyrmQylQuZSSoDGUohoexJJPu4vyEdOcI/lB40QuxdyCfQH0lXJhJj5QMp5QxPuXyBp/dwTSXBjt4jrMxxL+A1lPtYz/GfyTk1QrkLTxPG+wgexlgNZRceu1jLILXpX/0k0MvdqmRk9RPSs1o9kHvQDOVjVKK6y75XPRxg5TNa51jPqHuESEcezWKblaGheQ8QVWuePQWBy/WfPMHnyRK2V+2Hl6JelbFZv42nUyJbUEd3I/hQqy6kwpHS2otFrNeXYtXxU2iFeFJc1VpRHtPTGdYy6f8LBrSvbfG03fVsc3o2bqWLLJUJfWKgDOmTYSmyUB7HREwRmDirUiJX86mE9tixu9wFp8REo86BZI+5mpdVv7Nn6I+9FcaHjGnVaC8s57G7yNLQ1PqH6FLl7T1ypmD9CW0No4iZKg7KJKtd87WzMGRyaFrvTSEV7JQCfroLi4is6zNmxL0JKlT9GRk5Y49b5BNmWdDvEHsaN3b+KZtCeYS1lHG0QmOa1jv1XDX6LifH0Hu5XOBr9ffgN/Z5lMhjRutBq6BVHTMmRlNWe7FSaebTTv1pnRXjNa/8H2NbPw4WXZXiJLVuPYVPnT0RtXLuRu5fscqI8IxYZaz5gDtdX4sW/W64nzP/FLWN6HeVoyUsp8wjcgaqN63pnPuV3oidb3Ogz/hj1lh3RMqYoU+NMXO7YG9Zvyb0MVhwRmt9xxk3dA5V81vrGHsuFZo57RNOkfVeHSFexj2dNWfO34TVx86HOlLfp5qtdH3CVzNhTiSe3N9VJx94hGSBqLJmwPeUsTfGimUyYVeExG7EbOeOjfVGiUpmS3maHK8wIif3U0yLGSPZG6yaGAWZN2K0asqun12+crp1zV3mlvCUqs40L3M/T/V24KxOnUv1yRXMyezsqSTCJSupmFudRu5aXbDSuFOscKU62YydM6GFdceQlUwxIQ7xm/PX9kldvx3anDZjaFxX//LszbG2PH0/X5u+h//xt8/etWvY/199Ma1XmMNOsZyy89u0GOGecWYeItpdeN+/gg/PZllVWn+96LdPj71puduX0alX/qFP/lCO8e/geiJ35C1cj3GtzvhNoqOTRedvQXaX7IN8CZUH/uaybh/9DeeiFNJ42m3QV0xTcRTH8e+B0kLZe+Peq/eWMtwt5br3wK0o0FYRsFgVFxrBrdGY+KZxvahxz2jUBzXuFUfUB5/d8UF91cL9++Z5+eT3/+ecnBwiaK8/FZTzv/oEEiGRYiESC1FYsRFNDHZiiSOeBBJJIpkUUkkjnQwyySKbHHLJI58COtCRTnSmC13pRnd60JNe9KYPfelHfwbgQEPHSSEuiiimhFIGMojBDGEowxiOGw9leMM7GoxgJKMYzRjGMo7xTGAik5jMFKYyjelUMIOZzGI2c5jLPOazgEqJ4igttHKD/XxkM7vZwQGOc0ysbOc9m9gnNolml8Swldt8EDsHOcEvfvKbI5ziAfc4zUIWsYcqHlHNfR7yjMc84Wn4TjW85DkvOIOPH+zlDa94jZ8vfGMbiwmwhKXUUsch6llGA0EaCbGcFazkM6tYTRNrWMdarnKYZtazgY185TvXOMs5rvOWdxIrcRIvCZIoSZIsKZIqaZIuGZIpWZznApe5wh0ucom7bOGkZHOTW5IjueyUPMmXAquvtqnBr9lCdQGHw+E1o9OMbofSa+rRlerf41KWtqmH+5WaUlc6lYVKl7JIWawsUf6b5zbV1FxNs9cEfKFgdVVlo9980g1Tl2EpDwXr24PLKGvT8Jh7hNX/AtbOnHEAeNpFzqsOwkAQBdDdlr7pu6SKpOjVCIKlNTUETJuQ4JEILBgkWBzfMEsQhA/iN8qUbhc3507mZl60OQO9kBLMZcUpvda80Fk1gaAuIVnhcKrHoLNNRUDNclDZAqwsfxOV+kRhP5tZ/rC4gIEwdwI6wlgLaAh9LjBAaB8Buyv0+kIHl/ZNYIhw0g4UXPFDiKn7VBhXiwMyQIZbSR8ZTCW9tt+nMyKTqE3cY/NPYjyJ7pIJMt5LjpBJ2rOGhH0Bs3VX7QAAAAABVym5yAAA) format('woff');font-weight:400;font-style:normal}.joint-link.joint-theme-material .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-material .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-material .connection{stroke-linejoin:round}.joint-link.joint-theme-material .link-tools .tool-remove circle{fill:#C64242}.joint-link.joint-theme-material .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-material .marker-vertex{fill:#d0d8e8}.joint-link.joint-theme-material .marker-vertex:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-arrowhead{fill:#d0d8e8}.joint-link.joint-theme-material .marker-arrowhead:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-vertex-remove-area{fill:#5fa9ee}.joint-link.joint-theme-material .marker-vertex-remove{fill:#fff}.joint-link.joint-theme-modern .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-modern .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-modern .connection{stroke-linejoin:round}.joint-link.joint-theme-modern .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-modern .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-modern .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-modern .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-modern .marker-vertex-remove{fill:#fff} \ No newline at end of file +.joint-viewport{-webkit-user-select:none;-moz-user-select:none;user-select:none}.joint-paper-background,.joint-paper-grid,.joint-paper>svg{position:absolute;top:0;left:0;right:0;bottom:0}[magnet=true]:not(.joint-element){cursor:crosshair}.marker-arrowheads,.marker-vertices{cursor:move;opacity:0}[magnet=true]:not(.joint-element):hover{opacity:.7}.joint-element{cursor:move}.joint-element *{user-drag:none}.joint-element .scalable *,.marker-source,.marker-target{vector-effect:non-scaling-stroke}.joint-paper{position:relative}.joint-highlight-opacity{opacity:.3}.joint-link .connection,.joint-link .connection-wrap{fill:none}.marker-arrowheads{cursor:-webkit-grab;cursor:-moz-grab}.link-tools{opacity:0;cursor:pointer}.link-tools .tool-options{display:none}.joint-link:hover .link-tools,.joint-link:hover .marker-arrowheads,.joint-link:hover .marker-vertices{opacity:1}.marker-vertex-remove{cursor:pointer;opacity:.1}.marker-vertex-group:hover .marker-vertex-remove{opacity:1}.marker-vertex-remove-area{opacity:.1;cursor:pointer}.marker-vertex-group:hover .marker-vertex-remove-area{opacity:1}.joint-element .fobj{overflow:hidden}.joint-element .fobj body{background-color:transparent;margin:0;position:static}.joint-element .fobj div{text-align:center;vertical-align:middle;display:table-cell;padding:0 5px}.joint-paper.joint-theme-dark{background-color:#18191b}.joint-link.joint-theme-dark .connection-wrap{stroke:#8F8FF3;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-dark .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-dark .connection{stroke-linejoin:round}.joint-link.joint-theme-dark .link-tools .tool-remove circle{fill:#F33636}.joint-link.joint-theme-dark .link-tools .tool-remove path{fill:#fff}.joint-link.joint-theme-dark .link-tools [event="link:options"] circle{fill:green}.joint-link.joint-theme-dark .marker-vertex{fill:#5652DB}.joint-link.joint-theme-dark .marker-vertex:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-arrowhead{fill:#5652DB}.joint-link.joint-theme-dark .marker-arrowhead:hover{fill:#8E8CE1;stroke:none}.joint-link.joint-theme-dark .marker-vertex-remove-area{fill:green;stroke:#006400}.joint-link.joint-theme-dark .marker-vertex-remove{fill:#fff;stroke:#fff}.joint-paper.joint-theme-default{background-color:#FFF}.joint-link.joint-theme-default .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-default .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-default .connection{stroke-linejoin:round}.joint-link.joint-theme-default .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-default .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-default .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-default .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-default .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-default .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-default .marker-vertex-remove{fill:#FFF}@font-face{font-family:lato-light;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAHhgABMAAAAA3HwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcaLe9KEdERUYAAAHEAAAAHgAAACABFgAER1BPUwAAAeQAAAo1AAARwtKX0BJHU1VCAAAMHAAAACwAAAAwuP+4/k9TLzIAAAxIAAAAWQAAAGDX0nerY21hcAAADKQAAAGJAAAB4hcJdWJjdnQgAAAOMAAAADoAAAA6DvoItmZwZ20AAA5sAAABsQAAAmVTtC+nZ2FzcAAAECAAAAAIAAAACAAAABBnbHlmAAAQKAAAXMoAAK3EsE/AsWhlYWQAAGz0AAAAMgAAADYOCCHIaGhlYQAAbSgAAAAgAAAAJA9hCBNobXR4AABtSAAAAkEAAAOkn9Zh6WxvY2EAAG+MAAAByAAAAdTkvg14bWF4cAAAcVQAAAAgAAAAIAIGAetuYW1lAABxdAAABDAAAAxGYqFiYXBvc3QAAHWkAAAB7wAAAtpTFoINcHJlcAAAd5QAAADBAAABOUVnCXh3ZWJmAAB4WAAAAAYAAAAGuclXKQAAAAEAAAAAzD2izwAAAADJKrAQAAAAANNPakh42mNgZGBg4ANiCQYQYGJgBMIXQMwC5jEAAA5CARsAAHjafddrjFTlHcfxP+KCAl1XbKLhRWnqUmpp1Yba4GXV1ktXK21dby0erZumiWmFZLuNMaQQElgWJ00mtNxRQMXLcntz3GUIjsYcNiEmE5PNhoFl2GQgzKvJvOnLJk4/M4DiGzL57v/szJzn/P6/53ee80zMiIg5cXc8GNc9+vhTz0bna/3/WBUL4nrvR7MZrc+vPp7xt7/8fVXc0Dpqc31c1643xIyu/e1vvhpTMTWjHlPX/XXmbXi3o7tjbNY/O7pnvTv7ldm7bvh9R/eNKzq658Sc385+Zea7c9+avWvens7bZtQ7xjq/uOl6r+fVLZ1fXP5vuqur6983benqao0587aO7tbf9tHYN6/W+N+8XKf9mreno7s1zpVXe7z26+rjS695e2be1hq3pfvS39b/7XcejTnNvuhqdsTNzZ6Yr97i/+7ml7FIXawuwVLcg/tiWdyPHi4+rD7W/Dx+3RyJXjyBZ/AcVhlrNdZivXE2YAgbMYxNeBM5Y27FNmzHDuzEbuxzjfeMvx/v4wN8iI8wggOucxCHcBhHkGIUYziKAo7hODJjnlDHjXuKrjKm9HsO046rOI+Fui/rvKzzss7LOi/rsqbLmi5ruqzpskZ9mfoy9WXqy9SXqS9TX6auRl2Nuhp1Nepq1NWoq1FXo65GXY26GnU16srU1WJJzKJnLjrbczJIzTg149SMUzNOzXgsa/bGfbi/mY+e5uvxsOMVzXXxYrMUL6krnbvKuYPqanWNulbNOXcrtmE7dmAndmOfcTJ1XD3lu2Wcdt4ZnEWl7dMgnwb5NBgX/f8DanskqEJxD8U9kjQoRYNSVJGgymWlWyitxQPNk9Qm8WBzkuItVPZQ2ENdKyUVKalISUVKKlJSkZKKlFQoS6hKqOmhpjVrgxT1UNRj9lpKeuKVmCWPc5p7Y67aia7mI/zbQs0j1OyN7zVHYyFul97u5gR1e/k6wdeJuLP5Gm8neDsh05vN9mazvdlsb44nm9X4TfONeNq5fXjGe8+qz6nPqy80t8cfqPyj4xXN6Ugcv6S+3CzESjpW0TCovuHz1Y7XOF6rrnf9DRjCRgxjE95Ejo6t2Ibt2IGd2I33XHc/3scH+BAfYQQHcBCHcBhHkOJj1x5Vx3AUBRzDcXzisyI+xWfIXOOE90/RWMZpes9gio9nVXPK9UdkYYssbJGFLXHRe92y8KUZqMrCl/Edee5UuyRqPm7x/iIsaw7Jw4QsVGXhiCyksjARv/T9fqx0ziDWYL3vbMAQNmIYm/Am9jl3HKd97wymXOOsWsE5xxfVn1HUR00fJX2yUInbvdvt7MVYgju9lqr3tJXl4l5n3sf/+5sZdQOU7TWnBfNpLo2xyhiD6mp1jbpWzTl3K7ZhO3ZgJ3bjLeO9jT3Y277HBvhbpXyAvxX+VnTQp4M+6vuo7+Nrha8VvlZ00Rc3Ut7vyv2u2u+K/c7sd2a/b/b7Zr9v9sddnM9xu5fbvdzOyXsm75m8L+R8TsbvkOtUrlO5TuU5k+dMnlN5zuQ5ledMjjNZzbif436O+znu57if436O+zk5S+UslbNUzlI5S+UslbNMzlI5S+UslbNUzlI5S+Usk7NMzjI5y2QsNWu9ZqvX/TqHO11Wr/m4xfEirMcGDGEjhrEJb2LK987hp9w5+a05vTKfv25e0OsFvV5wD0/o84IeL7hXC+Z03Fo5bl7HOXuSsyc5e/Kac3nAuQdxCIdxBClGMYajKOAYjqM1zyfUU8YtYxpVnMevYtZXEzEXneiKe3SxMOart+upW64XYwmW4h4sa74gmX2S+bpkLpPMPh1O63Bah9O6m9bdtM7e0dkRnb0TK429yriD6mp1jbpWzfl8K7ZhO3ZgJ3Zjn7EPGOcgDuEwjiDFKMZwFAUcw3Fkzjuhjjv3lPHLOO1aZzClp7NqBeccT/usivO46L07zPywmb/VzN9q5ofN/LCs9lmHSzqs6rCqw6oOqzqsSsWwVAxLxbBUDEvFsFQMS8WwtbFkbSxZG0vWxpK1sWRtLFkbS7qq6qqqq6quqrqq6qqqq6quqrqq6qqqq6quWnNXlbJbpYwuczJpTibNyaQ5mTQnk+ZkwopR5eckPyf5OcnPSX5O8nOSn5NWgKoVoGoFqFoBqryajGe+vldv/tb9mrhfE1caat+vi9UluLO51BWHXHEoHvvqfzzp5kk3T7o9l+51Hyfu44Q/3e7jhEfd7uPEc+kh93IiEb0SMeC59Gep6PVcGpKKXvd4IhW9EtF7zXs95/tbsQ3bsQM7sRvv0bMf7+MDfIiPMIIDdBzEIRzGEaT42HVH1TEcRQHHcByf+KyIT/EZMtc44f1TNJZxZb2YRhXn8fDlJ3/xqid/nrM1zuY5W7QC/pCjRU7ul6pRDtY5WOdgnYO7OVfnWp1jZy4/sWvtJ/Zq9dLTusahIoeKHCpyqMihIoeKHCpK3ajUjUrdqNSNSt2o1I1K3SgX6lyoc6HOhToX6lyoc6DOgToH6hyoc6DOgbpu67qt6bZ21ZM3f9WTN6/7mu5ruq+1n7wvc2ABBwY4sIADCzjwOgcSDrzOgQHZystWvu1Ea3VZ5L0rK8ylfF1aZS7tfRLuJNxJuPOCfOXlK8+lRL7ynErkK8+tf8lXXr52ydeIfK2Tr10cXMDBhIMLZCzPxYSLC7iYcHGAiwNcHODiABcHuDjAxYFrrkrX3vMkHE44nHA44XDC4UTO8lxOuJxwOeFywuWEy4mc5eUsL2d5OctfXsESziect9Ok9wym+HdWreCc42mfVXEeF733Ey6nl10tcLTA0QI3C9wscLLEyRInS9wrca7EtTLHJjjVWptT7qScSXVf0H1B9wXdF3Rf0H1B9wUdlnRY0mFJhyUdlnRY0l1JdyXdlXRX0l1JdyXdFHRT0k2qm5TqlOqU6lQ6ZrXuFHRihQS92PwvNTX7m6K9TdG+pmhPUrQnKdqTFO1JivYhxfiuM0ecOWJvV3P2iOfRZs+jumfRZvu3mtEaUpAZrWEv1xpxxIgjRhwx4ogRR4w4YsQRI47ETXK7XGaXU7W8ndlWXlc6HsQanMYZXJqH5eZheXseLqrz+ZvxN+NvaxfT2sFkvMp4lfEq41XGq4xXrV1JxquMVxmvMl5lvGrtQrKY59rrXHtd+5lzrWfIlO+cw/fdbYWvz7rF8aL2fDfoadDToKdBT0PiCxJfkPiCxBckviDxBYlvzWuD1gatDVobtDZobdDaoLVBa4PWBq0NWhu0Nr5WcP3Xu6UrO6EZ8So/5+qm047iZv54asWiWBw/ih/b594Vd8fS+Lln8C+sGff6LX9/POC30IPxkDX0sXg8nogn46n4XTwdfZ5Rz8bzsSJejCReij+ZlVUxYF5Wm5e1sT42xFBsDE/eyMV/Ymtsi+2xI3bGW/F27Im9fr2/E+/F/ng/PogP46PwWz0OxeE4Eh/HaIzF0SjEsTgen8cJv8hPRdlcn7FbOGuOz8V0VON8XPw/fppwigAAAHjaY2BkYGDgYtBh0GNgcnHzCWHgy0ksyWOQYGABijP8/w8kECwgAACeygdreNpjYGYRZtRhYGVgYZ3FaszAwCgPoZkvMrgxMXAwM/EzMzExsTAzMTcwMKx3YEjwYoCCksoAHyDF+5uJrfBfIQMDuwbjUgWgASA55t+sK4GUAgMTABvCDMIAAAB42mNgYGBmgGAZBkYGELgD5DGC+SwMB4C0DoMCkMUDZPEy1DH8ZwxmrGA6xnRHgUtBREFKQU5BSUFNQV/BSiFeYY2ikuqf30z//4PN4QXqW8AYBFXNoCCgIKEgA1VtCVfNCFTN/P/r/yf/D/8v/O/7j+Hv6wcnHhx+cODB/gd7Hux8sPHBigctDyzuH771ivUZ1IVEA0Y2iNfAbCYgwYSugIGBhZWNnYOTi5uHl49fQFBIWERUTFxCUkpaRlZOXkFRSVlFVU1dQ1NLW0dXT9/A0MjYxNTM3MLSytrG1s7ewdHJ2cXVzd3D08vbx9fPPyAwKDgkNCw8IjIqOiY2Lj4hMYmhvaOrZ8rM+UsWL12+bMWqNavXrtuwfuOmLdu2bt+5Y++effsZilPTsu5VLirMeVqezdA5m6GEgSGjAuy63FqGlbubUvJB7Ly6+8nNbTMOH7l2/fadGzd3MRw6yvDk4aPnLxiqbt1laO1t6eueMHFS/7TpDFPnzpvDcOx4EVBTNRADAEXYio8AAAAAAAP7BakAVwA+AEMASQBNAFEAUwBbAF8AtABhAEgATQBVAFsAYQBoAGwAtQBPAEAAZQBZADsAYwURAAB42l1Ru05bQRDdDQ8DgcTYIDnaFLOZkMZ7oQUJxNWNYmQ7heUIaTdykYtxAR9AgUQN2q8ZoKGkSJsGIRdIfEI+IRIza4iiNDs7s3POmTNLypGqd+lrz1PnJJDC3QbNNv1OSLWzAPek6+uNjLSDB1psZvTKdfv+Cwab0ZQ7agDlPW8pDxlNO4FatKf+0fwKhvv8H/M7GLQ00/TUOgnpIQTmm3FLg+8ZzbrLD/qC1eFiMDCkmKbiLj+mUv63NOdqy7C1kdG8gzMR+ck0QFNrbQSa/tQh1fNxFEuQy6axNpiYsv4kE8GFyXRVU7XM+NrBXbKz6GCDKs2BB9jDVnkMHg4PJhTStyTKLA0R9mKrxAgRkxwKOeXcyf6kQPlIEsa8SUo744a1BsaR18CgNk+z/zybTW1vHcL4WRzBd78ZSzr4yIbaGBFiO2IpgAlEQkZV+YYaz70sBuRS+89AlIDl8Y9/nQi07thEPJe1dQ4xVgh6ftvc8suKu1a5zotCd2+qaqjSKc37Xs6+xwOeHgvDQWPBm8/7/kqB+jwsrjRoDgRDejd6/6K16oirvBc+sifTv7FaAAAAAAEAAf//AA942sR9B2Ab15H2vl0sOha76ABJgCgESIIESIAECPYqik2kSFEiqS5Rnaq2bMndlnvNJU7c27nKjpNdkO7lZPtK2uXSLOfuklxyyd0f3O9c7DgXRxIJ/fPeAiRFSy73N9kktoDYeTPzZr6ZN29A0VQnRdGT7CjFUCoqIiEq2phWKdjfxSQl+7PGNEPDISUx+DKLL6dVysLZxjTC1+OCVyjxCt5OujgbQPdmd7Kjp5/rVPw9BR9JvX/2Q3ScPU4JlIdaQaWNFBWWWH0mbaapMBKLoyJ1UtJaM/hn2qql1GHJZMiIpqhYEJescOSKSV4UlqwmwSQZ2VSKksysYBJdqarqZE0zHY+5aauFo/2+oFmIC3Ck8keY9zmnz2r2u4xGl99cmohtpBkl0wE/9GD+qsXn4hJMHd0792JkeHRDKrVhdBjT+zLzOp0AerWUlaqiYIBUWNTHZ1R6SqMIi6YYEm2EZobPiAwv6YA2js9IdhSmqqoxCSoOATGhkoXDl0c1NGfieBp5ckeM4ioUzr77kGCxCA/NHxF+jVGUYjU8P0HVoyEqHQN+iSXxtBHokHhzPD5To4gZDeFp1pOsC9jjUo0yMx2oqIwH7LEZrYrcUrpT9fiWFm7pBJMTbiGxISqWnZRKjJl0SZk2PN1a4tPAB/OSGQZgM2akRhQWE65Xmx/7ww8pa1grxiKcqD8hRdSnWJE/8WrzbX+YItdNcB3+LIyvm3jJqT4lxvhpNqY3w4PJbx3+LUb4aSHCm/Ezpt0lTrjuIb8D+LcY5qcrwib5bZXkbfAh8fwfJskVeE8dfs90Kv/OenydodL6cAT+oVYrq9TpeRih2xMIV1RGYvFkXao+cr5/YqsLy6cRtaC42ZtM2OPmZtSAGK85HrNaVExcpQz5GThWeRmQWW1N0uxlOBRGZjgr8Zq9YzTzL6uyc0pF+T+NK5ym8GZUvTlcjMb/XcmWvbHqf3jY7H9tKufMaCz7D2OsUwhveo0TUAJVr8r+A/oNq9Xy6K6QD6GHzZZsA/obj1qR3Q7n2YOuymy9IKgU6L7sVrsJ/a2hHt1FwSx8MHtK4VceoxqoZdRK6m+ptBVrIkyKdk1GDIJAh6Mif1JqFDJiIy/VgRRrOBB3TZ06PLOSo4pBWUMxsYaX+uFWRMhII7KAW/5j9hksSIUYAkm6Tkht7CnRdoKdtrbZgMshfrog5AKmB/FvsY2fbsfXGWra5gq1Eba/aLW5CoJt7QuclRpBCKIyJenq4FWbklbWwGt3SuwXRH9KjJgkrxtmblV1C0rAhFXYzRGmFiZvC8IyULmRXaX0+yJ0iHGzeDIbEeZ8MoLMFjdtN3MMaob3w/0HC/SCpjBU2z2R8i67fkdr7c57tmiQ0Vii3/Fgm13L68taN3a4q7aM99cVN+5/fKceGQ0l+mPvjFau2J4qWnHxihBKDl+zprJm9f7m50uNNl9pwMXQt9lqR46u7z62s4X5Omf+vmqg1S94y4Ls3EtGX1nt8g1NYw9e0s3+1GD+s3KS+X3L2taIha5VVA9sOfPXbN3aI12d69srzBTFUuNnf89+m32FMlMhsB2dMJe/TKVLYQanW7HZ62Uz6QqQYprFk9nPZmZWJVpZQ1haBYdOIzl0shkkjhMLYzFmRAsvuUF+WjjU8lI1HHbBYRcvDcJhA0zbCXh1WwRT2siWplIpabALjhOtlSlsKVf1gtFsqIbLficcaakUWE3zOVYzQieBx/FYM40Z7PdxtJkIBSn96DPeOB4dPtDSsn+kqnrVvuaWA8PRwUDTcCQy0hIItIxEIsNNgTKFUWnius783mCjV1atPNAK745Wj+xvajm4smpFoHk4GhlpCgSa4N0jzQHFwMQtayORtbdMjN+MX28eHzzQ7fN1HxgcPNDj8/UcODPJ3qPWnt5lQmMTt6yLRNbhd05EIhPwzv3Lvd7l+wcHDy33+ZYfAju69+wH7GGQRSs1TF1HpeNYCo1YCstUmbQBC8ANB24D2ELKbdOALxohXG8Dn9PGS2rgqx/mlh9MHByawNqDtSvHcwms/Sp4dfoF04yBbVy2ImBPiSZB7EuJ5aZ0qDpJeO9eBrcpdXUS35a5Dgpdm+OpXYk1PhiKMJiTVovNDlxPYsZzSIWdRhRxzGKmJ1EwxDF7a9dd3dvTU7P5xpGuy9YmaU7vMKg5RuVvHG9s2ra8dPVa9K1IUk3r9Sm6qwVVrzU5+B9F9l37lZUDX71k+dbGzYfrl199YH0oW65kO/f2l6GLem/cP1Y4fP/Y8ssm4tGhXSlGwRp0BV3N4WDXhrpV949lm3of7TMYN31vffZdtfHvayfaAvGtf7Fl8PBgyNswWI3+nlUVDW0+CK6LQth3IgPxnX7Zc+bcJhJ1eZ9JfvRLneW8h1zkF+HzvpH9kEbKAsoJMwqJLvIZBvj7AvnvMUvtNrDeSuCgCR8ZUYT5hrttajBsUF12xRWXq7jw4FSbm77hyL/+8tdHC1RGre5vsmv//d+ya/9apzWqXUf/9Ze/gudMZj9EL5HnJOTnaE+KVGzGIJtRAy+xsgrgB0sGLcwwWm0HKYusIDLYrtlrkglTbQ0dCoZqWpCbwVNGFQpOqi+//IqjKsSFV0y1FxW1T60Ic7/Q6v4aPflv/46e/BudllMXHP31L//1yJFf/fLXR1wqzMOrmHvoNHuKqqWSlFgSndHoKRXmYCIqlpyU1LFYbCZA6JK09lhMSgJFgRLBNM1yxWWgaZgvSTtY1AhqQnGrRalqBpdnBz6DmfUgVSiCQm5UhPy1NYkkh4woBFoHihm6quAt3sKpVbWsWm/l33KdMBaYTC7+Lec7RqtBiS/rbMYTrrc4l9ns4tiByEGt2WR2m/75n0xus2DRHIgc0GhpRqM+ED2oEQRTgfDP/yQUCEZBs7/ygFrDMFo10ZED1CuKasVfUjqYlyIVFVVxCSkzIhtLUwjjEkqrCacRhQ8Rg6elnoiDjkkasHyKWFqjxfc0KnibVoMPtZQGpCKrRK0XlMpr9Qp+4QB6eQi9ku0eom/pQ9/PxvqyVegHsp4ezM6hIPUNqoCKU2knNgqMHsxuIVYwkQPIC3gU/xQBc5UUuDIbTGjGSXwchp3gxGw5EWM2NjNJosYHq0srqmxlKb9RrVRoi4udCqVRE6xaE4g3VpePjazwGtVaVqvQlibbSmg6LtOynU7QHfQt4PF9mB8S0mTwDxIVUYlC4RnGimcQ1kB5fNbt6Od0YmQE/+0UYOsyGIdAlS1C1vkDhFH0ArrGSI/6BGieOhcpnwuP4Rlnz5x9lv5H9keUmjJSIhNFoiYqacknqVAC/ASMnKWvNJaWz12v9gqrlXTwNGWxUATL9p39UDGe84edOQqdmkzO/6mBwlLZ0xkWPJ05I5XlfFoO75/ju0zNCKhHJquFxjyPoE+4pb6Vd7w+NfXGHcPDd7y5Z+r1O1ZOdh66d9Wqew915l/pd99E9hfHx1/MZt58M5vBR8j+pnTqkeXLHzkliacf6el55DTm7yxg8RD7TYqnAIkrMfUqFaD+GLFt05wSqUE/haioBtNmyKQZNVZHhgXNVDP4UK0EzTTBaBg16A6CsSAODnR4JIjoKehrTRJ8rS80ix7vQ01zVjTAZN/SwrRRNKFDpx/q71fc4w9lfwNmAFHXAz1h4GeMWk+lKUxPpTaT9mBuGrHKxKOiS+ZmeSztsmASXDA5MG+12E4YMlIN5jHmLevBvK0E7ZYU5WDKjMI0a3MFiLOKY63OYS7MUuKr/KFmJq84KvBWcW/MVoSu12nQfzbtGqioHb+4teui8Xq91kMr6Wr9wOH7xkfuuagjtvpQc7be2x2gD/IWv86hRv/VfPjSK7qHLukPlPfubAog9fovT9ZUbf7y1uHbr72sJVutVpv5FJkb15/9QBGF8S6nbqfSnXi8HGgP14kHxoFxSMeIImkAPTk6Y3n01BMVK09KpcCFUlmnkiAbdxL/kdsB3HDzorn4pCC1ADt64XZpJfCAUQMP3MI0F2vsxGZUcoCkJKoFrjoFsTEl+k3p8krs2rGBxQbAg9zsvN7VnsusKFrEKzfKI6jrQ3q9zsKqlbZA7cDOjnW3rY+Ub3nskg1f2lQdX31Rc9dFYw2c2q1iY4b+w/ePj3zlQGvFwM6mRx9ffuXxySue3N2Atgis1mgxJesbIoVNGy9Jdlw0XL2Mjgztbx842Osr69nZkmMnxkbdh1bXG92v3TF+7/7m9j3Xw3xsA/05yj4H+myjeqm0DmMi4qYNgg4ZwiITlwyg4GqILuxRUXcSwl1JC8gHjK8D640up8WCAQ6olIgEsIx5XbYowwjMrhfceRK0OpFso3+6BmkMxt+NzY0aBWYzvZdm0G+Zd2Y7EjpDdhN61KBL0H8SSi1E1veCrBWAHaLUP1HpMJa1msmk7VjARdrMjNcUtgOF5rjkVWfEYqCwKioaTkpBEGJ1LnSd+yOJbEQ7BDYQ0UhFmlOc6D7xquFXb92Ib7BicURyF6nhGiuZbXDTekK08tMWq9kcflX7lRO/gnfpQD+mPe5iczgNv4tvLb7VrwRVSKXhXfBCzVhtbosnIgegGqvNXuQ2WzzFiwNNBFSB8jiceIaZYOqnKSZINEeOfxaZK6UqZMas83sZYtjmwfa9hVqLITY41b3qy3uaIuvv2lR/fU/rIfq2AvfcH9d0XVZ38OsXNwzd/OKOxr2bhg6WGj0l7sT2ezauOLa+BpvG68othdkiwdh68aMbLnrh6g5rIIrt8W3A4yrgcSFEJ2DRHJjLPnUmrcQ6wFU4lDCFOCVMoWpilotgChXxUghEbwY2x+A1VARQQ8c5VGSOVPjw2Mw6eVZgmyF7BNW5Y1lqoW9bvRXdJvhXZ4eKa22NT29Z//Ch1u4rpV3bnjnSvjG+7oaRsTsma2s2HRuauHNLDfr70ZM30BbH3PfKewPN3U0HHt665amjHW2XS2Mrb9maTG6+cXDkxvXxlq1Xy/70BtDxHpJvci3ScMmoJf4w5wSxHwVoRMJMlEiCzt7A/LVKObdTXWhvpx8ymGbf0PHs7pYKwaU5/TPeynoKrDz+fIa6HHhYBjYpBJH5IPUmlfYTOwyxBEnR9CkzM21JvxF0tS4utangqUOEmbI9Ehux5dHCsTYqNcomCvPVbchMW9wxNYQncHFZFBtxaaWs18Lzb1+J1ZcTWV7sOCGl7KdEJwTsdSknCcxZZ6qDqOMM66yTD0lQvqwRZGX0VyaJrJLYyrnBi0p9bXBk0abmoxKmdhEmUMno9byR4ZLzyyOrLu5q2drur9/7wOZND+xt8HduaVl20arosiue37nzG5cvm6zdcsvIyM1bEsv2Hmtqun5qWTQ4dNmqkcuGSsLDRwYGjo6E0dVDV65r4k2tY3uaB26aTKUmb+5vmhprNRmb1105tO7uncnkzrvX91wyGo2OXtKz8er+4uL+q+md9XtHY7HRqYbmqaHKyqEprNsiyD0GcnGDdwTdNlP5ODuizsy4AmYcXLtUspMEcXiAzR6eQA1tzi2WeTCMtrvMhF+RAOi2lrKnlsbMKgSGDkdrBH98gkli1+XHJzc9dnGrPdJenr3e6B9DX/fUWBuObxq/Z2/z5tj4Vf1rbtlQFV93Vd/QjRsTCuX6Rw63tx15envdju1TTXM/dtCrwwOB9uUNU/dNDl0zHm3cdKRpEKZ1fN01BFPdDZhvmPkF6LefqlxAfaI3Ktkx5gsQEIsNtzUjFpIXqeR8yE849/Ru42IgmDz3bEnWdGwJSiR0AaaW6aqkOnIW3Ap0GaMyFo1ERdNJiSqGmMUBlGnJixQFvjtM8+kLSrKGwbU4PpGmCJovBLqX0K08PwZnrj6H5DnqUzH5E8jIPKEYBD9JmWsRsRRKFYToOHB6gqH0/Nx3fKVhD50wGugHytGtHTpek/1XQavhs79UC7oOzI9n0X8yp5jLSD7dJSN7CHMA1LNYCdVRSTNviRD8PMsMzkrMIPrPvj7U2t9P6IB/RgWS6UAEkiVwpIaCTQhZEdIb6WRxmSUgzH27gKGQsUNnUqFiXsNyauTmbB3ZS8qBDt/ZD+kfwLwopeqpKSpdh+US0ecwuBdj8IaoaD4pmTic4Zi2m+IcTAWQUFlUiltJ1qMQTxKBpIglkxlPEm+kDic94oLIp8RCAOrE1XkjcI/SmoJyxmMeAimMyB8CG6PIzxGAu0vE6yvsGtlSv/yqTXVVvav7amh9B1vdM9pTHe7dVNu5pTOkMqpf5FzeRZEKGy6Ml9rDQxctX3FgtK2u3vfMN9nylsamgcmu5Jomj78ioD8zcB493X9WryxlR6gV1Gbq25TYG5Va2Ey6pRfDw5ZOgIfGqGiNS2FFRlwVE9dHJQ+bEWtBbBhabiG2ox5YVc9LLmDHIMSkgzzG+DNBOVsQ5KUqzC8uI22V7XdT5vffku33OC9OnJD8ylOi7wQ17fOPTxC7PX9EsINpUDC9yFo9tS2964GRUlUQT4/2bjI9jC0ksSqth2nygpZymarqc+klUyKwiJ8h2TjJht1mZzjQ4nPsFMIpE5siHktgMOtBSoXfFwjSJfl0kzmCsKT2H/khsj9yy+xbFzfsvG1wYi2d+otVqVV1Be3XvHZJYlNwvV5vD1a76vcMV2197tfX3D77xoGL/w5pvnrvme0qHafkL8q+/8zx7M/+8Ur0nqWssaxksKfFNuys8a+7Z1c9HXsOlbx32ejx008eePn6no3jG0dLuzYk13zz9jGTKftQtM9dWefVNR36y8l7//VrPVPvZD967IXs+69sXNbOcsH+4anvo4o1Zd1xt7N13yhqUqn7jn4NyxcMIusC/28AjFshR0mAa2WYq+EogLmSBs9AexRj2lxEZsZBD4qTXBSD8/5+sxfBVAMoY6RX7qJXruTM7HNzdc8qLMYP6VuyP1VxahWnYo+fXmM0oCeza3UCzdE/EyqdTpwJxjjhPfBHXwM6LJSHKqf25OI1K8QvBI+UQ9BS7CHkFGNywkSzrGaMbQGTkqSj0ZyZVhmdAAqCcD0YlVQQHFfAjaAVaNaDOnjwgTElFgtwKpabRBUeiOBdEnqUeGMJIneIN4kKBP3e99BjV7xwaX1p/97u515pv/LFi7NfRlN/9U7Nli+tzX4FNUzetTb86lvZv2OPV2+8dU1qz0S7yfXNv1j3lR2JVU9+tWtff9lAfNWeui/fQ+zl1Wc/YCMkLo1T6Qgep1ubszAW7bzLdVqIn6Uki1swzWgpQ7DsXN2VVwEUckY0p4cYSXrkXCiir97xOmIfHjx2cFtVsdqkKapoXn2w+/pfPDIx/sBPrlhx2faxMKtValVllbuvumfintMzk/S7TyL+r/fYK9rDEb21OFhsXXv8w6/e/+HT46COIYVSVVE1kCza9TYyEdsAMmMfAJnpKSdVl5OYgclJzMlk5nOQIA6DvHSmssjpSMmJY6J59ucTFCXe/JTzvkfzD2Rf3LbtxewD2Qn01LGf4mTET49lJ9jjk29k//j0M9k/vjE5uvqJ39137++eWE34inWoAejRUd05ajR5ahRMZoZVE/1hMWF6QpjGLKfISPpMowNrRsfkXFkuQSYnx+Sf95jJOSV92dyN9Gn2+Jq5F0fnnlhDnfNcDdUqP3fhmWqWPFONn6k9zzMhKs89ULfkgfLj7p6bwg97ZM3cdmped7aC7tRQ+6l0FdEdZkF3ZkrKqjByK8GOqjavRqKTl/zA/DAE9v4wfq6/FJ6YwDl7J1hLga3C2dmwIBm02GqWgMKJ4ZRkKSMOyuA8j97Np+JziocD2SbkFbDqgWG8evsbyPD0yO1Hd1UVagSN2tiw9Wu77/jNo2PjD//LjX2X7d5Ylf0PHY++lDh8w33rHspmX91Ov/sMEt7eZatoK680KpSV1aGJZz685/6Pjk8YPRUF6CZOk5qbCzaUWnPqJ/OdrSXybslZLpVsuUQ2PsNoCecZ1by0dWYcmos6sloBMiD2IS9nvCgfx/G48N5u5rZdu2YPs8fn1tFPnF5DvzjXKz9vDn5th+cxlHeRnHHqkWTr4dPwDzv/iXO7sMWT/3bt2Q/o78LfuiAOkiNJHZMBWkQljnAoiCoF8lkFZJnSDJ9TiKeJDqdTmZSoFEQFzqWSVY/5mFhewQcrvJZmEK3nNK5AxL3iyrHI7qb9j01GNhq4IqOGU6lV1dse2Ml8a7b+slevbuUIPX8C3vnY5ygflcrxzpbjnQF455V5h7XITwbnI7yTApgmxgs0mVLyGOXFFrIERnLmduIUUIQJI+FPO1ebixwWPb2cL7SOzt1kdpttPoF+cLTAZph7QGe2e53rwU1sZrScjh7nublLLKBbLuvccgCKh3SCjp1blpMz83vgHZv3UBKTm9dIVOZ5n2aofDpRUi0I1freTloEMYjj8zqj3A+f5cnPVVHIjdsYz9dXeAQS7OBMpAA4DtdTmCDYEdU4I4kzgOrClDx8wArIZgehEA6A+uDsZBj5QshmFd5bzgkaerlRrzRo6JRa4HrWK+b+hivgXca5Fxn2uNIwyxd5eS/H/N6gPL1G8eOColl9QQHzX+6CM5WL9duUt66iLkerBmg1E1pNAsGceP1NB7RaiI/GNCqNi2gMYlXx58iKA1nMs8y6mIObHQY6VPozDk+h4sTpNRbFf3gKzjRi237V2Q/ZXy/NRee9lF+7kIu2LOSiLf+7ueirtr2UvRes/uQkWP375l7atmf0gZPXHnvvvlWr7nvv2LUnHxil330arMTuXe9kfw8e4Pdv7wJrIDxz3wfPjI0988F99374zPj4Mx9i+kG/FfuIb7JT7Yutsh2QhM5A9FuHk8AOMgw9dlExUS97KRamnxNz0o69FCt7qWIFAQdeJ5oHBX9Cl1BnEdN9w19dmv0D4jbds7vu+9/N/oE9/i//sPHRi1vnXqYfrN1wTf/TMzKWvir7ltIDPMX5pMF8PinP0wrtQiLJMp9IwjydTySxVoeRBNs+B5BlTYkVQlprpFJL2YuDbjILP4vNFcOHe9HRMYtPn/1u211Dn8nxfW89fm0ku1fHoRUFhefnfJ73Pwfe28G6rM1prkHWXMkH7Lc5CPttqnnzYgf2O2KiXVYkzP4AViQ7aI9JKy8cCjjJbCP1EqJPyAslF+Pa8mYHhZETxRfkc/DMn1NT92xymtFHa3mHLlsllJa/Obvpvl113307+zF7/O3XRm7Z2a41uubugPiwz26aO0j/PLL6aP8DX5XtxfjZD5h3QWZN1D4q3YAlpgXbo20gK2k4p16ER1UK10qL8LVSP16Ea46KjpNSpSEjVvKSEYaSMGSkFnitdJBVMdEovKC1FJXEGnBcmDCJxTC6Ui12t47iBHG3udqPnNyU+dBEpVT5ZCmC61XmwpfxIj2vKSqr79vavPqmDdUt26+75bodzcndD00enO51agRD+fKpwcFLV5Y37yB3mi/9+v67/uH5SqMjUB5w1Exc0T2wtb0ynBi+YkPPjTubu3ujAgpGQpUrttf1buqMVCaGj4yvfezSzm0yTwIg31tAviqIkck6jyxaisGLPThYF5UnsRDTrBKzhMVsUrL4UInXHhciebzuGFBsyzI72aHx8dMiO0Q+/ztnf8+a4fOdVJJKW0luWyvbe5GL50ElmHxcUAb+W+LNuaVmhkyL3Fq5ZYmTjNDf2dV08KmdO5+8qHFn313fvfrq793ZT5cx18xeu+2b1/Usv1bcBsfXHPnB/WNj9/8A04FjIyfQwWN/z+NxUrKDxKtY2D1QEsXnYKw55wsSOWfoN45ADIT+02zQmdDvWLNxeO7ZDexxo+HMimhtslKR1gkADcBSU5Tqx/CMEPVzKh3Cz/AUB+PxOHmUxLnjcWxpsV3FsfHbH79/guTsqQgnKniR4iXGcYqFQynkOPVq4+/e30VuB3HV2QlJy58SdSdefcf3fiqf0OdE7wnJrD0lmk682lTxuyr5ugfXNvHY6Tl18HEumIe6UwwFGq7Q6kxmp8tbslAbhlp5Kn/d7Sn2lgRD5ysfk6gQYEuVzS/bp3gMJ4TmfWXMds4p8qNgSAlmS1jjVqN9Sg3L6lTofoWFK8JsvF+lY1m1Cu1lbNxQtm5DdpVaqdRkR9azxwvPjFuiLlfUonhaJwB7xy2VLmeEnIFPzTgLC51n7LLeAq8Vr5B8fnDB99N5tSqKYuNDSTT2niob8Z4aRMSap1IjWxmSCfcLtD6r38FxLHqZUbPouJLTTWZ1tGYHJ7DZpEKbbVWZ9fT/oN/Wa+ZuVBvV9ISam+ucMwMmeMDIzV2nETBNLqApTeLeqlwWlsqDEaucaALltuUySQSBUPJBXuUWMxGmk2steHf0MGdVq60celhp5tbNZXazxw2GuR2OCps97KDv0xlnn597ll6Nn38JPP9pEv+7c9gKcClZ4ZADJS6K7RdFFjmTyIsXAlTIa71Ez9w/e7HCzs3uZB4Omk2sak3AZjk9uwZ/5jQ4w1NKAT4zSjJ5ajYjqqISYsnn4cmr5jNpNcFragOJunIPMecXxuJ4sXQaLTNxP/4xZ8r+QeUJGIRT23hDCYXO/vnss/TJ/Bo7tXiNncFahmWkLi810leWCl41+6PgqazZiunaB3Sl83QZohIDdCnhT3N0KQAGAF0KPaZLgenS5Omy1yQwvJNDHO8+HlPFo87s6xkDr3yA5wJ/xnUxP2DizLcIXsvX81CkGoVYRXN0AZzll7TlBIqcOMFZlB+g9U1owzKdif1Yw7Esp/kTyxuYOH3J3K2cFr0peAS+WMi2q3lZn6nsb5nQ2QjEI3ZcayBRbAb/kFoIOQqxgo1lQrP/+COCo8cUT6KvgC/TgF8majaj1FNGXC1DQtMZ1koZFPlI1EzWbDGBYxucDv2jSb1Jzb7Cmf6o0mIfvw/84hqFHuxWkrqBShfg2eSN51Z32EzagiiSOUpryLq6htOEZ9i434IDcExi3aJVHoxwRDYmuXD9Mi8VGTN4MqbwWjNmlpASY0Kas2BDIhaZRDdMgjhenqHcqZSkYclb5Hx9Ert9kjGNotyimoCPlxSHQZS6r+ehj5+/7EjvjuWVRotOGBL3D1++sizkUXHlIxO7mmu29kU2+JK9pQ1bR3sDf/Hjm1s/bts3XK3Yc8e9ZdVl5qKh4ZrNt47O7Sy6rqy90u5u3dob76uyuyItJUirCDSPEhwknv1IwYKeWkAfVlJpDvOIiksO4IoSs6dYlRFRNLcGgau3JVqIkXQWrqTRGMhKhFRkxWiew3C6GNBDWiMwqRy0F/AYTbkYMARhedI9D358SpW4pTN94LUf1R96cs/u++uUjCNYf+e6iZvXRp55aNsTbeyP5i6d2Jmdy84eeOvO4ZGVV7p+MdbdfuTpyV+f3Lme6NfE2Y+YvQodRF1Ncl2mVACks5h0AQ4E4tIFPQY8lWQINiA5gpVcKAAoo6aK/fPFfAS7yFnWxXmD+WwVPdF8+Ln9Wx9IOVmtWhtoGG8du3l9LL7u2FDv1tagzqAucCyf2FW/+bGL2lD28InbBloSflZd6C1oPvzUjqknDzX6y/xar6c2ZF124zvA+3Gg/Rs53q+h0iY5eiK8JwPwAO81i3mP2Y5BhJqLxSRdjvcFmPesCfROJ4hGnEHEEqDUxkXLXDY7ia2iBG3TZosNJ4kFOR88Dryf2nFP3ZaES6HtfOHgaz+aJLxvuGti4qa1UXQGs36gh153OlLw6LoppEAKzH3ataa77cjTWIewDF4EGZSAf5ik0l4sBUt+EBXKzEyQ8+KMT1AxHz4YDbjiWTTmIgg+F0EYgXLW4sWTSCtIzkKsUBwuhaXwcUoMCgCtFy8kKf3eT4op6c0FERMth5/bu/rLU40Gbs6T2HLb6oGD/ZU6g6rAuXLrodTOr1/eMUk/Wjl8aNnglWvraNO+V27sbzj01B47b7no+UsavOU+LK2gbfnt3/7J8HUT1bF11xKd88Cgr2Rfg9c2Kl2IpQZwrygu2ZUwV2IYd6lVGUmHRwvBeiGpdCuAAdti6YJCrI8FToCY3hzEjC+GzcQyFCEZdoaCnucrhy9aVtzqZJBZX+6JjTb5UF/2pc1fcjPTpdeuuX6sQqeN4pxG+66Bq3pm9zFf0tJyrnogez3zM7B99dQQNYni4LexMDYpM9N28yZ1WHIpMmIiKrUCyX1RqQI0LRyDQEdajQ3fNiKjBj4jNvCSUgc2jicr3StxHoiDaB487kqBmMW1OAaCQzcvdcFhtZBJV3fhMVY7YIzbZUj4pw9OPCkvl/Tz4vITUrn6lBg5wU6HyyPm8KunzCc24SqN6Up8Cm+Z7ulfbg6n4XRRrQZcw7UaL/SXV0aW9+RQ3ov95eGFU3mxZW2pYGrVMGabX5doXb0JBy9uQSwATeprBU2qbsDBKISlOGXlB6tVCmerBUlXAq8u0zTnXrmWWATwp7nq3vkiX5vdiwtS89U/IbIEozzP2roixDFLl9YHdq+PN/LeiKdnZc2mm4Y7DlYituj+InftxhtWji0PVzdtv+7G67Y1tx55dtfUY/uSayLj165acePWVHzV3iNHa0LtVa6Wku7tbe3buwIly7a3tm3vLplaebhYaK+3RSNlfPltG3ovXR0tdvtctC60Odl7ZDRa4Oz0VERtSpU5MtLZcslEoqJvS0flQJ3X3zJWU9XgNQBANZbGGhkqtbGzpKRzQ738ulH23U+BIv0d2Ccr1ZXDovq47BWEnFewzVsmmvgEHOnoDWTrjGSwkjASDK2cH1zwBsTjCbL9F57a3P3CwVXXrApvOXbT5Nc7weJfvmZH7eSd43OH6dvuenzHxJwC25j7gaBB9gXKDDiimUpb5msBjPpM2opwms1xzsYjC9l4ZDeQLIlkn8/3fLJaHgdi93POYrPJ6+B5h9dk8jq5ss3shMnn5Dinz2Qqxq/Fp19mzsyyFH3277M35mgJ4ayuk6SbgAwtwnAdMJsGMFuMZJ80JzE/pu0aCwfzxConn/QaIMbpJ8QwpPAMzPFConQpfXEWGdRu18jQZk/j2mZ39KWltGYfrNarJ0YUV545VjvREdQqv7OEcpClCLJ8E2Tpns+lWuJpHRA8wxRROpxIZWWReggX3USkUjHJpRaB/Pj5XGrifKlUBHhY3FLFOXl0r85hXp1t1pp1vF2PfjrK2fTZVUKRO8r+aPZitRFdrzNmR7UmpdpumMvqDOg7Jm4uS/TtHfgVABoZsKwyjZigXOYaBIl/FjLX72xmf3Q6ktNT9ocEA+zLxQcOP0SnCEYny8QUl0pBY4tieRBQYcALHGIFT3I4fsP8pgCHjA6kCook1cQAdjhgJkQDKRo04RQIjr1YQz5z6SF1gTZ7bmk8p9jcOSpeW6DQuDsG1lQduMFh6li9rbb/6GjllmuP1G7pq9h86cGRO5PMGddXyrviBddd1LKuqSi25UvrsPp/7cHgwEX9+Ojuh7eOzWbzcxLGaqcGcjziciNV44lpVs2nC+3yGO1ycofLT4TcwIwCCdTM1HzykAzlE7MTk77slUMLExQovW9sz5IJKmOZ00DXObnYPAbwq85bF2z49FzsZ2xVabn0+X37nr+kpeUS/Hppy2R07c1r18rbTPBrFGWPvHVrb++tbx05cuLWnp5bTxzZ/uThlpbDT27f9hT+s6ewXXkqey/QrQcbF6DGqbSQp5uwVIOJ94Lm4ACuZB4BszYZAbtz1i6INzNSctLMLUgagVRO4FUrvUUpozCBRCrnQGEnOgcIP1VrEJAG8NfrP2w48OTUznuT9XetxQDs6Ye3PdmavZfdqjM+tG4qOytj4b6+rJHuHlsug+FdG/BYxmEs34CxYDw5LuNJAibxNF9AlNxSRMlhIF8AiNKQQ5TcPKI0yFpyXkSZJOGmcCFEueuBpAYVJbZ0Tu/PI8rkl9cuIMqhgUOu0w/RRRM75xFlwaoegihzc5r+PYzFga29nBmfl4hFlwEbyhefiMo10k4yGpi6JEDDJstIVhfs86sLMusXMpNYs+MCj9TVTxyJrPBzjKC0+6qLL747wpzhTO9dcbvZ3MEjjVZ9101zu/JrYwwL+t1I/ZBK15N1WyUEjvUkcFRowulCTFkIroUIxAv5cMjRFBXtYG0AH1XIfK4VMlKzDIren3zHIoMiMy8KJ6So85RYfQJOpk1mAXBQlJ+uilYDDoLfi3AQ3CQ4SDCZo1XVORx0zhlBQRU4L61UgAw5YVpTGMA1JWKtSfL4sHKGNDiNa/fU5tK4i9brzsnj+j+Zx13rYPU6Q2nz+q62LW2+6qFtU9uGqqNrrlyx/ktNNpVRV1I/2pRc1xqAO3vgTtXaG0anHpjyqTXeoDfQPBKJd0S93lDDaGtisr+yNukD9+Qqru0OVbVWFntLG1c3dRxaVd1JeF579gP6QXYT5aMOydG7HNIVkJDOpgnjLUieuKQmsDut1uXr80nG3k08r6iKpfVufEOPN6G4Sd7EjQvo9bzEcBmcksAugMHLyTRwRifki9Vqk2Q7KVnoztkeHGFgh1eL0yy133Aigz6CWrMnrMG4u6Q25ODVBaEjbTsu/rLOyDwb1KO9Gi57ec/cQHljyGxzWbXhcM2hI/TLBhjb7aBP32DOyHbcgPUbJ9YkZc70iNp43o6D18NJZA1ojTFG7A224xqG1LiIelyvRUlImfPRJKssT8aFiC9C37712I1bv961JVGENN2vHBq9elUYHaBvmzt81xPbJ+jsLFtwz9huMOpULt/HfA9oM+Gcsonk+1Au35fPEFGmCyb4/K5+zqRAQ1ody+o0aJg16Xuzw6uZM0bt7M8c5TZbhY0J6DhAUvhZdvDd/wAIr5z6M5Uux/6sME4eJ3EFOK8cjuLyGDxf3tG+f2w+r8ySvLLCcIqFQ6nccOrVt3/4u5Q8nXy86DkhCcpTouXEq43Z9x+S88eF8GcOXizkJTve6OyAUFp96tV3yt8vJiXiAsw7wQLzzsdPF/s85vC0F/9Ow8VFsw/uwIvoTVGtOgUrmCx2h6fY64sszjwbqdydgkJPcfk5N/PTExhYjtdo/amlLASjGsuv1+LKa7wgKiff8KKtvZczMwipNApWr0YmlbXUrkIGo1ahUSNaXbA8+9xyXpX9LatmGDWb/XeluXOB7WE7E7bbZ9+NhG0VdibgnGVtTIPRY4T/Z//GllszYW4DuRfM5575eJpGueWEwihO+eRzz9bFuefEeVLPAXQg+/B6nHoOKzhkZ3ntRPZBdGg9zjx/l9Vm31PxOlqD/qDXZIcEC7pVY8ia5/4gaNDbFmN2o8aIdQP82feBHhvBg7IKitboQqEXZb2gFpJ93vYhI2jiGqVWweqUaIQ16/rmXlRaTMtmCFt+aywW+GKecei4029wJnQnPKMfeLACnrko15xPhZEqzwvkmvuN9DVzX6F/aZw7Rh8KCVZm80CZTZj9ywHM17bsH9AZpUAtR4cosT4q1bAZUjwKIbgtKvG5DS4tELu0gheO8hmpMBKLpVuipIARacLTndEWCGZUHfG4VA63PWG4XU72zJSnwJYJMbzrhWyYeOOjdfJW8NaIGAZd46WI5pQY5qUOzalX31r1kYZMIW1E9ETw9uNCuOnhJRW+WfxHA5kJWn5arVXBBNDg3zBhposK8Xxw49+vNs/+8XHytgg/XREJw/VK/BueNN3W2gGn7fh3Go4Xpo3YnkrDu/BRRSoNn7boljuVhufgI0AarbxKrdEWFrk9eO9/a1t7x9JVG/SSWlPkrqic36uen081oJXleG8PBCIlKdFmknTFZHbV5kAj9moNiKTuc8m9RbXx+BQv+BTN11jiP2kLNJTbzHZzqGeqs86k9lUsr3Gb7CZnebLInSh3wqG7ZnmFT22q65zqCcEbbeWN9JYWW3nKW7dnz5765j0rKsI6vSc1HKvfP7UnGWyJFquUxVXNwcTU3n31seGUR68LVwzubknB2+t8deV4HiJ99l40DvrCyFXG8yGQMUN+5BAIgX1H+oHsvaqjf75JxkxT2T/QJUTPrqPE5fLaQV1USoKe+aNSKKdnEJJqC0HP2kGRIm2gSO1ky2V7HehZU7tGTZpfYD03OEHdmuBd1c3wLq6JbNFaDuoWXFC3b390j6xuzogIonDyUjVoVIQo1qtvRT/6K6JuhojYFsHldc1ws42XtPim4Y8XET0y8NM6gxYUR49/v9r84R93k+tOftrlLITrBfi3WM1PR6sjcFqFf7/6VtlHPydva+anW5rb4Hor/p2GP1mkXAWpNLwdH0VTaXjbolutqbQe7/tNiTqsd1qd3uB0FRRGAEY1t7S2fVLvdHpXQbSqpfVcvasDPyxx7aB3SQH7Y79JclSmUrnlmEWql9uTgU9BAYNN89tpSP7Sukglw2iK1/gqemrcZpvZWZ5wY12DQ3dNT4VPw9d17ukNWWwWe3l9IFBfbofDUO9UR92vZUVL7d8LitZcVaxUFUdbSxJTU/sa8oq2Yk9zamrP7hRWNNBSUDhQu1TznsEKoj93odcVFnoOrO1qCuyspFVn0layNdeKEZMrKrFwhXWRBXNeM9/rxWMktUg4zOSNci2S0YNDCCvGmi4t9nSOxTEdAZrxXGBHNtjd5W0eT9Xu272tItgcdgwWN0+kavbt2VYRagw7EHq9bvPystLq0oLqztK6zd34sBAOSS8amCvHAZdzVCHY7jSDDbVenwFvhVdLyTqeNYN/pgvUOCFUaMD3REucZGStMRLEFRQCiXoGU6uHQ9Ei733CpC6kZJJxMBWC//1E6aIuNPNNaDYyz5cmOJevFO7VzS2b7z8TmZN75jyenWPOKLJUlKqnbpL3UoglcakWAjJ7LF1LKh5rCzVynIZXARIqnDAmpfwwiCogtkpuVhAE1FpbfFIQw3HJDsdBXlLK1eliAudnbXCgi5HK/mCCRPeSHaPDEhhdohZwP0cJxfNrHov6dXCI9Osg6QycSs+37GCSuZYdj7dd9fJhHTJyJfrxWxMOVmPy1Q2nKgZ2dpXq1GqF07FsYk+DfH/LXx5u2VS19pqhyg1fnqxB2Yv+6tZB+kcGy5/UDVEfq3a4C9jZa2l/qVfBFrtjQTv9Hm7F0X/Da5dOPnKoTcVcybRe/ATWyS6KUkyxLwPXLpI7PkiVTEY+ADea1uHcm0uTmaEUcZ0hLBbH8eqiWCIzLnUSR4QhvC8olg6l8nFZOhXChykKF7am4powZhYlVeIOJ+UpyaUAbeDNsvMgi6r5Dg+Li0oFeY+fQLbjx+UTvGVU6DILxxO7Htm54tLxVltIYxA4S7RlrHno0uEy9B+CIVvT22oPO5ig0zrr8bfHi+ibvEYrqtz4xJHOYNtYtZ0VipuiBbUbb1yZ/XGpzpT99torKhSKMmNRh6GsYagWrZD1CVEQNm+ASD9JraAwIiqDMCgOU1Qpr1wWn5QCoAkBnuSzOC5DFivxFqiXaLVgcRX5daROK14GV9Q6coWW1SJpl6PlpJ1UmytVdlVIbuqgCpFceCKpWpKNeTz2cORAW8uByMOxh0rC5SUPxx+OHGyB80diD5eUl5WwFX3bU6ntfRX5V0V5/GF4Y+Ch+EO5P4yTNz6cP/95altvRUXvNnh3f0VF/3bQhTWgC+3scaqYuliuTMvXusy4ChyUvJUUr2tYYzNuD7lgjEtuuCCAOnhxuRPePYXzYqZY2u7AOmC3gmHjY2mHHZ85XHgvcUzy4USZg1TNALLwLJTPEIyZT4B6reQ/XJBbS/5bs7LAgLaoOVYjoC24nCa7Ak1mb0GXZm/ZLL/A5eOuuTWWgOAL0cd1xtnvNx5pzB5FN8ELqUtb5PtVME7i/dVk+5cihp2/qIxJKrCxmnkMwMg4YACQAFMw+2+K9Uzh7G/kGrc7z17GXEP2Wq+jHqHkuWJTZtI2EinbBBhsNCo1wJUGAjUbEtimrycGp4fPTCt7sMUsADTQw+NeQ1IALpYHRuBiK1xsjWIwipsrbMg3VYilxB5BTIDjNYl14GOFVr3OzHhC0YauwaHxCZyDGDGRMjlbg2B6QcmVx4YmcrYosWiZZWnmQTm/4zoYSp6brADjpAB9lRdd0J0bdtV1L8pGBBpGm1Ib2gLxVXv271kVX70q2UUyEg822VmDzhBq3bCsZWuHv3bswMX7xxJrSrsmtmyP9LSUNI+s21Sxtp/+58GrgsFt/cmtA5WJhN/g9LiKE8tLo8vqotWp7k0to1cFQpPdJGNR51ervcFiX/NIVc2KxupYbffavvL2RCRc4fJuaY4sT1WWl9pDm7FcShU/pKPsEYivS6gaCu9O8sXJhj9HDL9IjC0GChuMiogsZ2CcbiGL7Bm8WgpyN52bG0WBJeelBkcRRDZ2jrMX87zbgVYaHO75C4LbwZp8HnziEXi33WCwF517Ctq35uwflEVgdwvAY63DPY9IjZtXkUmrcFFGWEEFFOGZsX6ryhCWxkCF+sewCvWvxCjSqlKHZ2rbyb1abI+ITs0UytupCuXtVN1CRuzmcfJ0hpO7n2A1CnaDObJ6VeHa+tExYqCa+gXTi1xhsIrqHsUK1C6I9bLzUuDiQ7wZDW8xWZofti822osX9BO5rf5yYmRN7aabnnh9+/Y3nrxpYyKx8aYnX9+x7Y0nbtpU27j75Y/vuOPUK7t3v/LnO+/4+OXdH3Rd/uy22vH+do9DxWl9DeuXjd42mUhsvn5wzVVJvY7V0MWNT16y5anD7fS7297EH4E/+s1t29/IH7+x/c5Tr+7e/eqpO+889dqePa+dumP7s5d18kXlhT5dgacgse2u8XVf2lpTDngaPmt5x9Fn5Xm8lxmmO0AWQdCWq6m0Bc9jjWJx2Yroi85UEJGIsegMS47ymytC4AVCcqMpFuN+B7gCvK0ihON4TgDkWi3AR/nwqqjDJBblNoFLToBsYkyQqKLFFSzm81Sw2HAByyfbG9VyaG944z1Ty/oqGssKdUaVoXpv1449Xp2O1bpiiZaArzlauMziDTt8qViF7esPML8raY8V0zUrVtqdds5eHbl0W/Zqtb7LEXAaTMGGisJSl87o9FvuZJcRvjxC3UJ/h3mYzKMglZsxMy4rpQY+FMdIaYEL4aJks6Mo10in1my32S0qBm/+NMORES25hBd4H/nYzSP1awaNVv+aCgluDp+rXsfnr6sEN23g0DFea9Trsz+xaNWW7I91BqOWR9ef97Icmz2D1jKn6J9QLFWV3zma746j0Mh7BBSkm1JaQfqMKKj5PQK4A45feIZZuYq+pS97E4qAGzxnfi6jBqknLzBDu7rJLOwCrNTVjT+4qwrUpTE2Uz1IblSz+e3sS6bnMjDt3TFxGS/14bw1nNWeM1lXwtW+ZWDErd6wqo3sHa0VIKoSgyaxEXSou0swzcC0pcitQUGs/RyTlhTVyeZ+SbV0AnQujD7/bEVfnXvo0euP6C0aFBjWGpXZ/6l2FRy894qj+44+9bnn59zzzG2XHN1+TFCZjdmbVFq0Q8dl96MfTa7fsBpkamFpmJddC31+2IxcQLjQ50d9Tp8fC5h9uoPsJV7PjNF/y75K1svaqfn2cXhvNel4klst4xZWy7j/ndWy9VUjB1vbDo5UwWtb24GRqp6SltXV1WuaS0qaV8eqV7eUKG5pOTASjY7sxx3d4G37W/BV8q7VbSUlbatlW3SAGlZUKx6CMRupjYv2QOOQBaCnqImlFaTmSsHhYEZBYkUV1nA+KnInMX4xGHE/krSBw/cMDKijNpbmDCS9gONMQDqCvLtd3ki90P6JeWu2Jd8Carivj97Uhx7NburLbkMP4Dm2lbmf7lFeRVVSvYSyMuCnJSpq45irBQp5x7r2pFTMZdLa4vk+U1EM/stI15wgmDyLIClZ3D0HV7zLIUDLfOMcucfbfOEeaWxI+uYUoa1KzQdFsaDNUVpb1NJrVVloA+Pmrt5YOdTgdYbr3T8xl1qR08nc71ALqo+KUvVN3kCt39STMiPEbtlVEOurLlvW1uh5j2UdYWIzJpm/oPtgPC3USgrCGckAUNYenXHIhr4EMH4Ub2pGgMRE00mxICYlABpWgaK05TeGpClFghh2QYynpOISGGRBldzwhlhuD3IzizreoPlRqhaqExehrwg96VGoWLWRYRSWksZIeWuZzRbtS65fZy+tcbf1mpRmFe/krlpfuSJV3NPcNxhsH6tuGkl5FSsMNK1Wq/XlJUUFFbVOX23QGqMHWv1xH9/eaEGMYssuV1VnRee4RVjdWT1Y5/HUdGEe/ETxJC3k60EVuXrVC9aDknZ7uEr1J4/pnI5NP1cLBsWTfzRx2TmtSrbDt+M1UuYMVYRXSM1yTQvIe37VRSwAxO0mk88lkLIW1zlrLx7sU+T+YaKGZHz0pvkVGIm3pS60BhMMAROxn1y8FLP8Gzsnbw6yTLXFkX2HrVu8HDOxYbCnYqIkK9kI3cmzTYpfQexjxrU4xFroNfLqFplteo6UAiOs7xzpqCca+BlKdoVUFOfecLsoDZ+RrPOd9iBq9ZPthH4Bm4yWi5/ZTf/bv6/JimO7jl/comgbvmFDfNWp3yodp37L3JWavAXTcRz9GR2hvwV0RDBynWH1lAXcjPxCHg9C0VrJRfll8QMXWajjfGGJxRYqFITCkM1SUsjTG+bPgoU8D54DP++m7N3op+A1i6ijFMhmRk2UP60mi4Bq0k0OpCWcnDHJ3ssk9+/F7W89ub36sd91yjlKIcKJ/AmFZHKd4kTzCWqaF0xmktyDcD+/VV/A2aoCbF7VBaQlUq45FIGOpGNpMr4QjdykVWlZobDMXVPvirWXhpvdazcWxrrKyoeyf1Wk1xl0lSGX12Zgb9nCNzd6qn1mB4zpPrBTHcqjYEF7KHD8Myp5QjO4AzMelgrl7KWaJH0v0IRMWNSEDNMYF+JWb21cSOLJG7rvpw33ZK/4S8VX1Gqdmn39jbmRWIwuC16rRFpix8eZQfoJ9iWQo2fe/xQpiP+x5woXF/qVuuR+pSSz51rwP0X2T/E/NtlngzEZLx2YWtY51V9a2j/VuWxqoHTFnn27p6Z279ujONZ9cGU4vPJgd/718PXXH774hhtkXzMD+O6XgO8sVBkgPCSWk0BYG5sJyo41jOMFmItpJW9NkWqqZA1etMUdNZhgbU0LMluZULBk0cVQ/uKM6nUlXqBUvq4yuT/+2C0ghfo1+QpAPvnStE6PKnUGBcvpUIXOwGv47JVc9gpeI1zoBqZbQcFEYb/MPg/ydVKl4I0el3fmiP7czkhLXAryuHxB9MZnymThF8XSZUEs27JCTXhGpeSRIbygGMRzfZo24BXiAOh7eWzGn4NxMdKJJachYkBIuwrKsCvwk/1HUlmQtNzGu3YrU0v0BzfzyC+j+UsQvmMJI6u/1usjjcCSt/y08WvZK7F2aXSqx5i41mUJz35XV2hCZ9CuzmuFA63ZaQfdjkoYxYevz6ue5kyUvUEwn77UxJ1Cv856S/hvfYsvQWscRXLNKubbVI5v3dRjVNolr0FKHWwmz7mZsloX3phXBji3rJYwLEIY5lrCsOWfi2FSPbwhQKo4Ai6YVD3nsGzaGqttJUFohwu3WmoF9pUJaU+sPtc07kI88y4FDaoLgIZzGHmAqdE6rTIj6QGl+kOAE1Y7hhN9FqWVttIO7hqAE/U+gBOen5jLLMjlvAB/nWqeYIxmjDGE9hYzomnFlp0uDDK6W5sAZCidYayro0RX01Qb1UdNAKJ7jUq3Y66PxtOVmOPL4lKxIiONtRN9HYnPrJVZPBhLryUR/9oVwH5DU3slCAUAyozDjg9zIAWJm6JiwUmRj0kx3IwG56fr4CDGS6tBW9fFZkZlbV0RkzYD61fXwWzuH1iL9XRUELuB82vHQBr9KbFJEDem8pimLodpalNisSldUh5LfS5MU46X0s+Haj5d20fnMY+5pClS3lIOmKc/sX6tDTBPS79ZBbZDazIS1FPn7W3qW1GCUc+qOl9mYWYI6A9LZgZzXQ4SlQWLCsO1LoBEFoBEbf64V+hJWEBgzJZdzmqMiczCmo7qwZTbXds5+/iFphBIK3s7/Y8KHVjLBmoTlY7itZCUPgNIUbLjbfKNS3dja7jMtF1dzoWlGmtGaoIr5bgnP2sE7qoFXM6mMU3bS6IpMgdSdlw0pC4szpVHNytaUNyOQ7mFEnxbvgb/3E7TwXB1z+r+GlrXoYQD0gOopntze4lWo1G4SJ+g7qs31SEf5/JZFlZX2lbsG6yPJ/xPf4MNNyUS3Rs7kmONxYGKgEpZWhgvdZQPHlLUfqIfECP3i1FZSL+Y4k/tGOON4lzvZ3eMQfMbjT6td0z2Py922rn/6NEL2vO3kaHDGsOPFer/OzQyBPyycOnTaBzLcE7HRdl3tSb9+WlE7T82aH6uYvM0Kj8mNIY+lUZ59+fn4GMybifxE5zi5aVPJTU7++G6D/vUFtVxWkGrnlWZ1Rei+HvfY9kbYMKwN7ALdP+C0B2jDl6Qbgwo7HHJC2FiNCoVwksgRjrb2E/OxGS7FCNeYqZEznnglnKBmGB6AZnoQnM5mRW5IUtRL8wcD1n6vZCA5lc/E8mFxU/lp7Yj+jdzScLnb07VFoYrUdLkT/h9TfWJwnAFfQFeDPibI05vibeuItAYcXmD3vowwSQyT+YIT8qpRmrswlwJRnGfw0IwHJFYvoTRa82IXp4grriVlDBKYRjwNG1C5sVsuLDklwDEEnl5NX/6qXrwkcHu5nk5Q83jDDV6ttrHux0Gg8PNC3B+AV6c4D34PfhvbAaDzc37YovOqAW+qEpzfEl8mrYEozMR2fnVRGcKc/4tSbQlLGtLmKRZZ7yytuAvcKjGTb2ASYXBc9gk1URAW7z2z6Et50PUn8atLxVGmv3+lkhhYaTFD8pQmGivibe3x2vaL8ClB/2NYacz3OgPNIQdjnBDAL8bfggGP/s7ilL+hvTetFNfodL63P7AxU2LREtshjPpkbwAx6lwl4oZVq2fb2TkiOKSRRyLnbj24zOkIsQSETURHFooCk6JGl7Sw4uCn2YVGnN4Wo1/w81pgwV/+YgZ/2ZeUrBqjd5gtpz79R9+vAxnzv0AC5VwAfioMjPFzHuzb/bSR+a+MkA/Oqepn3s4Y3CjFrpySm3RzXdHQm9lx100x/QVRO2kd1H2btL3apC6lEr34dFG4ue0LwKJz7TLQWg7aUDc3oSjtaHFjYzwTqiYkXT7lLqceDuShXVHosn63j6iBe1J0IL6lNgniLHUf6t31sImpGBoSXQaoT9/U60dV9y9xp6PWAvOjWVLbs88te6zu21F+5NuNJCPbs2Lg95L1AfeQmoq34dL0QD+TkdZP7vzle2zOl/ZP9H5asFDL+qBNVe+yCHnBK6y5Hzw/wOa5j3yYpp+s9gD54hShnNOd4FX4Hd1VOFn01X0WXS5z0PXEi+8mLy6TzrdeSKX+FmZzjmg00NVUzs+nVLcNaoyLgngVvzgVmIXJJuYA5zCAZdj4/EWJKnUSha+458cyad7lcXjin62E8mP8/hn+g2awl/s8DjojgY8RxGV1uJqBB3p9sSRHLPBnMn3C5jXTLxUr5rXyMSunCqe+jZpwUVTb8EHr/t8nzmvWfgz31rQKP2uvCqdejfX2IsG7aboEdAnnmRSyB6XtIl8rhWnziRLrn2DRcBfg4F0ci7FvFRLcFrTulQ7Htx1rlrMPxb0Q4/HA/qB9+yV4V5WZNce+dIjYxRXP+E174JYLrGzeKkb99qx86RDeTHAjfB5M4iYHvO5AtcvFfKHu4bOlfInhHtqByZYefw8Mo4BNvhxrrfKjtyeJgG0myHJMtBuRBkZuegIAXh0w0h8UdFI9vsKZrzfLC0YyWaFYk04bRTwoRGvcAg82SGpsWRwz7tcMyyNXa44OqfZoFcwL7QbxEof+zktPDD30uTkS9n7536/Gz197D3cdPC9Y9lx9HB2C/1GO/3sQu9B+o25e/PtB+eea8/1Q6wFbGyiItQVn+jYhbEf+PAiGE04KjlYuS17dHHcaAaAE5HhToTMzhzcwfAw3+ELrx8WY4TjCKZSi3p9SeEivABRdoGuX+YLAOQl3cBOfQom/kSfMGXifICYkXuHwVzD62/V2Mqep3tY7Hzdw+K5NbhpI1taSbz5F2wgtuCpPruVGCqcNxefq6sY87Ts3P6/jm/eNn2O8Z1cMF2fa4D0m/OOMjdGsGt4jHUXGGPqfGOsXzTG8H9vjEts4+cYavlS0/k5B3yO01007l+QcXdQx84zblz8WBqXYiyp0qrE7Y5hHncu5kUpzNwOeeZ28FItnCXks8QCnzCOre2ACMbo9FeyDedySmqFSFiqav7cPLvA7P4crOu54Iz/fDz89vlsgCLHxznCxwZqgNp9Pk5CgNcTlyrBU7UAC1csYaEUs5JsJq627YTDzgXm4a9za4xhJXP62f+Wkn06uPkcfPN+Fub5fEal8TPxEKIeok4rGMUGwIKUWYOSGmTXIJUGPYSuyt6UQEfRpYnszejKmux12WtRFF2NjiazN6Ijyewt2WO16MrstbJe383+mn0fvG0llaI2UGkblkZ1XhpleD7Xy60+QQA+npQxCcDqBnj14UVZd0pMCC+pWZuT8wQjuPBEwFu3KamsWjC9RHGC06MuSeXDrFyVKymAtuUFEQypyN6hII647Uje0Wqe36orG+0r3h09pDdZ647vOIS5f8l3R240+ITKN/Yf3bN5DT3b89JezP//2f3N7VgeY0M5Pne23ccbf7Ml++sZwuzm+hmBp85uQSWvPXFmlYKtbwZuz/XUJDDzH/xoFcYgpM8c2HEn5cddWT/ZaS5wvk5zJblOc2mry5NDc+ftNreATc/Td+7jBd9zoQ507FbZ3/zfpnPBp5yHTiQtciIXolRxWd5x5GgFv+Gkys9Pa/h8tFYs0Fr06bQu8Q3nI1n5CWdwYcKXOAAmR/8c0F9JtVDrPjkCsSwqNsQlDxit6hgpD1kYDl7LDVjnC8MTcJhYGGRbrkZcsqo/TW0+3TKdZ8Bzn2mJLjj+P3+G9aHl/nSgexbK/ckOdZ75DnXFn79D3UIu/fy96poXx/Dna1vHvDuPUxb6vHIgsb5FfV5nDEYSHRs0mRnGKbcz1sx3JOeAZNoYi4kcj0soSCdouS25cb4t+QVavu5E3Pl7vmZ/Lnd9zf4zOkq6vk5j2/29sx8o2tjXqF7q8hx1xZTcuQkgg6TEBbx9hKReQ0bslb+Zlnyjs1xVWiBkpnUF1eqw1AIhQkuUhAD4K2rr8HeVlvlT+Ks0JWUnvLYAlLAVV9Q2En/YWYG/eajAH5K/oWzRt5coFm04X1LwrVj8rRNW4XsdR57esubmddGqnlU9Vb667r5lKV/NumsHd3y1ycZyOkOweW1r48Y2b+PEronG6r7VfdVFrbv6eq7enFSgHU8eaqwZ2R5v2diTqmsMlsRK3L7y5tHGZRevinTW5fast6yq6hquDcX722K9LY1do/XFvW3hiok7Ns0imIukxxz57qAk1UbdfZ4uc3X462E/q9Vc+2e2mus4p9XcDGfx1zVhB3ehZnNSHQBcsekLN51bcAlfuP3cjvkmfF+sEZ3i5lzLvs/Fz8b/T/xsxPys++L8nK9J+8L8/PV8EdsX4ydzcb7kLc/P44Sfy6kHzsPP1OfhZ89n8rP3HH6+gPlZ3zbPUNEliA3nZWvqv8tW7GWj+Ct0EfGyX5i7Vf+y5hftvP5RJUsr6cdYTvMFmXzF7Kz+aYVaoaSfZlWLdPdWwusR6t0v3HESW9m6uNQOdncoKjXBhS7w3qsWsx5M78yIHKeNLBbE9DJXTB2e6ZJvdUVnlslHC/IZXSSfOkHkUlLXCER2Fn9lkwavSkhFMeFCqj/UDldaV6S+uJQuEPN9YWElLKE6n78pUVNQUYkazcGk39dYV1MQrqS/oNSeLWmLunwhX11VSWu0wFfqa4iQdUBZdkeI7Hqp9dTbX1x63VFxIi41AegaArFtWCw2vPWuHZBW+zkyG8Uyk/rhej/Ix7p4Nm1cJK0UlpbYbpIqsSvtFySLBu/MMElDE3KZzP+RZqOftafoC4ss+VmbkL6g5H716VuW5mX4cyLDPmrNeWfgKMZdTfL63afLc2awm2syhGcGcyu9Y0vnYb88xfp5aRjO2uWz9guYx/Gl00/sN4n+lDgszFgqm7o1nzEDRwfhSnvdf38Gnm8Z+QuL9NbCqtZAoLWqqEh+LWzIry1/QYevKGmucDormktKGiudzsrGknhbW37NmdhRpVGhp9qpYZiJIpVuxlJMxKXlMMvKYqTdn1gQJ4vy47G0xjovvZFAs9UQFlfEpREF7gaVn4YdIIsOXhqQJRMAmDoSwxEQ/tL3Yj5DplsHRb4yRBwQ0py1GReYBUySA7+uEtIFZaSMvtgkRapxSjuwHNdCwTHZ0iiIxbhUSjLN73JfEFCu7s9mn68783uXdCzFXwO/WG5NcBXle5guFpLOyAqDz+299m571Ss3DtywpU7Lza2rnrh6Rc/2ZSEtp3Y6+tbtrL3x7SrLmv3/q7dzD46quuP4fe4z+7jZZ7J5bTbJ5r3Ze5MseUMChIQkBBLAPARDERGCgBgEX4hCK0lFKyhi29FSFehUu3fJjNba6YBV207/cqa0U1un49ROM+NMy1inLUjo+Z1z95l9JNX2D2DvJsy9v98595zfOef3+3wfWoaaxLeluG1YXHn/iATNx5xgtlf07GzvPTgs0prOAyMBrvvJFyrESr0GNdmxe+99vO3g6/c6zAdem2pxlxfrCgF++uQ3102uzC9cuWtd03opp2bzkfXH+YquMdqweXqr1HjHCWDwzp/GDN5u6igV6oK2KpNklyophjfo8802k9evGRedNjfA8fmaMJsXjvxwIpppDidjttnh+FzgXWVen9jZhdcNzT5SatolQLn20ji+dLqTczYj4Lf2h5M5Y3fkiasrKgdzdSodn51XkV/f4vJ3lpeOnNrVlIb72zLIrU96TH5Y1X/8J9DvMUcXxb7A0cX17hGSrp8JE9wScbotKXC6rQpOd5a3uv2g1pAGqCv7YZRpXAJYN7pIWBJidyayQFgUbJflo+uC1L5p+N/6pgF841+Cb+hIwL8k39DqSLS/KOfQ12LqWsL+uYj9syLOP2JK/3Sm8E9XrH/qM/hHXKp/FkTuS3LTcGLUvjhn/Ts+WOcUfx3C/uqiNlHT6bnVsIc2JMmNKLjrQbPK5gTPAby6xYZxyXBmMoA+DkT9eRukAbWgUcrqroaTAFnnhfraL0u3zhSxLcmvY5mitUX5mdmSPkhjKBSI0VtwPZeBqlRyHGCvDkMqI4kOBpLoIFN6BU8an0ThiYwj7RMK7/9GL4bzKnXBFP2HhHtwKe/B6SNlPuEXF+7xYuR1tE9EashujJG7MLc+hRvh3AAr1ajkVMCeXiibjkmsMMQlVmix3iedrdyPTXwR8GZrYv8+NcG9Ftt5bwwphrK3PkN2XsccATvJr8A7n1aa5FeUkfyKPJJfEUUJgHiUMtFCfoU7kl/BJPQfeJzEPmZI6CbvTNRkQAvc0MPzJn6L22ns1j/Yv/MvIv/1ArtHhPevVY21sjFrjWw6BtCzBsywMw0KwzXK3uKKAFq86vnc0nIRxwSgjB2ianRx2s6OWtqLtYU7YDMek0s6YKs34MBl3gtlsQME7jLWuv/VXY17dtzmNj29/4KgzjradmKtTkBNMj47+B0Lb7xvxe51VS33yVO3f/+B1RNNE492j57YIrGm1tHDA6NPjNfSH2x7/bG1ec2jbT/+V9/pfI1Ol7W3uM7MmIysnbMa28SZAo1Gb9hR9/C59w89+ZdXRjofkvdufW5H4+pjP7u/fucGqW3PM6QvEwb3NOWgJOpkCuIvnFc4JblYNRes8+HkDeDf1CdQgFFjz0pkkSKZ4eQlRt42TAhuiBKC5VIJ4qp8CzkgV0DBch2gAYpqm1Ijg1Ot+ReihL0pF/XJIMPch0mX7mjuw+xhRQfOTw3H0IfLI3MfRhCLyRDEaRIe5HKY3GoWUV8dHZ8yc4m/HRm9MhKK2U0kAkpnY/WXtLEabCxfhI3RwGYR7GVHZPjMaCTTGYlkwnZeVHI6Yu2siLezKZmdaRI75IrF2rkgQMls7vbEUTuz0b0J24cR26cT8zpiKNrhvA5VsrwOw+LyOgxLyuvI4KoU73pmj+1K+e5ndt2hFHt4xH+HsP+aY/M5Yj0Y8AV7ST7H8mg+B3FdRXw+xyr0cVXUaRnyOdI7KlOsltlhuzMFaJn99qMMO2jQB/dRH3N+DjTuLShWq6VAz0CdNRcGPbh9siNrDp/mc1eDVlHOskGIAdOJwrigY8+Cy4S4q33s5ZuXY/l5sZ+ZE2vXzr9ZvsycU2KxenJMAZaOuSDvxyXOwHXgeqlGaqOSH+ILbzSUw0FlANcI54uy24ArVqBkR0CtB2eW9W5AnfF2p7GglIyC5T6SFuIs0JQ0xu0fBBQsnqL0oSYoPDo2J8ROGpiM+KOnlo3orRbp6bbl0ISv3DNk8Aje6dXdW+tEhqs93D82vcX31Mj02PTtvg2kqcTa+03Gy6uuHIb2Wr9PML+16leP7brQwrxRVbvi4Pl5d/fyqVd3/HwKxwGYF43GfwflhhP/eGK0k1H46BgbXZwCG+1RsNEhixMSGBLQ0VBOmZ8aIB2d4JKgpN+NzmjJoNLcufA6PoMdeV+FHXkC4XcntyM6iSVDYq+IzlrJDGFPxqy5w7aAhmj5Qlty4mypSGFLZdQWVxJbctLasmCiSmLSyQUzU1LDnoufjVjFtkPItkqqDXh7SRnlQa8v2CzJ+WiAqBOxpGjUSqCUF9twnhakzjTYMEEoxnbQGsWkKYsKzTogirIolHmmoTSJE57NOHYmdcqNjOMlQxjVqD9DFSdaa7qYKC0do6rD1ZsKqjroEoKO1MBqNtI7U6OrhUgfTQ6x5o5EO6mib8F/gFnuir4biNoSonUBlrbAKivkZcsGfTeLKEJqh0vRd4PXzZUd0XcrsMfou1kS9d0SRS0mVob2pRC0UDffPDh6d1jbbbB/XhOvZ8Eqvj2EV7et1EAsAxwS1ZtIkaKPFCk644oU65UiRbeiQlwlyBo7PH4mZDiToXelbpefZupkKZrr0wy9DHSuP9PcjfpYEVVPPaEojtkkuYydC1pEgnU0hivU6ti5WVN2HmxbmaA8iDDg3FbsGUDA2KtEEdZ6wMA0YrivERiYWSL6IGircE6lDmpZebw/lQ2YCAfoxYQodxUMUcZsZZeKZLAyjph6HLeA96iSyDmPvfznma3nZ/aUsSPhkpwvzpftmTm/dfqTl8d2989cmTp4ebqvb/rywakrM/1KwqR//NgwvTFcqrdp+NhY3c4rtPnC2WvnR0bOXzv7/LWLo6MXr5HYWfUIp6dEajXq56epUC14CcXKy9RQY0KwugZJ7kSX/eJst70WXNQN26AbsIsk5BKJnD3A7ki3CBskayDTyTyH4ZdtaD0s1wIZyo46E3JFcE12yOAqbyL5TUWg5yTbl6GomiryVEk4maQbJIOCnUqPU0ILRSko+UEQnSx65MNbfiMt+87deer9KuuaOx7o7f/615bpTTdv948dGVh15+pKfZbG5ewbv6tx+r3aql88v/2lfS3bKzce2Tj8yHBlJfoLfaxkVcydFWt3tvdODYskCvnuzMrJgcqYg5/wtt7zz518KUkUaQmf+7Ak7051k7Ki+a+ZGorPvIMQsVGSc9EbWk1ovLarcqENk6ItOBMPJ5BBzO23kT35xSbnpc8+TJ6xt4ga4mR5fNzQInKf3dxrTAPeC6yJaqoKCodEwEQkBQWXHVFX1TaFK6xi5m934mQdv/UH9/Jyv2MCaI3oovqooMUHtbg6FJc7fTgFwSCCTgPc0EUWfS6c2hlm9oFkp8EF77YFOqsTk7nt8WTu+IVc6i2apNsxNLWDaWS6GOgdFKwGdtB/ZBqHhoif/tufnWGq2beZKaIhSxYi8CdGQxb+yxm2lKnu6SG/z7+f+ff5OuX3j3PNdAP/OerHzVQw2zfLZlE6jmziooFBb5oL6XGBoh64MZR51mSlJORN2NnVk0NjigBsYVtRDaKAZH+xlj4+0J6nUXmlEt603G7lfjN4qs2i0qhV9XcFWjs0WqPK5e0nNu7namk3/1f0DG34GbKiz8BflU2muaDJPKvFNw5qfSEtrivTAr4OHsMEextZ5DECQDwhm56E3uwt208eocNhHejIU3PrNCppZ6ClQ6MxqnO9fd7B060WFTzD/HXaTc1+6WdwZH6GTxY+QrYK5jrUFkwPbosKtBZFTxH0SkqDBJ2RUsFUbRLUk1zZIvTzIpwWUORCP7eZZ0usVL2CjFLaTLaZUPdnIZemSAh6U7ZhaeaGpa39HXBZDwamamdvisZnoO2Zetz2FdTusM3E+UE3sTm9/+EICud1I7NzS+DbXBuwzXMLtMRkpW0gC88LeQ0gYJOir5SGv/SmbDzagi49PG1uR9ft+Sk6lCZpL8P2zl9n6nE/+//a6/iK7E3aebXJezToeZTSy9hH2G/hmsugETPz1ISZp4bXy4IHbK0Nf0n+wSJLdX6oAIqZ2ehS34bJh/Zu8Pk27G1v27PBx2xr3wvMzns62ibh20myhzN56xpvp16nBMpDNQAvEO+CuSUJnwjJjgpRJF/xsJXTGFt8iyYoOQ+2dAgdqxbNzAHC4ozn+ZSmvZw05hTbojs79OemnGKrpSTHbM7xWNH1PzHnJ3K9Lo7hU57mioyVL1In6Hcx99dNhd1nslFGDmf3QP0w6L+hKDU58DeR7psC50vuNYvu9SFm0MG9bGECnYBvh8c9gSj/paLPLQDNXUoDj6OpolvXuGn+DbTaOUaFeqCRmrVzIROE9oUotKfoHpOhKuiTZIqbC9aLs1oN/qJCAiI05tesw2+PbgCF+dWWObmkAbV2Nc6/qfbDS1JdBmDWagxmhXdJI8qDeIXajIbDFSvRUrwQ9EmtTqUcGY7NAp4GiYStSmINplKoieqBymbFwrjoIwZvcdGzam/R92iGO3fBPH7yrf2de7cOlRVxOq3G7hFXjbWMv3Bfn4nZaRJuhliaZgSzad5i6D1wdrxjW29Daa5Wpy0r3bTzwTX3vT29ych0t1rL7aK/9Ru/fXbQUdNVXcKrbYVlhbblD795uFCfXSfZvbbCLOHI5aMrnGXVZTk6j68/kD949qOn8JjTy47zpShGU6N34gCJ0mStTSJ+ZMUwixnAihqHiBZDVAHkJaEgVnVV5o1odYXRjDyLnKfC3lSB83hS9OwxYgVROGJzkFALKpucHkAl5pNCmgYC28SEY4fF0aioy3mEAOqanmIv6xB66Y9/vYY+3azTqT/S89rf81pdy3L+TxohS9B8ouL3tLbe/BsjoD/9nGZ+psBspKc03M1L9Hs18w+aaYF+vGq+GfoQDAI32BtoJPDGaCcqMkIQisJAQ/5R4iG/4Bbgv8DBMta3Zh/lf4n+3aqsNh2SInFti0pcqxLlra0ihJtwpuwwzIUVFSiidC07UdgZ0giYLSBrQGRP35Sgfu0B9WtVPu1WmKQgfx3YdWaiuMfJ0QZ9dfG5ILNx27yJqF9v3nLm7qYsnV+nfvUHw1+Uss+E1a/J81/i36GKQY28kMLLkZABWlxAMbJghmefzc0v1JDa/VxsExYNLMTGgPhtjhgqKMRigXmgCWGWzTCsGObwsGguQMboNValDCxsBEhIoecm28OxIt4NO85u86ztbrP1TgQe8PcfHqqmvfMfEju6Rl/Yv5xXcdf7+H2Mpm7s6GBXRMj7P61y/VcAAHjaY2BkYGBgZOo//7DZK57f5iuDPAcDCFz2z/KA0f/P/mvhyGTXAHI5GJhAogBrnAx3AAB42mNgZGBg1/gXzcDA8eL/2f/PODIZgCIo4CUAogoHhnjabZNfSJNRGMaf7/z5VjD6A6bQjctWClFgEV1LiVR2FTHnMCjXruY/hCCCRdCwUApyYEWyZDUsKKUspJuI6MYKuggGIl5Eky4WXgQjarGe92uLJX7w4znnPd855z3vc44q4AhqPmcUUCkU1CrmTQZd5K7bhLC9ij7nLeZVDE9IVB9AgmODTgpDahoxalwtln8xdpyUyJUKbeQWGSVJcpHMOitICWzfJ49MxnFUEU3uTQzYZmy2AeTsPVxy65AzL8k4+yX2/cipKH7rKURsB4qmATlfO3ISd88wp1coilo/x/YhbB4jaJexIGv68thq3nlst1twnud4ppbKP6j9zOGj3s2zh9Clv7B/GrM6g25q2NSjW42j0WzECXMSWeZ9x/lc/qBXvXO8cXuQlTgJmw4q5+i9yOpBRNQiDjI+pvPcM48GPYOgFp1EJ/dtUzHHT41z/xtSf6k92xnSXtGQ/GMUrjO3FneY/Rn06QTSHJuWOV4shDodRI94oh6gl0QZ+yR72004pAJ4yP4I47dVifklMGef4prHC5xi7fd4dV8HX2/5m3jh+VADffCR12Qb8bud2F/1YS3Ma9LzRbyoQbwQz8wU3kvd18MdoIoX9f/D2u8kaWelXCDfzVFE/vmwFtal0h6rRbwQz0Q3fGWuy/yHObFWO0izTgG+FqCq6izfyAJp/Qvy1H7qOY7xHVTh2hO8FxN8F0l5I5V3kiSiQ7zvu+xlxGWuuoA0mZN1mWfAPscx/ZPtw7xzI2j8AyV25OAAAAB42mNgYNCBwxaGI4wnmBYxZ7AosXix1LEcYTVhLWPdw3qLjYdNi62L7RK7F/snDgeOT5wpnFO4EriucCtwt3Gv4D7F/YanhDeFdwWfHF8T3yl+Nn4b/kP8vwQkBBIEtgncETQSLBC8ICQl1Cf0RbhOeJ3wJxEVkVuiKqIpon2i+0RviXGJOYlFiTWIC4kXiV+QMJFYI/FPSkEqTWqNNJt0hHSJ9CsZM5lJMj9k42SXySXInZOXkQ9SkFBIUJilcETxjuIPZQnlIiA8ppKk8k41Q/WWGoPaGXU59ScaBRrHNN5pvNPcoHlOS0urQuuBdpJ2l/YzHS2dJJ0zuny6Cbp79CL0hfR/GNQYnDNUMKwxYjOaZKxkPMvEzWSCyR1TA9N1pjfMWMwczBaYc5n3mf+zKLB4YznByswqwuqRtZl1j/UbmxKbI7YitpvsouyZ7Hc4THOscIpxNnG+4ZLm8s21z83LrcZtndsH9wD3Rx4lHs88ozxveFV4S3lneD/z8fLZ4Cvnu8mPyS/B74l/WYBBwJaAV4FWOKBHYFhgSmBN4JTAa0ESQVFBV4J9go8E/wnJAcJFIbdCboW2hf4JkwmrCXsEAOI0m6EAAQAAAOkAZQAFAAAAAAACAAEAAgAWAAABAAGCAAAAAHja1VbNbuNkFL1OO5BJSwUIzYLFyKpYtFJJU9RBqKwQaMRI/GkG0SWT2E5iNYkzsd1MEQsegSUPwBKxYsWCNT9bNrwDj8CCc8+9jpOmw0yRWKAo9vX33d/znXttEbkV7MiGBJs3RYJtEZcDeQVPJjdkJwhd3pD7QdvlTXkt+MrlG/J+8K3Lz8H2T5efl4eNymdTOo2HLt+U242vXW7d+LHxvctb0mkOXd6WuPmNyy8EXzb/cnlHjluPXX5Rmq3vXH5JWq0fXP5ZbrV+cvkX6bR+d/lX2dnadPk32d562eQ/NuTVrdvyrmQylQuZSSoDGUohoexJJPu4vyEdOcI/lB40QuxdyCfQH0lXJhJj5QMp5QxPuXyBp/dwTSXBjt4jrMxxL+A1lPtYz/GfyTk1QrkLTxPG+wgexlgNZRceu1jLILXpX/0k0MvdqmRk9RPSs1o9kHvQDOVjVKK6y75XPRxg5TNa51jPqHuESEcezWKblaGheQ8QVWuePQWBy/WfPMHnyRK2V+2Hl6JelbFZv42nUyJbUEd3I/hQqy6kwpHS2otFrNeXYtXxU2iFeFJc1VpRHtPTGdYy6f8LBrSvbfG03fVsc3o2bqWLLJUJfWKgDOmTYSmyUB7HREwRmDirUiJX86mE9tixu9wFp8REo86BZI+5mpdVv7Nn6I+9FcaHjGnVaC8s57G7yNLQ1PqH6FLl7T1ypmD9CW0No4iZKg7KJKtd87WzMGRyaFrvTSEV7JQCfroLi4is6zNmxL0JKlT9GRk5Y49b5BNmWdDvEHsaN3b+KZtCeYS1lHG0QmOa1jv1XDX6LifH0Hu5XOBr9ffgN/Z5lMhjRutBq6BVHTMmRlNWe7FSaebTTv1pnRXjNa/8H2NbPw4WXZXiJLVuPYVPnT0RtXLuRu5fscqI8IxYZaz5gDtdX4sW/W64nzP/FLWN6HeVoyUsp8wjcgaqN63pnPuV3oidb3Ogz/hj1lh3RMqYoU+NMXO7YG9Zvyb0MVhwRmt9xxk3dA5V81vrGHsuFZo57RNOkfVeHSFexj2dNWfO34TVx86HOlLfp5qtdH3CVzNhTiSe3N9VJx94hGSBqLJmwPeUsTfGimUyYVeExG7EbOeOjfVGiUpmS3maHK8wIif3U0yLGSPZG6yaGAWZN2K0asqun12+crp1zV3mlvCUqs40L3M/T/V24KxOnUv1yRXMyezsqSTCJSupmFudRu5aXbDSuFOscKU62YydM6GFdceQlUwxIQ7xm/PX9kldvx3anDZjaFxX//LszbG2PH0/X5u+h//xt8/etWvY/199Ma1XmMNOsZyy89u0GOGecWYeItpdeN+/gg/PZllVWn+96LdPj71puduX0alX/qFP/lCO8e/geiJ35C1cj3GtzvhNoqOTRedvQXaX7IN8CZUH/uaybh/9DeeiFNJ42m3QV0xTcRTH8e+B0kLZe+Peq/eWMtwt5br3wK0o0FYRsFgVFxrBrdGY+KZxvahxz2jUBzXuFUfUB5/d8UF91cL9++Z5+eT3/+ecnBwiaK8/FZTzv/oEEiGRYiESC1FYsRFNDHZiiSOeBBJJIpkUUkkjnQwyySKbHHLJI58COtCRTnSmC13pRnd60JNe9KYPfelHfwbgQEPHSSEuiiimhFIGMojBDGEowxiOGw9leMM7GoxgJKMYzRjGMo7xTGAik5jMFKYyjelUMIOZzGI2c5jLPOazgEqJ4igttHKD/XxkM7vZwQGOc0ysbOc9m9gnNolml8Swldt8EDsHOcEvfvKbI5ziAfc4zUIWsYcqHlHNfR7yjMc84Wn4TjW85DkvOIOPH+zlDa94jZ8vfGMbiwmwhKXUUsch6llGA0EaCbGcFazkM6tYTRNrWMdarnKYZtazgY185TvXOMs5rvOWdxIrcRIvCZIoSZIsKZIqaZIuGZIpWZznApe5wh0ucom7bOGkZHOTW5IjueyUPMmXAquvtqnBr9lCdQGHw+E1o9OMbofSa+rRlerf41KWtqmH+5WaUlc6lYVKl7JIWawsUf6b5zbV1FxNs9cEfKFgdVVlo9980g1Tl2EpDwXr24PLKGvT8Jh7hNX/AtbOnHEAeNpFzqsOwkAQBdDdlr7pu6SKpOjVCIKlNTUETJuQ4JEILBgkWBzfMEsQhA/iN8qUbhc3507mZl60OQO9kBLMZcUpvda80Fk1gaAuIVnhcKrHoLNNRUDNclDZAqwsfxOV+kRhP5tZ/rC4gIEwdwI6wlgLaAh9LjBAaB8Buyv0+kIHl/ZNYIhw0g4UXPFDiKn7VBhXiwMyQIZbSR8ZTCW9tt+nMyKTqE3cY/NPYjyJ7pIJMt5LjpBJ2rOGhH0Bs3VX7QAAAAABVym5yAAA) format('woff');font-weight:400;font-style:normal}.joint-link.joint-theme-material .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-material .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-material .connection{stroke-linejoin:round}.joint-link.joint-theme-material .link-tools .tool-remove circle{fill:#C64242}.joint-link.joint-theme-material .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-material .marker-vertex{fill:#d0d8e8}.joint-link.joint-theme-material .marker-vertex:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-arrowhead{fill:#d0d8e8}.joint-link.joint-theme-material .marker-arrowhead:hover{fill:#5fa9ee;stroke:none}.joint-link.joint-theme-material .marker-vertex-remove-area{fill:#5fa9ee}.joint-link.joint-theme-material .marker-vertex-remove{fill:#fff}.joint-link.joint-theme-modern .connection-wrap{stroke:#000;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;opacity:0;cursor:move}.joint-link.joint-theme-modern .connection-wrap:hover{opacity:.4;stroke-opacity:.4}.joint-link.joint-theme-modern .connection{stroke-linejoin:round}.joint-link.joint-theme-modern .link-tools .tool-remove circle{fill:red}.joint-link.joint-theme-modern .link-tools .tool-remove path{fill:#FFF}.joint-link.joint-theme-modern .marker-vertex{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-vertex:hover{fill:#34495E;stroke:none}.joint-link.joint-theme-modern .marker-arrowhead{fill:#1ABC9C}.joint-link.joint-theme-modern .marker-arrowhead:hover{fill:#F39C12;stroke:none}.joint-link.joint-theme-modern .marker-vertex-remove{fill:#fff} \ No newline at end of file diff --git a/dist/joint.min.js b/dist/joint.min.js index 7a065fb38..ce3ae931d 100644 --- a/dist/joint.min.js +++ b/dist/joint.min.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -44,23 +44,26 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. }(this, function(root, Backbone, _, $) { !function(){function a(a){this.message=a}var b="undefined"!=typeof exports?exports:this,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a.prototype=new Error,a.prototype.name="InvalidCharacterError",b.btoa||(b.btoa=function(b){for(var d,e,f=String(b),g=0,h=c,i="";f.charAt(0|g)||(h="=",g%1);i+=h.charAt(63&d>>8-g%1*8)){if(e=f.charCodeAt(g+=.75),e>255)throw new a("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");d=d<<8|e}return i}),b.atob||(b.atob=function(b){var d=String(b).replace(/=+$/,"");if(d.length%4==1)throw new a("'atob' failed: The string to be decoded is not correctly encoded.");for(var e,f,g=0,h=0,i="";f=d.charAt(h++);~f&&(e=g%4?64*e+f:f,g++%4)?i+=String.fromCharCode(255&e>>(-2*g&6)):0)f=c.indexOf(f);return i})}(),function(){function a(a,b){return this.slice(a,b)}function b(a,b){arguments.length<2&&(b=0);for(var c=0,d=a.length;c>>0;if(0===e)return!1;for(var f=0|b,g=Math.max(f>=0?f:e-Math.abs(f),0);g>>0;if("function"!=typeof a)throw new TypeError("predicate must be a function");for(var d=arguments[1],e=0;e0?1:-1)*Math.floor(Math.abs(b)):b},d=Math.pow(2,53)-1,e=function(a){var b=c(a);return Math.min(Math.max(b,0),d)};return function(a){var c=this,d=Object(a);if(null==a)throw new TypeError("Array.from requires an array-like object - not null or undefined");var f,g=arguments.length>1?arguments[1]:void 0;if("undefined"!=typeof g){if(!b(g))throw new TypeError("Array.from: when provided, the second argument must be a function");arguments.length>2&&(f=arguments[2])}for(var h,i=e(d.length),j=b(c)?Object(new c(i)):new Array(i),k=0;k>>0;if("function"!=typeof a)throw new TypeError("predicate must be a function");for(var d=arguments[1],e=0;ethis.length)&&this.indexOf(a,b)!==-1}),String.prototype.startsWith||(String.prototype.startsWith=function(a,b){return this.substr(b||0,a.length)===a}),Number.isFinite=Number.isFinite||function(a){return"number"==typeof a&&isFinite(a)},Number.isNaN=Number.isNaN||function(a){return a!==a}; -var g=function(){var a={},b=Math,c=b.abs,d=b.cos,e=b.sin,f=b.sqrt,g=b.min,h=b.max,i=b.atan2,j=b.round,k=b.floor,l=b.PI,m=b.random,n=b.pow;a.bezier={curveThroughPoints:function(a){for(var b=this.getCurveControlPoints(a),c=["M",a[0].x,a[0].y],d=0;dj.x+h/2,n=fj.x?g-e:g+e,d=h*h/(f-k)-h*h*(g-l)*(c-l)/(i*i*(f-k))+k):(d=g>j.y?f+e:f-e,c=i*i/(g-l)-i*i*(f-k)*(d-k)/(h*h*(g-l))+l),a.point(d,c).theta(b)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a),b&&a.rotate(q(this.x,this.y),b);var c,d=a.x-this.x,e=a.y-this.y;if(0===d)return c=this.bbox().pointNearestToPoint(a),b?c.rotate(q(this.x,this.y),-b):c;var g=e/d,h=g*g,i=this.a*this.a,j=this.b*this.b,k=f(1/(1/i+h/j));k=d<0?-k:k;var l=g*k;return c=q(this.x+k,this.y+l),b?c.rotate(q(this.x,this.y),-b):c},toString:function(){return q(this.x,this.y).toString()+" "+this.a+" "+this.b}};var p=a.Line=function(a,b){return this instanceof p?a instanceof p?p(a.start,a.end):(this.start=q(a),void(this.end=q(b))):new p(a,b)};a.Line.prototype={bearing:function(){var a=w(this.start.y),b=w(this.end.y),c=this.start.x,f=this.end.x,g=w(f-c),h=e(g)*d(b),j=d(a)*e(b)-e(a)*d(b)*d(g),k=v(i(h,j)),l=["NE","E","SE","S","SW","W","NW","N"],m=k-22.5;return m<0&&(m+=360),m=parseInt(m/45),l[m]},clone:function(){return p(this.start,this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.end.x===a.end.x&&this.end.y===a.end.y},intersect:function(a){if(a instanceof p){var b=q(this.end.x-this.start.x,this.end.y-this.start.y),c=q(a.end.x-a.start.x,a.end.y-a.start.y),d=b.x*c.y-b.y*c.x,e=q(a.start.x-this.start.x,a.start.y-this.start.y),f=e.x*c.y-e.y*c.x,g=e.x*b.y-e.y*b.x;if(0===d||f*d<0||g*d<0)return null;if(d>0){if(f>d||g>d)return null}else if(f0?l:null}return null},length:function(){return f(this.squaredLength())},midpoint:function(){return q((this.start.x+this.end.x)/2,(this.start.y+this.end.y)/2)},pointAt:function(a){var b=(1-a)*this.start.x+a*this.end.x,c=(1-a)*this.start.y+a*this.end.y;return q(b,c)},pointOffset:function(a){return((this.end.x-this.start.x)*(a.y-this.start.y)-(this.end.y-this.start.y)*(a.x-this.start.x))/2},vector:function(){return q(this.end.x-this.start.x,this.end.y-this.start.y)},closestPoint:function(a){return this.pointAt(this.closestPointNormalizedLength(a))},closestPointNormalizedLength:function(a){var b=this.vector().dot(p(this.start,a).vector());return Math.min(1,Math.max(0,b/this.squaredLength()))},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},toString:function(){return this.start.toString()+" "+this.end.toString()}},a.Line.prototype.intersection=a.Line.prototype.intersect;var q=a.Point=function(a,b){if(!(this instanceof q))return new q(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseInt(c[0],10),b=parseInt(c[1],10)}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};a.Point.fromPolar=function(a,b,f){f=f&&q(f)||q(0,0);var g=c(a*d(b)),h=c(a*e(b)),i=t(v(b));return i<90?h=-h:i<180?(g=-g,h=-h):i<270&&(g=-g),q(f.x+g,f.y+h)},a.Point.random=function(a,b,c,d){return q(k(m()*(b-a+1)+a),k(m()*(d-c+1)+c))},a.Point.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=g(h(this.x,a.x),a.x+a.width),this.y=g(h(this.y,a.y),a.y+a.height),this)},bearing:function(a){return p(this,a).bearing()},changeInAngle:function(a,b,c){return q(this).offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return q(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),q(this.x-(a||0),this.y-(b||0))},distance:function(a){return p(this,a).length()},squaredDistance:function(a){return p(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return f(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return c(a.x-this.x)+c(a.y-this.y)},move:function(a,b){var c=w(q(a).theta(this));return this.offset(d(c)*b,-e(c)*b)},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return q(a).move(this,this.distance(a))},rotate:function(a,b){b=(b+360)%360,this.toPolar(a),this.y+=w(b);var c=q.fromPolar(this.x,this.y,a);return this.x=c.x,this.y=c.y,this},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this},scale:function(a,b,c){return c=c&&q(c)||q(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=u(this.x,a),this.y=u(this.y,b||a),this},theta:function(a){a=q(a);var b=-(a.y-this.y),c=a.x-this.x,d=i(b,c);return d<0&&(d=2*l+d),180*d/l},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=q(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&q(a)||q(0,0);var b=this.x,c=this.y;return this.x=f((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=w(a.theta(q(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN}};var r=a.Rect=function(a,b,c,d){return this instanceof r?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new r(a,b,c,d)};a.Rect.fromEllipse=function(a){return a=o(a),r(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},a.Rect.prototype={bbox:function(a){var b=w(a||0),f=c(e(b)),g=c(d(b)),h=this.width*g+this.height*f,i=this.width*f+this.height*g;return r(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return q(this.x,this.y+this.height)},bottomLine:function(){return p(this.bottomLeft(),this.corner())},bottomMiddle:function(){return q(this.x+this.width/2,this.y+this.height)},center:function(){return q(this.x+this.width/2,this.y+this.height/2)},clone:function(){return r(this)},containsPoint:function(a){return a=q(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=r(this).normalize(),c=r(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return q(this.x+this.width,this.y+this.height)},equals:function(a){var b=r(this).normalize(),c=r(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=Math.max(b.x,d.x),g=Math.max(b.y,d.y);return r(f,g,Math.min(c.x,e.x)-f,Math.min(c.y,e.y)-g)},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a);var c,d=q(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[p(this.origin(),this.topRight()),p(this.topRight(),this.corner()),p(this.corner(),this.bottomLeft()),p(this.bottomLeft(),this.origin())],f=p(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return p(this.origin(),this.bottomLeft())},leftMiddle:function(){return q(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return q.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return q(this.x,this.y)},pointNearestToPoint:function(a){if(a=q(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return q(this.x+this.width,a.y);case"left":return q(this.x,a.y);case"bottom":return q(a.x,this.y+this.height);case"top":return q(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return p(this.topRight(),this.corner())},rightMiddle:function(){return q(this.x+this.width,this.y+this.height/2)},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this.width=j(this.width*b)/b,this.height=j(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(b,c){b=a.Rect(b),c||(c=b.center());var d,e,f,g,h,i,j,k,l=c.x,m=c.y;d=e=f=g=h=i=j=k=1/0;var n=b.origin();n.xl&&(e=(this.x+this.width-l)/(o.x-l)),o.y>m&&(i=(this.y+this.height-m)/(o.y-m));var p=b.topRight();p.x>l&&(f=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:Math.min(d,e,f,g),sy:Math.min(h,i,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return Math.min(c.sx,c.sy)},sideNearestToPoint:function(a){a=q(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cc.x&&(c=d[a]);var e=[];for(b=d.length,a=0;a2){var h=e[e.length-1];e.unshift(h)}for(var i,j,k,l,m,n,o={},p=[];0!==e.length;)if(i=e.pop(),j=i[0],!o.hasOwnProperty(i[0]+"@@"+i[1]))for(var q=!1;!q;)if(p.length<2)p.push(i),q=!0;else{k=p.pop(),l=k[0],m=p.pop(),n=m[0];var r=n.cross(l,j);if(r<0)p.push(m),p.push(k),p.push(i),q=!0;else if(0===r){var t=1e-10,u=l.angleBetween(n,j);Math.abs(u-180)2&&p.pop();var v,w=-1;for(b=p.length,a=0;a0){var z=p.slice(w),A=p.slice(0,w);y=z.concat(A)}else y=p;var B=[];for(b=y.length,a=0;a1){var g,h,i=[];for(g=0,h=f.childNodes.length;gu&&(u=z),c=d("tspan",y.attrs),b.includeAnnotationIndices&&c.attr("annotations",y.annotations),y.attrs.class&&c.addClass(y.attrs.class),e&&x===w&&p!==s&&(y.t+=e),c.node.textContent=y.t}else e&&x===w&&p!==s&&(y+=e),c=document.createTextNode(y||" ");r.append(c)}"auto"===b.lineHeight&&u&&0!==p&&r.attr("dy",1.2*u+"px")}else e&&p!==s&&(t+=e),r.node.textContent=t;0===p&&(o=u)}else r.addClass("v-empty-line"),r.node.style.fillOpacity=0,r.node.style.strokeOpacity=0,r.node.textContent="-";d(g).append(r),l+=t.length+1}var A=this.attr("y");return null===A&&this.attr("y",o||"0.8em"),this},d.prototype.removeAttr=function(a){var b=d.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},d.prototype.attr=function(a,b){if(d.isUndefined(a)){for(var c=this.node.attributes,e={},f=0;f'+(a||"")+"",f=d.parseXML(e,{async:!1});return f.documentElement},d.idCounter=0,d.uniqueId=function(){return"v-"+ ++d.idCounter},d.toNode=function(a){return d.isV(a)?a.node:a.nodeName&&a||a[0]},d.ensureId=function(a){return a=d.toNode(a),a.id||(a.id=d.uniqueId())},d.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},d.isUndefined=function(a){return"undefined"==typeof a},d.isString=function(a){return"string"==typeof a},d.isObject=function(a){return a&&"object"==typeof a},d.isArray=Array.isArray,d.parseXML=function(a,b){b=b||{};var c;try{var e=new DOMParser;d.isUndefined(b.async)||(e.async=b.async),c=e.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},d.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var c=a.split(":");return{ns:b[c[0]],local:c[1]}}return{ns:null,local:a}},d.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,d.transformSeparatorRegex=/[ ,]+/,d.transformationListRegex=/^(\w+)\((.*)\)/,d.transformStringToMatrix=function(a){var b=d.createSVGMatrix(),c=a&&a.match(d.transformRegex);if(!c)return b;for(var e=0,f=c.length;e=0){var g=d.transformStringToMatrix(a),h=d.decomposeMatrix(g);b=[h.translateX,h.translateY],e=[h.scaleX,h.scaleY],c=[h.rotation];var i=[];0===b[0]&&0===b[0]||i.push("translate("+b+")"),1===e[0]&&1===e[1]||i.push("scale("+e+")"),0!==c[0]&&i.push("rotate("+c+")"),a=i.join(" ")}else{var j=a.match(/translate\((.*?)\)/);j&&(b=j[1].split(f));var k=a.match(/rotate\((.*?)\)/);k&&(c=k[1].split(f));var l=a.match(/scale\((.*?)\)/);l&&(e=l[1].split(f))}}var m=e&&e[0]?parseFloat(e[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:m,sy:e&&e[1]?parseFloat(e[1]):m}}},d.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},d.decomposeMatrix=function(a){var b=d.deltaTransformPoint(a,{x:0,y:1}),c=d.deltaTransformPoint(a,{x:1,y:0}),e=180/Math.PI*Math.atan2(b.y,b.x)-90,f=180/Math.PI*Math.atan2(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:Math.sqrt(a.a*a.a+a.b*a.b),scaleY:Math.sqrt(a.c*a.c+a.d*a.d),skewX:e,skewY:f,rotation:e}},d.matrixToScale=function(a){var b,c,e,f;return a?(b=d.isUndefined(a.a)?1:a.a,f=d.isUndefined(a.d)?1:a.d,c=a.b,e=a.c):b=f=1,{sx:c?Math.sqrt(b*b+c*c):b,sy:e?Math.sqrt(e*e+f*f):f}},d.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=d.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(Math.atan2(b.y,b.x))-90)}},d.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},d.isV=function(a){return a instanceof d},d.isVElement=d.isV;var e=d("svg").node;return d.createSVGMatrix=function(a){var b=e.createSVGMatrix();for(var c in a)b[c]=a[c];return b},d.createSVGTransform=function(a){return d.isUndefined(a)?e.createSVGTransform():(a instanceof SVGMatrix||(a=d.createSVGMatrix(a)),e.createSVGTransformFromMatrix(a))},d.createSVGPoint=function(a,b){var c=e.createSVGPoint();return c.x=a,c.y=b,c},d.transformRect=function(a,b){var c=e.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var f=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var h=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var i=c.matrixTransform(b),j=Math.min(d.x,f.x,h.x,i.x),k=Math.max(d.x,f.x,h.x,i.x),l=Math.min(d.y,f.y,h.y,i.y),m=Math.max(d.y,f.y,h.y,i.y);return g.Rect(j,l,k-j,m-l)},d.transformPoint=function(a,b){return g.Point(d.createSVGPoint(a.x,a.y).matrixTransform(b))},d.styleToObject=function(a){for(var b={},c=a.split(";"),d=0;d=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L"+f*n+","+f*o+"A"+f+","+f+" 0 "+k+",0 "+f*l+","+f*m+"Z":"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L0,0Z"},d.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?d.isObject(a[c])&&d.isObject(b[c])?a[c]=d.mergeAttrs(a[c],b[c]):d.isObject(a[c])?a[c]=d.mergeAttrs(a[c],d.styleToObject(b[c])):d.isObject(b[c])?a[c]=d.mergeAttrs(d.styleToObject(a[c]),b[c]):a[c]=d.mergeAttrs(d.styleToObject(a[c]),d.styleToObject(b[c])):a[c]=b[c];return a},d.annotateString=function(a,b,c){b=b||[],c=c||{};for(var e,f,g,h=c.offset||0,i=[],j=[],k=0;k=n&&k=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},d.convertLineToPathData=function(a){a=d(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},d.convertPolygonToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b)+" Z":null},d.convertPolylineToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b):null},d.svgPointsToPath=function(a){var b;for(b=0;b0){var f=joint.util.getByPath(a,d,c);f&&delete f[e]}else delete a[e];return a},flattenObject:function(a,b,c){b=b||"/";var d={};for(var e in a)if(a.hasOwnProperty(e)){var f="object"==typeof a[e];if(f&&c&&c(a[e])&&(f=!1),f){var g=this.flattenObject(a[e],b,c);for(var h in g)g.hasOwnProperty(h)&&(d[e+b+h]=g[h])}else d[e]=a[e]}return d},uuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"==a?b:3&b|8;return c.toString(16)})},guid:function(a){return this.guid.id=this.guid.id||1,a.id=void 0===a.id?"j_"+this.guid.id++:a.id,a.id},toKebabCase:function(a){return a.replace(/[A-Z]/g,"-$&").toLowerCase()},mixin:_.assign,supplement:_.defaults,deepMixin:_.mixin,deepSupplement:_.defaultsDeep,normalizeEvent:function(a){var b=a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0];if(b){for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return a},nextFrame:function(){var a;if("undefined"!=typeof window&&(a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame),!a){var b=0;a=function(a){var c=(new Date).getTime(),d=Math.max(0,16-(c-b)),e=setTimeout(function(){a(c+d)},d);return b=c+d,e}}return function(b,c){return a(c?b.bind(c):b)}}(),cancelFrame:function(){var a,b="undefined"!=typeof window;return b&&(a=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame),a=a||clearTimeout,b?a.bind(window):a}(),shapePerimeterConnectionPoint:function(a,b,c,d){var e,f;if(!c){var g=b.$(".scalable")[0],h=b.$(".rotatable")[0];g&&g.firstChild?c=g.firstChild:h&&h.firstChild&&(c=h.firstChild)}return c?(f=V(c).findIntersection(d,a.paper.viewport),f||(e=V(c).getBBox({target:a.paper.viewport}))):(e=b.model.getBBox(),f=e.intersectionWithLineFromCenterToPoint(d)),f||e.center()},parseCssNumeric:function(a,b){b=b||[];var c={value:parseFloat(a)};if(Number.isNaN(c.value))return null;var d=b.join("|");if(joint.util.isString(a)){var e=new RegExp("(\\d+)("+d+")$").exec(a);if(!e)return null;e[2]&&(c.unit=e[2])}return c},breakText:function(a,b,c,d){d=d||{};var e=b.width,f=b.height,g=d.svgDocument||V("svg").node,h=V("").attr(c||{}).node,i=h.firstChild,j=document.createTextNode("");h.style.opacity=0,h.style.display="block",i.style.display="block",i.appendChild(j),g.appendChild(h),d.svgDocument||document.body.appendChild(g);for(var k,l,m=a.split(" "),n=[],o=[],p=0,q=0,r=m.length;pf){o.splice(Math.floor(f/l));break}}}return d.svgDocument?g.removeChild(h):document.body.removeChild(g),o.join("\n")},imageToDataUri:function(a,b){if(!a||"data:"===a.substr(0,"data:".length))return setTimeout(function(){b(null,a)},0);var c=function(b,c){if(200===b.status){var d=new FileReader;d.onload=function(a){var b=a.target.result;c(null,b)},d.onerror=function(){c(new Error("Failed to load image "+a))},d.readAsDataURL(b.response)}else c(new Error("Failed to load image "+a))},d=function(b,c){var d=function(a){for(var b=32768,c=[],d=0;d=1)return 1;var b=a*a,c=b*a;return 4*(a<.5?c:3*(a-b)+c-.75)},exponential:function(a){return Math.pow(2,10*(a-1))},bounce:function(a){for(var b=0,c=1;1;b+=c,c/=2)if(a>=(7-4*b)/11){var d=(11-6*b-11*a)/4;return-d*d+c*c}},reverse:function(a){return function(b){return 1-a(1-b)}},reflect:function(a){return function(b){return.5*(b<.5?a(2*b):2-a(2-2*b))}},clamp:function(a,b,c){return b=b||0,c=c||1,function(d){var e=a(d);return ec?c:e}},back:function(a){return a||(a=1.70158),function(b){return b*b*((a+1)*b-a)}},elastic:function(a){return a||(a=1.5),function(b){return Math.pow(2,10*(b-1))*Math.cos(20*Math.PI*a/3*b)}}},interpolate:{number:function(a,b){var c=b-a;return function(b){return a+c*b}},object:function(a,b){var c=Object.keys(a);return function(d){var e,f,g={};for(e=c.length-1;e!=-1;e--)f=c[e],g[f]=a[f]+(b[f]-a[f])*d;return g}},hexColor:function(a,b){var c=parseInt(a.slice(1),16),d=parseInt(b.slice(1),16),e=255&c,f=(255&d)-e,g=65280&c,h=(65280&d)-g,i=16711680&c,j=(16711680&d)-i;return function(a){var b=e+f*a&255,c=g+h*a&65280,d=i+j*a&16711680;return"#"+(1<<24|b|c|d).toString(16).slice(1)}},unit:function(a,b){var c=/(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/,d=c.exec(a),e=c.exec(b),f=e[1].indexOf("."),g=f>0?e[1].length-f-1:0;a=+d[1];var h=+e[1]-a,i=d[2];return function(b){return(a+h*b).toFixed(g)+i}}},filter:{outline:function(a){var b='',c=Number.isFinite(a.margin)?a.margin:2,d=Number.isFinite(a.width)?a.width:1;return joint.util.template(b)({color:a.color||"blue",opacity:Number.isFinite(a.opacity)?a.opacity:1,outerRadius:c+d,innerRadius:c})},highlight:function(a){var b='';return joint.util.template(b)({color:a.color||"red",width:Number.isFinite(a.width)?a.width:1,blur:Number.isFinite(a.blur)?a.blur:0,opacity:Number.isFinite(a.opacity)?a.opacity:1})},blur:function(a){var b=Number.isFinite(a.x)?a.x:2;return joint.util.template('')({stdDeviation:Number.isFinite(a.y)?[b,a.y]:b})},dropShadow:function(a){var b="SVGFEDropShadowElement"in window?'':'';return joint.util.template(b)({dx:a.dx||0,dy:a.dy||0,opacity:Number.isFinite(a.opacity)?a.opacity:1,color:a.color||"black",blur:Number.isFinite(a.blur)?a.blur:4})},grayscale:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},sepia:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},saturate:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:1-b})},hueRotate:function(a){return joint.util.template('')({angle:a.angle||0})},invert:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:1-b})},brightness:function(a){return joint.util.template('')({amount:Number.isFinite(a.amount)?a.amount:1})},contrast:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:.5-b/2})}},format:{number:function(a,b,c){function d(a){for(var b=a.length,d=[],e=0,f=c.grouping[0];b>0&&f>0;)d.push(a.substring(b-=f,b+f)),f=c.grouping[e=(e+1)%c.grouping.length];return d.reverse().join(c.thousands)}c=c||{currency:["$",""],decimal:".",thousands:",",grouping:[3]};var e=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,f=e.exec(a),g=f[1]||" ",h=f[2]||">",i=f[3]||"",j=f[4]||"",k=f[5],l=+f[6],m=f[7],n=f[8],o=f[9],p=1,q="",r="",s=!1;switch(n&&(n=+n.substring(1)),(k||"0"===g&&"="===h)&&(k=g="0",h="=",m&&(l-=Math.floor((l-1)/4))),o){case"n":m=!0,o="g";break;case"%":p=100,r="%",o="f";break;case"p":p=100,r="%",o="r";break;case"b":case"o":case"x":case"X":"#"===j&&(q="0"+o.toLowerCase());break;case"c":case"d":s=!0,n=0;break;case"s":p=-1,o="r"}"$"===j&&(q=c.currency[0],r=c.currency[1]),"r"!=o||n||(o="g"),null!=n&&("g"==o?n=Math.max(1,Math.min(21,n)):"e"!=o&&"f"!=o||(n=Math.max(0,Math.min(20,n))));var t=k&&m;if(s&&b%1)return"";var u=b<0||0===b&&1/b<0?(b=-b,"-"):i,v=r;if(p<0){var w=this.prefix(b,n);b=w.scale(b),v=w.symbol+r}else b*=p;b=this.convert(o,b,n);var x=b.lastIndexOf("."),y=x<0?b:b.substring(0,x),z=x<0?"":c.decimal+b.substring(x+1);!k&&m&&c.grouping&&(y=d(y));var A=q.length+y.length+z.length+(t?0:u.length),B=A"===h?B+u+b:"^"===h?B.substring(0,A>>=1)+u+b+B.substring(A):u+(t?b:B+b))+v},string:function(a,b){for(var c,d="{",e=!1,f=[];(c=a.indexOf(d))!==-1;){var g,h,i;if(g=a.slice(0,c),e){h=g.split(":"),i=h.shift().split("."),g=b;for(var j=0;j8?function(a){return a/c}:function(a){return a*c},symbol:a}}),d=0;return a&&(a<0&&(a*=-1),b&&(a=this.round(a,this.precision(a,b))),d=1+Math.floor(1e-12+Math.log(a)/Math.LN10),d=Math.max(-24,Math.min(24,3*Math.floor((d<=0?d+1:d-1)/3)))),c[8+d/3]}},template:function(a){var b=/<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g;return function(c){return c=c||{},a.replace(b,function(a){for(var b=Array.from(arguments),d=b.slice(1,4).find(function(a){return!!a}),e=d.split("."),f=c[e.shift()];void 0!==f&&e.length;)f=f[e.shift()];return void 0!==f?f:""})}},toggleFullScreen:function(a){function b(a,b){for(var c=["webkit","moz","ms","o",""],d=0;d0&&b[0]||[],e=c>1&&b[c-1]||{};return Array.isArray(d)||(e instanceof joint.dia.Cell?d=b:d instanceof joint.dia.Cell&&(b.length>1&&b.pop(),d=b)),e instanceof joint.dia.Cell&&(e={}),a.call(this,d,e)}}},sortedIndex:_.sortedIndexBy||_.sortedIndex,uniq:_.uniqBy||_.uniq,uniqueId:_.uniqueId,sortBy:_.sortBy,isFunction:_.isFunction,result:_.result,union:_.union,invoke:_.invokeMap||_.invoke,difference:_.difference,intersection:_.intersection,omit:_.omit,pick:_.pick,has:_.has,bindAll:_.bindAll,assign:_.assign,defaults:_.defaults,defaultsDeep:_.defaultsDeep,isPlainObject:_.isPlainObject,isEmpty:_.isEmpty,isEqual:_.isEqual,noop:function(){},cloneDeep:_.cloneDeep,toArray:_.toArray,flattenDeep:_.flattenDeep,camelCase:_.camelCase,groupBy:_.groupBy,forIn:_.forIn,without:_.without,debounce:_.debounce,clone:_.clone,isBoolean:function(a){var b=Object.prototype.toString;return a===!0||a===!1||!!a&&"object"==typeof a&&"[object Boolean]"===b.call(a)},isObject:function(a){return!!a&&("object"==typeof a||"function"==typeof a)},isNumber:function(a){var b=Object.prototype.toString;return"number"==typeof a||!!a&&"object"==typeof a&&"[object Number]"===b.call(a)},isString:function(a){var b=Object.prototype.toString;return"string"==typeof a||!!a&&"object"==typeof a&&"[object String]"===b.call(a)},merge:function(){if(_.mergeWith){var a=Array.from(arguments),b=a[a.length-1],c=this.isFunction(b)?b:this.noop;return a.push(function(a,b){var d=c(a,b);return void 0!==d?d:Array.isArray(a)&&!Array.isArray(b)?b:void 0}),_.mergeWith.apply(this,a)}return _.merge.apply(this,arguments)}}};joint.mvc.View=Backbone.View.extend({options:{},theme:null,themeClassNamePrefix:joint.util.addClassNamePrefix("theme-"),requireSetThemeOverride:!1,defaultTheme:joint.config.defaultTheme,constructor:function(a){this.requireSetThemeOverride=a&&!!a.theme,this.options=joint.util.assign({},this.options,a),Backbone.View.call(this,a)},initialize:function(a){joint.util.bindAll(this,"setTheme","onSetTheme","remove","onRemove"),joint.mvc.views[this.cid]=this,this.setTheme(this.options.theme||this.defaultTheme),this.init()},_ensureElement:function(){if(this.el)this.setElement(joint.util.result(this,"el"));else{var a=joint.util.result(this,"tagName"),b=joint.util.assign({},joint.util.result(this,"attributes"));this.id&&(b.id=joint.util.result(this,"id")),this.setElement(this._createElement(a)),this._setAttributes(b)}this._ensureElClassName()},_setAttributes:function(a){this.svgElement?this.vel.attr(a):this.$el.attr(a)},_createElement:function(a){return this.svgElement?document.createElementNS(V.namespace.xmlns,a):document.createElement(a)},_setElement:function(a){this.$el=a instanceof Backbone.$?a:Backbone.$(a),this.el=this.$el[0],this.svgElement&&(this.vel=V(this.el))},_ensureElClassName:function(){var a=joint.util.result(this,"className"),b=joint.util.addClassNamePrefix(a);this.svgElement?this.vel.removeClass(a).addClass(b):this.$el.removeClass(a).addClass(b)},init:function(){},onRender:function(){},setTheme:function(a,b){return b=b||{},this.theme&&this.requireSetThemeOverride&&!b.override?this:(this.removeThemeClassName(),this.addThemeClassName(a),this.onSetTheme(this.theme,a),this.theme=a,this)},addThemeClassName:function(a){a=a||this.theme;var b=this.themeClassNamePrefix+a;return this.$el.addClass(b),this},removeThemeClassName:function(a){a=a||this.theme;var b=this.themeClassNamePrefix+a;return this.$el.removeClass(b),this},onSetTheme:function(a,b){},remove:function(){return this.onRemove(),joint.mvc.views[this.cid]=null,Backbone.View.prototype.remove.apply(this,arguments),this},onRemove:function(){},getEventNamespace:function(){return".joint-event-ns-"+this.cid}},{extend:function(){var a=Array.from(arguments),b=a[0]&&joint.util.assign({},a[0])||{},c=a[1]&&joint.util.assign({},a[1])||{},d=b.render||this.prototype&&this.prototype.render||null;return b.render=function(){return d&&d.apply(this,arguments),this.onRender(),this},Backbone.View.extend.call(this,b,c)}}),joint.dia.GraphCells=Backbone.Collection.extend({cellNamespace:joint.shapes,initialize:function(a,b){b.cellNamespace&&(this.cellNamespace=b.cellNamespace),this.graph=b.graph},model:function(a,b){var c=b.collection,d=c.cellNamespace,e="link"===a.type?joint.dia.Link:joint.util.getByPath(d,a.type,".")||joint.dia.Element,f=new e(a,b);return f.graph=c.graph,f},comparator:function(a){return a.get("z")||0}}),joint.dia.Graph=Backbone.Model.extend({_batches:{},initialize:function(a,b){b=b||{};var c=new joint.dia.GraphCells([],{model:b.cellModel,cellNamespace:b.cellNamespace,graph:this});Backbone.Model.prototype.set.call(this,"cells",c),c.on("all",this.trigger,this),this.on("change:z",this._sortOnChangeZ,this),this.on("batch:stop",this._onBatchStop,this),this._out={},this._in={},this._nodes={},this._edges={},c.on("add",this._restructureOnAdd,this),c.on("remove",this._restructureOnRemove,this),c.on("reset",this._restructureOnReset,this),c.on("change:source",this._restructureOnChangeSource,this),c.on("change:target",this._restructureOnChangeTarget,this),c.on("remove",this._removeCell,this)},_sortOnChangeZ:function(){this.hasActiveBatch("to-front")||this.hasActiveBatch("to-back")||this.get("cells").sort()},_onBatchStop:function(a){var b=a&&a.batchName;"to-front"!==b&&"to-back"!==b||this.hasActiveBatch(b)||this.get("cells").sort()},_restructureOnAdd:function(a){if(a.isLink()){this._edges[a.id]=!0;var b=a.get("source"),c=a.get("target");b.id&&((this._out[b.id]||(this._out[b.id]={}))[a.id]=!0),c.id&&((this._in[c.id]||(this._in[c.id]={}))[a.id]=!0)}else this._nodes[a.id]=!0},_restructureOnRemove:function(a){if(a.isLink()){delete this._edges[a.id];var b=a.get("source"),c=a.get("target");b.id&&this._out[b.id]&&this._out[b.id][a.id]&&delete this._out[b.id][a.id],c.id&&this._in[c.id]&&this._in[c.id][a.id]&&delete this._in[c.id][a.id]}else delete this._nodes[a.id]},_restructureOnReset:function(a){a=a.models,this._out={},this._in={},this._nodes={},this._edges={},a.forEach(this._restructureOnAdd,this)},_restructureOnChangeSource:function(a){var b=a.previous("source");b.id&&this._out[b.id]&&delete this._out[b.id][a.id];var c=a.get("source");c.id&&((this._out[c.id]||(this._out[c.id]={}))[a.id]=!0)},_restructureOnChangeTarget:function(a){var b=a.previous("target");b.id&&this._in[b.id]&&delete this._in[b.id][a.id];var c=a.get("target");c.id&&((this._in[c.id]||(this._in[c.id]={}))[a.id]=!0)},getOutboundEdges:function(a){return this._out&&this._out[a]||{}},getInboundEdges:function(a){return this._in&&this._in[a]||{}},toJSON:function(){var a=Backbone.Model.prototype.toJSON.apply(this,arguments);return a.cells=this.get("cells").toJSON(),a},fromJSON:function(a,b){if(!a.cells)throw new Error("Graph JSON must contain cells array.");return this.set(a,b)},set:function(a,b,c){var d;return"object"==typeof a?(d=a,c=b):(d={})[a]=b,d.hasOwnProperty("cells")&&(this.resetCells(d.cells,c),d=joint.util.omit(d,"cells")),Backbone.Model.prototype.set.call(this,d,c)},clear:function(a){a=joint.util.assign({},a,{clear:!0});var b=this.get("cells");if(0===b.length)return this;this.startBatch("clear",a);var c=b.sortBy(function(a){return a.isLink()?1:2});do c.shift().remove(a);while(c.length>0);return this.stopBatch("clear"),this},_prepareCell:function(a,b){var c;if(a instanceof Backbone.Model?(c=a.attributes,a.graph||b&&b.dry||(a.graph=this)):c=a,!joint.util.isString(c.type))throw new TypeError("dia.Graph: cell type must be a string.");return a},maxZIndex:function(){var a=this.get("cells").last();return a?a.get("z")||0:0},addCell:function(a,b){return Array.isArray(a)?this.addCells(a,b):(a instanceof Backbone.Model?a.has("z")||a.set("z",this.maxZIndex()+1):void 0===a.z&&(a.z=this.maxZIndex()+1),this.get("cells").add(this._prepareCell(a,b),b||{}),this)},addCells:function(a,b){return a.length&&(a=joint.util.flattenDeep(a),b.position=a.length,this.startBatch("add"),a.forEach(function(a){b.position--,this.addCell(a,b)},this),this.stopBatch("add")),this},resetCells:function(a,b){var c=joint.util.toArray(a).map(function(a){return this._prepareCell(a,b)},this);return this.get("cells").reset(c,b),this},removeCells:function(a,b){return a.length&&(this.startBatch("remove"),joint.util.invoke(a,"remove",b),this.stopBatch("remove")),this},_removeCell:function(a,b,c){c=c||{},c.clear||(c.disconnectLinks?this.disconnectLinks(a,c):this.removeLinks(a,c)),this.get("cells").remove(a,{silent:!0}),a.graph===this&&(a.graph=null)},getCell:function(a){return this.get("cells").get(a)},getCells:function(){return this.get("cells").toArray()},getElements:function(){return Object.keys(this._nodes).map(this.getCell,this)},getLinks:function(){return Object.keys(this._edges).map(this.getCell,this)},getFirstCell:function(){return this.get("cells").first()},getLastCell:function(){return this.get("cells").last()},getConnectedLinks:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=[],f={};if(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),b.deep){var g=a.getEmbeddedCells({deep:!0}),h={};g.forEach(function(a){a.isLink()&&(h[a.id]=!0)}),g.forEach(function(a){a.isLink()||(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)))},this)}return e},getNeighbors:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=this.getConnectedLinks(a,b).reduce(function(e,f){var g=f.get("source"),h=f.get("target"),i=f.hasLoop(b);if(c&&joint.util.has(g,"id")&&!e[g.id]){var j=this.getCell(g.id);!i&&(!j||j===a||b.deep&&j.isEmbeddedIn(a))||(e[g.id]=j)}if(d&&joint.util.has(h,"id")&&!e[h.id]){var k=this.getCell(h.id);!i&&(!k||k===a||b.deep&&k.isEmbeddedIn(a))||(e[h.id]=k)}return e}.bind(this),{});return joint.util.toArray(e)},getCommonAncestor:function(){var a=Array.from(arguments).map(function(a){for(var b=[],c=a.get("parent");c;)b.push(c),c=this.getCell(c).get("parent");return b},this);a=a.sort(function(a,b){return a.length-b.length});var b=joint.util.toArray(a.shift()).find(function(b){return a.every(function(a){return a.includes(b)})});return this.getCell(b)},getSuccessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{outbound:!0})),c},cloneCells:function(a){a=joint.util.uniq(a);var b=joint.util.toArray(a).reduce(function(a,b){return a[b.id]=b.clone(),a},{});return joint.util.toArray(a).forEach(function(a){var c=b[a.id];if(c.isLink()){var d=c.get("source"),e=c.get("target");d.id&&b[d.id]&&c.prop("source/id",b[d.id].id),e.id&&b[e.id]&&c.prop("target/id",b[e.id].id)}var f=a.get("parent");f&&b[f]&&c.set("parent",b[f].id);var g=joint.util.toArray(a.get("embeds")).reduce(function(a,c){return b[c]&&a.push(b[c].id),a},[]);joint.util.isEmpty(g)||c.set("embeds",g)}),b},cloneSubgraph:function(a,b){var c=this.getSubgraph(a,b);return this.cloneCells(c)},getSubgraph:function(a,b){b=b||{};var c=[],d={},e=[],f=[];return joint.util.toArray(a).forEach(function(a){if(d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a)),b.deep){var g=a.getEmbeddedCells({deep:!0});g.forEach(function(a){d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a))})}}),f.forEach(function(a){var b=a.get("source"),f=a.get("target");if(b.id&&!d[b.id]){var g=this.getCell(b.id);c.push(g),d[g.id]=g,e.push(g)}if(f.id&&!d[f.id]){var h=this.getCell(f.id);c.push(this.getCell(f.id)),d[h.id]=h,e.push(h)}},this),e.forEach(function(a){var e=this.getConnectedLinks(a,b);e.forEach(function(a){var b=a.get("source"),e=a.get("target");!d[a.id]&&b.id&&d[b.id]&&e.id&&d[e.id]&&(c.push(a),d[a.id]=a)})},this),c},getPredecessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{inbound:!0})),c},search:function(a,b,c){c=c||{},c.breadthFirst?this.bfs(a,b,c):this.dfs(a,b,c)},bfs:function(a,b,c){c=c||{};var d={},e={},f=[];for(f.push(a),e[a.id]=0;f.length>0;){var g=f.shift();if(!d[g.id]){if(d[g.id]=!0,b(g,e[g.id])===!1)return;this.getNeighbors(g,c).forEach(function(a){e[a.id]=e[g.id]+1,f.push(a)})}}},dfs:function(a,b,c,d,e){c=c||{};var f=d||{},g=e||0;b(a,g)!==!1&&(f[a.id]=!0,this.getNeighbors(a,c).forEach(function(a){f[a.id]||this.dfs(a,b,c,f,g+1)},this))},getSources:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._in[c]&&!joint.util.isEmpty(this._in[c])||a.push(this.getCell(c))}.bind(this)),a},getSinks:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._out[c]&&!joint.util.isEmpty(this._out[c])||a.push(this.getCell(c))}.bind(this)),a},isSource:function(a){return!this._in[a.id]||joint.util.isEmpty(this._in[a.id])},isSink:function(a){return!this._out[a.id]||joint.util.isEmpty(this._out[a.id])},isSuccessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{outbound:!0}),c},isPredecessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{inbound:!0}),c},isNeighbor:function(a,b,c){c=c||{};var d=c.inbound,e=c.outbound;void 0===d&&void 0===e&&(d=e=!0);var f=!1;return this.getConnectedLinks(a,c).forEach(function(a){var c=a.get("source"),g=a.get("target");return d&&joint.util.has(c,"id")&&c.id===b.id?(f=!0,!1):e&&joint.util.has(g,"id")&&g.id===b.id?(f=!0,!1):void 0}),f},disconnectLinks:function(a,b){this.getConnectedLinks(a).forEach(function(c){c.set(c.get("source").id===a.id?"source":"target",{x:0,y:0},b)})},removeLinks:function(a,b){joint.util.invoke(this.getConnectedLinks(a),"remove",b)},findModelsFromPoint:function(a){return this.getElements().filter(function(b){return b.getBBox().containsPoint(a)})},findModelsInArea:function(a,b){a=g.rect(a),b=joint.util.defaults(b||{},{strict:!1});var c=b.strict?"containsRect":"intersect";return this.getElements().filter(function(b){return a[c](b.getBBox())})},findModelsUnderElement:function(a,b){b=joint.util.defaults(b||{},{searchBy:"bbox"});var c=a.getBBox(),d="bbox"===b.searchBy?this.findModelsInArea(c):this.findModelsFromPoint(c[b.searchBy]());return d.filter(function(b){return a.id!==b.id&&!b.isEmbeddedIn(a)})},getBBox:function(a,b){return this.getCellsBBox(a||this.getElements(),b)},getCellsBBox:function(a,b){return joint.util.toArray(a).reduce(function(a,c){return c.isLink()?a:a?a.union(c.getBBox(b)):c.getBBox(b)},null)},translate:function(a,b,c){var d=this.getCells().filter(function(a){return!a.isEmbedded()});return joint.util.invoke(d,"translate",a,b,c),this},resize:function(a,b,c){return this.resizeCells(a,b,this.getCells(),c)},resizeCells:function(a,b,c,d){var e=this.getCellsBBox(c);if(e){var f=Math.max(a/e.width,0),g=Math.max(b/e.height,0);joint.util.invoke(c,"scale",f,g,e.origin(),d)}return this; -},startBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)+1,this.trigger("batch:start",joint.util.assign({},b,{batchName:a}))},stopBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)-1,this.trigger("batch:stop",joint.util.assign({},b,{batchName:a}))},hasActiveBatch:function(a){return a?!!this._batches[a]:joint.util.toArray(this._batches).some(function(a){return a>0})}}),joint.util.wrapWith(joint.dia.Graph.prototype,["resetCells","addCells","removeCells"],"cells"),function(a,b,c,d,e){function f(a){return e.isString(a)&&"%"===a.slice(-1)}function g(a,b){return function(c,d){var e=f(c);c=parseFloat(c),e&&(c/=100);var g={};if(isFinite(c)){var h=e||c>=0&&c<=1?c*d[b]:Math.max(c+d[b],0);g[a]=h}return g}}function h(a,b,d){return function(e,g){var h=f(e);e=parseFloat(e),h&&(e/=100);var i;if(isFinite(e)){var j=g[d]();i=h||e>0&&e<1?j[a]+g[b]*e:j[a]+e}var k=c.Point();return k[a]=i||0,k}}function i(a,b,d){return function(e,g){var h;h="middle"===e?g[b]/2:e===d?g[b]:isFinite(e)?e>-1&&e<1?-g[b]*e:-e:f(e)?g[b]*parseFloat(e)/100:0;var i=c.Point();return i[a]=-(g[a]+h),i}}var j=a.dia.attributes={xlinkHref:{set:"xlink:href"},xlinkShow:{set:"xlink:show"},xlinkRole:{set:"xlink:role"},xlinkType:{set:"xlink:type"},xlinkArcrole:{set:"xlink:arcrole"},xlinkTitle:{set:"xlink:title"},xlinkActuate:{set:"xlink:actuate"},xmlSpace:{set:"xml:space"},xmlBase:{set:"xml:base"},xmlLang:{set:"xml:lang"},preserveAspectRatio:{set:"preserveAspectRatio"},requiredExtension:{set:"requiredExtension"},requiredFeatures:{set:"requiredFeatures"},systemLanguage:{set:"systemLanguage"},externalResourcesRequired:{set:"externalResourceRequired"},filter:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineFilter(a)+")"}},fill:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},stroke:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},sourceMarker:{qualify:e.isPlainObject,set:function(a){return{"marker-start":"url(#"+this.paper.defineMarker(a)+")"}}},targetMarker:{qualify:e.isPlainObject,set:function(a){return a=e.assign({transform:"rotate(180)"},a),{"marker-end":"url(#"+this.paper.defineMarker(a)+")"}}},vertexMarker:{qualify:e.isPlainObject,set:function(a){return{"marker-mid":"url(#"+this.paper.defineMarker(a)+")"}}},text:{set:function(b,c,e,f){var g=d(e),h="joint-text",i=g.data(h),j=a.util.pick(f,"lineHeight","annotations","textPath","x","eol"),k=j.fontSize=f["font-size"]||f.fontSize,l=JSON.stringify([b,j]);void 0!==i&&i===l||(k&&e.setAttribute("font-size",k),V(e).text(""+b,j),g.data(h,l))}},textWrap:{qualify:e.isPlainObject,set:function(b,c,d,e){var g=b.width||0;f(g)?c.width*=parseFloat(g)/100:g<=0?c.width+=g:c.width=g;var h=b.height||0;f(h)?c.height*=parseFloat(h)/100:h<=0?c.height+=h:c.height=h;var i=a.util.breakText(""+b.text,c,{"font-weight":e["font-weight"]||e.fontWeight,"font-size":e["font-size"]||e.fontSize,"font-family":e["font-family"]||e.fontFamily},{svgDocument:this.paper.svg});V(d).text(i)}},lineHeight:{qualify:function(a,b,c){return void 0!==c.text}},textPath:{qualify:function(a,b,c){return void 0!==c.text}},annotations:{qualify:function(a,b,c){return void 0!==c.text}},port:{set:function(a){return null===a||void 0===a.id?a:a.id}},style:{qualify:e.isPlainObject,set:function(a,b,c){d(c).css(a)}},html:{set:function(a,b,c){d(c).html(a+"")}},ref:{},refX:{position:h("x","width","origin")},refY:{position:h("y","height","origin")},refDx:{position:h("x","width","corner")},refDy:{position:h("y","height","corner")},refWidth:{set:g("width","width")},refHeight:{set:g("height","height")},refRx:{set:g("rx","width")},refRy:{set:g("ry","height")},refCx:{set:g("cx","width")},refCy:{set:g("cy","height")},xAlignment:{offset:i("x","width","right")},yAlignment:{offset:i("y","height","bottom")},resetOffset:{offset:function(a,b){return a?{x:-b.x,y:-b.y}:{x:0,y:0}}}};j.refX2=j.refX,j.refY2=j.refY,j["ref-x"]=j.refX,j["ref-y"]=j.refY,j["ref-dy"]=j.refDy,j["ref-dx"]=j.refDx,j["ref-width"]=j.refWidth,j["ref-height"]=j.refHeight,j["x-alignment"]=j.xAlignment,j["y-alignment"]=j.yAlignment}(joint,_,g,$,joint.util),joint.dia.Cell=Backbone.Model.extend({constructor:function(a,b){var c,d=a||{};this.cid=joint.util.uniqueId("c"),this.attributes={},b&&b.collection&&(this.collection=b.collection),b&&b.parse&&(d=this.parse(d,b)||{}),(c=joint.util.result(this,"defaults"))&&(d=joint.util.merge({},c,d)),this.set(d,b),this.changed={},this.initialize.apply(this,arguments)},translate:function(a,b,c){throw new Error("Must define a translate() method.")},toJSON:function(){var a=this.constructor.prototype.defaults.attrs||{},b=this.attributes.attrs,c={};joint.util.forIn(b,function(b,d){var e=a[d];joint.util.forIn(b,function(a,b){joint.util.isObject(a)&&!Array.isArray(a)?joint.util.forIn(a,function(a,f){e&&e[b]&&joint.util.isEqual(e[b][f],a)||(c[d]=c[d]||{},(c[d][b]||(c[d][b]={}))[f]=a)}):e&&joint.util.isEqual(e[b],a)||(c[d]=c[d]||{},c[d][b]=a)})});var d=joint.util.cloneDeep(joint.util.omit(this.attributes,"attrs"));return d.attrs=c,d},initialize:function(a){a&&a.id||this.set("id",joint.util.uuid(),{silent:!0}),this._transitionIds={},this.processPorts(),this.on("change:attrs",this.processPorts,this)},processPorts:function(){var a=this.ports,b={};joint.util.forIn(this.get("attrs"),function(a,c){a&&a.port&&(void 0!==a.port.id?b[a.port.id]=a.port:b[a.port]={id:a.port})});var c={};if(joint.util.forIn(a,function(a,d){b[d]||(c[d]=!0)}),this.graph&&!joint.util.isEmpty(c)){var d=this.graph.getConnectedLinks(this,{inbound:!0});d.forEach(function(a){c[a.get("target").port]&&a.remove()});var e=this.graph.getConnectedLinks(this,{outbound:!0});e.forEach(function(a){c[a.get("source").port]&&a.remove()})}this.ports=b},remove:function(a){a=a||{};var b=this.graph;b&&b.startBatch("remove");var c=this.get("parent");if(c){var d=b&&b.getCell(c);d.unembed(this)}return joint.util.invoke(this.getEmbeddedCells(),"remove",a),this.trigger("remove",this,this.collection,a),b&&b.stopBatch("remove"),this},toFront:function(a){if(this.graph){a=a||{};var b=(this.graph.getLastCell().get("z")||0)+1;if(this.startBatch("to-front").set("z",b,a),a.deep){var c=this.getEmbeddedCells({deep:!0,breadthFirst:!0});c.forEach(function(c){c.set("z",++b,a)})}this.stopBatch("to-front")}return this},toBack:function(a){if(this.graph){a=a||{};var b=(this.graph.getFirstCell().get("z")||0)-1;if(this.startBatch("to-back"),a.deep){var c=this.getEmbeddedCells({deep:!0,breadthFirst:!0});c.reverse().forEach(function(c){c.set("z",b--,a)})}this.set("z",b,a).stopBatch("to-back")}return this},embed:function(a,b){if(this===a||this.isEmbeddedIn(a))throw new Error("Recursive embedding not allowed.");this.startBatch("embed");var c=joint.util.assign([],this.get("embeds"));return c[a.isLink()?"unshift":"push"](a.id),a.set("parent",this.id,b),this.set("embeds",joint.util.uniq(c),b),this.stopBatch("embed"),this},unembed:function(a,b){return this.startBatch("unembed"),a.unset("parent",b),this.set("embeds",joint.util.without(this.get("embeds"),a.id),b),this.stopBatch("unembed"),this},getAncestors:function(){var a=[],b=this.get("parent");if(!this.graph)return a;for(;void 0!==b;){var c=this.graph.getCell(b);if(void 0===c)break;a.push(c),b=c.get("parent")}return a},getEmbeddedCells:function(a){if(a=a||{},this.graph){var b;if(a.deep)if(a.breadthFirst){b=[];for(var c=this.getEmbeddedCells();c.length>0;){var d=c.shift();b.push(d),c.push.apply(c,d.getEmbeddedCells())}}else b=this.getEmbeddedCells(),b.forEach(function(c){b.push.apply(b,c.getEmbeddedCells(a))});else b=joint.util.toArray(this.get("embeds")).map(this.graph.getCell,this.graph);return b}return[]},isEmbeddedIn:function(a,b){var c=joint.util.isString(a)?a:a.id,d=this.get("parent");if(b=joint.util.defaults({deep:!0},b),this.graph&&b.deep){for(;d;){if(d===c)return!0;d=this.graph.getCell(d).get("parent")}return!1}return d===c},isEmbedded:function(){return!!this.get("parent")},clone:function(a){if(a=a||{},a.deep)return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null,[this].concat(this.getEmbeddedCells({deep:!0}))));var b=Backbone.Model.prototype.clone.apply(this,arguments);return b.set("id",joint.util.uuid()),b.unset("embeds"),b.unset("parent"),b},prop:function(a,b,c){var d="/",e=joint.util.isString(a);if(e||Array.isArray(a)){if(arguments.length>1){var f,g;e?(f=a,g=f.split("/")):(f=a.join(d),g=a.slice());var h=g[0],i=g.length;if(c=c||{},c.propertyPath=f,c.propertyValue=b,c.propertyPathArray=g,1===i)return this.set(h,b,c);for(var j={},k=j,l=h,m=1;m0)},getSelector:function(a,b){if(a===this.el)return b;var c;if(a){var d=V(a).index()+1;c=a.tagName+":nth-child("+d+")",b&&(c+=" > "+b),c=this.getSelector(a.parentNode,c)}return c},getAttributeDefinition:function(a){return this.model.constructor.getAttributeDefinition(a)},setNodeAttributes:function(a,b){joint.util.isEmpty(b)||(a instanceof SVGElement?V(a).attr(b):$(a).attr(b))},processNodeAttributes:function(a,b){var c,d,e,f,g,h,i,j,k,l=[];for(c in b)b.hasOwnProperty(c)&&(d=b[c],e=this.getAttributeDefinition(c),!e||joint.util.isFunction(e.qualify)&&!e.qualify.call(this,d,a,b)?(h||(h={}),h[joint.util.toKebabCase(c)]=d):(joint.util.isString(e.set)&&(h||(h={}),h[e.set]=d),null!==d&&l.push(c,e)));for(f=0,g=l.length;f0&&x.height>0){var y=V.transformRect(a.getBBox(),p).scale(1/r,1/s);for(e in m)f=m[e],h=this.getAttributeDefinition(e),t=h.offset.call(this,f,y,a,i),t&&(q.offset(g.Point(t).scale(r,s)),w||(w=!0))}}(void 0!==o||v||w)&&(q.round(1),p.e=q.x,p.f=q.y,a.setAttribute("transform",V.matrixToTransformString(p)))},getNodeScale:function(a,b){var c,d;if(b&&b.contains(a)){var e=b.scale();c=1/e.sx,d=1/e.sy}else c=1,d=1;return{sx:c,sy:d}},findNodesAttributes:function(a,b,c){var d={};for(var e in a)if(a.hasOwnProperty(e))for(var f=c[e]=this.findBySelector(e,b),g=0,h=f.length;g-1?l.splice(t,0,d):l.push(d)}else this.setNodeAttributes(e,i.normal);for(var u=0,v=l.length;u0){this.startBatch("fit-embeds",a),a.deep&&joint.util.invoke(b,"fitEmbeds",a);var c=this.graph.getCellsBBox(b),d=joint.util.normalizeSides(a.padding);c.moveAndExpand({x:-d.left,y:-d.top,width:d.right+d.left,height:d.bottom+d.top}),this.set({position:{x:c.x,y:c.y},size:{width:c.width,height:c.height}},a),this.stopBatch("fit-embeds")}return this},rotate:function(a,b,c,d){if(c){var e=this.getBBox().center(),f=this.get("size"),g=this.get("position");e.rotate(c,this.get("angle")-a);var h=e.x-f.width/2-g.x,i=e.y-f.height/2-g.y;this.startBatch("rotate",{angle:a,absolute:b,origin:c}),this.position(g.x+h,g.y+i,d),this.rotate(a,b,null,d),this.stopBatch("rotate")}else this.set("angle",b?a:(this.get("angle")+a)%360,d);return this},getBBox:function(a){if(a=a||{},a.deep&&this.graph){var b=this.getEmbeddedCells({deep:!0,breadthFirst:!0});return b.push(this),this.graph.getCellsBBox(b)}var c=this.get("position"),d=this.get("size");return g.rect(c.x,c.y,d.width,d.height)}}),joint.dia.ElementView=joint.dia.CellView.extend({_removePorts:function(){},_renderPorts:function(){},className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("element"),a.join(" ")},initialize:function(){joint.dia.CellView.prototype.initialize.apply(this,arguments);var a=this.model;this.listenTo(a,"change:position",this.translate),this.listenTo(a,"change:size",this.resize),this.listenTo(a,"change:angle",this.rotate),this.listenTo(a,"change:markup",this.render),this._initializePorts()},_initializePorts:function(){},update:function(a,b){this._removePorts();var c=this.model,d=c.attr();this.updateDOMSubtreeAttributes(this.el,d,{rootBBox:g.Rect(c.size()),scalableNode:this.scalableNode,rotatableNode:this.rotatableNode,roAttributes:b===d?null:b}),this._renderPorts()},renderMarkup:function(){var a=this.model.get("markup")||this.model.markup;if(!a)throw new Error("properties.markup is missing while the default render() implementation is used.");var b=joint.util.template(a)(),c=V(b);this.vel.append(c)},render:function(){this.$el.empty(),this.renderMarkup(),this.rotatableNode=this.vel.findOne(".rotatable");var a=this.scalableNode=this.vel.findOne(".scalable");return a&&this.update(),this.resize(),this.rotate(),this.translate(),this},resize:function(a,b,c){var d=this.model,e=d.get("size")||{width:1,height:1},f=d.get("angle")||0,g=this.scalableNode;if(!g)return 0!==f&&this.rotate(),void this.update();var h=!1;g.node.getElementsByTagName("path").length>0&&(h=!0);var i=g.getBBox({recursive:h}),j=e.width/(i.width||1),k=e.height/(i.height||1);g.attr("transform","scale("+j+","+k+")");var l=this.rotatableNode,m=l&&l.attr("transform");if(m&&"null"!==m){l.attr("transform",m+" rotate("+-f+","+e.width/2+","+e.height/2+")");var n=g.getBBox({target:this.paper.viewport});d.set("position",{x:n.x,y:n.y},c),this.rotate()}this.update()},translate:function(a,b,c){var d=this.model.get("position")||{x:0,y:0};this.vel.attr("transform","translate("+d.x+","+d.y+")")},rotate:function(){var a=this.rotatableNode;if(a){var b=this.model.get("angle")||0,c=this.model.get("size")||{width:1,height:1},d=c.width/2,e=c.height/2;0!==b?a.attr("transform","rotate("+b+","+d+","+e+")"):a.removeAttr("transform")}},getBBox:function(a){if(a&&a.useModelGeometry){var b=this.model.getBBox().bbox(this.model.get("angle"));return this.paper.localToPaperRect(b)}return joint.dia.CellView.prototype.getBBox.apply(this,arguments)},prepareEmbedding:function(a){a=a||{};var b=a.model||this.model,c=a.paper||this.paper,d=c.model;b.startBatch("to-front",a),b.toFront({deep:!0,ui:!0});var e=d.get("cells").max("z").get("z"),f=d.getConnectedLinks(b,{deep:!0});joint.util.invoke(f,"set","z",e+1,{ui:!0}),b.stopBatch("to-front");var g=b.get("parent");g&&d.getCell(g).unembed(b,{ui:!0})},processEmbedding:function(a){a=a||{};var b=a.model||this.model,c=a.paper||this.paper,d=c.options,e=c.model.findModelsUnderElement(b,{searchBy:d.findParentBy});d.frontParentOnly&&(e=e.slice(-1));for(var f=null,g=this._candidateEmbedView,h=e.length-1;h>=0;h--){var i=e[h];if(g&&g.model.id==i.id){f=g;break}var j=i.findView(c);if(d.validateEmbedding.call(c,this,j)){f=j;break}}f&&f!=g&&(this.clearEmbedding(),this._candidateEmbedView=f.highlight(null,{embedding:!0})),!f&&g&&this.clearEmbedding()},clearEmbedding:function(){var a=this._candidateEmbedView;a&&(a.unhighlight(null,{embedding:!0}),this._candidateEmbedView=null)},finalizeEmbedding:function(a){a=a||{};var b=this._candidateEmbedView,c=a.model||this.model,d=a.paper||this.paper;b&&(b.model.embed(c,{ui:!0}),b.unhighlight(null,{embedding:!0}),delete this._candidateEmbedView),joint.util.invoke(d.model.getConnectedLinks(c,{deep:!0}),"reparent",{ui:!0})},pointerdown:function(a,b,c){var d=this.paper;if(a.target.getAttribute("magnet")&&this.can("addLinkFromMagnet")&&d.options.validateMagnet.call(d,this,a.target)){this.model.startBatch("add-link");var e=d.getDefaultLink(this,a.target);e.set({source:{id:this.model.id,selector:this.getSelector(a.target),port:a.target.getAttribute("port")},target:{x:b,y:c}}),d.model.addCell(e);var f=this._linkView=d.findViewByModel(e);f.pointerdown(a,b,c),f.startArrowheadMove("target",{whenNotAllowed:"remove"})}else this._dx=b,this._dy=c,this.restrictedArea=d.getRestrictedArea(this),joint.dia.CellView.prototype.pointerdown.apply(this,arguments),this.notify("element:pointerdown",a,b,c)},pointermove:function(a,b,c){if(this._linkView)this._linkView.pointermove(a,b,c);else{var d=this.paper.options.gridSize;if(this.can("elementMove")){var e=this.model.get("position"),f=g.snapToGrid(e.x,d)-e.x+g.snapToGrid(b-this._dx,d),h=g.snapToGrid(e.y,d)-e.y+g.snapToGrid(c-this._dy,d);this.model.translate(f,h,{restrictedArea:this.restrictedArea,ui:!0}),this.paper.options.embeddingMode&&(this._inProcessOfEmbedding||(this.prepareEmbedding(),this._inProcessOfEmbedding=!0),this.processEmbedding())}this._dx=g.snapToGrid(b,d),this._dy=g.snapToGrid(c,d),joint.dia.CellView.prototype.pointermove.apply(this,arguments),this.notify("element:pointermove",a,b,c)}},pointerup:function(a,b,c){this._linkView?(this._linkView.pointerup(a,b,c),this._linkView=null,this.model.stopBatch("add-link")):(this._inProcessOfEmbedding&&(this.finalizeEmbedding(),this._inProcessOfEmbedding=!1),this.notify("element:pointerup",a,b,c),joint.dia.CellView.prototype.pointerup.apply(this,arguments))},mouseenter:function(a){joint.dia.CellView.prototype.mouseenter.apply(this,arguments),this.notify("element:mouseenter",a)},mouseleave:function(a){joint.dia.CellView.prototype.mouseleave.apply(this,arguments),this.notify("element:mouseleave",a)}}),joint.dia.Link=joint.dia.Cell.extend({markup:['','','','','','','',''].join(""),labelMarkup:['',"","",""].join(""),toolMarkup:['','','','',"Remove link.","",'','','',"Link options.","",""].join(""),vertexMarkup:['','','','',"Remove vertex.","",""].join(""),arrowheadMarkup:['','',""].join(""),defaults:{type:"link",source:{},target:{}},isLink:function(){return!0},disconnect:function(){return this.set({source:g.point(0,0),target:g.point(0,0)})},label:function(a,b,c){return a=a||0,arguments.length<=1?this.prop(["labels",a]):this.prop(["labels",a],b,c)},translate:function(a,b,c){return c=c||{},c.translateBy=c.translateBy||this.id,c.tx=a,c.ty=b,this.applyToPoints(function(c){return{x:(c.x||0)+a,y:(c.y||0)+b}},c)},scale:function(a,b,c,d){return this.applyToPoints(function(d){return g.point(d).scale(a,b,c).toJSON()},d)},applyToPoints:function(a,b){if(!joint.util.isFunction(a))throw new TypeError("dia.Link: applyToPoints expects its first parameter to be a function.");var c={},d=this.get("source");d.id||(c.source=a(d));var e=this.get("target");e.id||(c.target=a(e));var f=this.get("vertices");return f&&f.length>0&&(c.vertices=f.map(a)),this.set(c,b)},reparent:function(a){var b;if(this.graph){var c=this.graph.getCell(this.get("source").id),d=this.graph.getCell(this.get("target").id),e=this.graph.getCell(this.get("parent"));c&&d&&(b=this.graph.getCommonAncestor(c,d)),!e||b&&b.id===e.id||e.unembed(this,a),b&&b.embed(this,a)}return b},hasLoop:function(a){a=a||{};var b=this.get("source").id,c=this.get("target").id;if(!b||!c)return!1;var d=b===c;if(!d&&a.deep&&this.graph){var e=this.graph.getCell(b),f=this.graph.getCell(c);d=e.isEmbeddedIn(f)||f.isEmbeddedIn(e)}return d},getSourceElement:function(){var a=this.get("source");return a&&a.id&&this.graph&&this.graph.getCell(a.id)||null},getTargetElement:function(){var a=this.get("target"); -return a&&a.id&&this.graph&&this.graph.getCell(a.id)||null},getRelationshipAncestor:function(){var a;if(this.graph){var b=[this,this.getSourceElement(),this.getTargetElement()].filter(function(a){return!!a});a=this.graph.getCommonAncestor.apply(this.graph,b)}return a||null},isRelationshipEmbeddedIn:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a.id,c=this.getRelationshipAncestor();return!!c&&(c.id===b||c.isEmbeddedIn(b))}},{endsEqual:function(a,b){var c=a.port===b.port||!a.port&&!b.port;return a.id===b.id&&c}}),joint.dia.LinkView=joint.dia.CellView.extend({className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("link"),a.join(" ")},options:{shortLinkLength:100,doubleLinkTools:!1,longLinkLength:160,linkToolsOffset:40,doubleLinkToolsOffset:60,sampleInterval:50},_z:null,initialize:function(a){joint.dia.CellView.prototype.initialize.apply(this,arguments),"function"!=typeof this.constructor.prototype.watchSource&&(this.constructor.prototype.watchSource=this.createWatcher("source"),this.constructor.prototype.watchTarget=this.createWatcher("target")),this._labelCache={},this._markerCache={},this.startListening()},startListening:function(){var a=this.model;this.listenTo(a,"change:markup",this.render),this.listenTo(a,"change:smooth change:manhattan change:router change:connector",this.update),this.listenTo(a,"change:toolMarkup",this.onToolsChange),this.listenTo(a,"change:labels change:labelMarkup",this.onLabelsChange),this.listenTo(a,"change:vertices change:vertexMarkup",this.onVerticesChange),this.listenTo(a,"change:source",this.onSourceChange),this.listenTo(a,"change:target",this.onTargetChange)},onSourceChange:function(a,b,c){this.watchSource(a,b),c.translateBy&&this.model.get("target").id||(c.updateConnectionOnly=!0,this.update(this.model,null,c))},onTargetChange:function(a,b,c){this.watchTarget(a,b),c.translateBy||(c.updateConnectionOnly=!0,this.update(this.model,null,c))},onVerticesChange:function(a,b,c){this.renderVertexMarkers(),c.translateBy&&c.translateBy!==this.model.id||(c.updateConnectionOnly=!0,this.update(a,null,c))},onToolsChange:function(){this.renderTools().updateToolsPosition()},onLabelsChange:function(a,b,c){var d=!0,e=this.model.previous("labels");if(e&&"propertyPathArray"in c&&"propertyValue"in c){var f=c.propertyPathArray||[],g=f.length;if(g>1){var h=!!e[f[1]];h&&(2===g?d="markup"in Object(c.propertyValue):"markup"!==f[2]&&(d=!1))}}d?this.renderLabels():this.updateLabels(),this.updateLabelPositions()},render:function(){this.$el.empty();var a=this.model,b=a.get("markup")||a.markup,c=V(b);if(Array.isArray(c)||(c=[c]),this._V={},c.forEach(function(a){var b=a.attr("class");b&&(b=joint.util.removeClassNamePrefix(b),this._V[$.camelCase(b)]=a)},this),!this._V.connection)throw new Error("link: no connection path in the markup");return this.renderTools(),this.renderVertexMarkers(),this.renderArrowheadMarkers(),this.vel.append(c),this.renderLabels(),this.watchSource(a,a.get("source")).watchTarget(a,a.get("target")).update(),this},renderLabels:function(){var a=this._V.labels;if(!a)return this;a.empty();var b=this.model,c=b.get("labels")||[],d=this._labelCache={},e=c.length;if(0===e)return this;for(var f=joint.util.template(b.get("labelMarkup")||b.labelMarkup),g=V(f()),h=0;hd?d:l,l=l<0?d+l:l,l=l>1?l:d*l):l=d/2,h=c.getPointAtLength(l),joint.util.isObject(m))h=g.point(h).offset(m);else if(Number.isFinite(m)){b||(b=this._samples||this._V.connection.sample(this.options.sampleInterval));for(var n,o,p,q=1/0,r=0,s=b.length;r=this.options.longLinkLength){var e=this.options.doubleLinkToolsOffset||b;d=this.getPointAtLength(c-e),this._tool2Cache.attr("transform","translate("+d.x+", "+d.y+") "+a),this._tool2Cache.attr("visibility","visible")}else this.options.doubleLinkTools&&this._tool2Cache.attr("visibility","hidden")}return this},updateArrowheadMarkers:function(){if(!this._V.markerArrowheads)return this;if("none"===$.css(this._V.markerArrowheads.node,"display"))return this;var a=this.getConnectionLength()g);)c=d.slice();return h===-1&&(h=0,c.splice(h,0,a)),this.model.set("vertices",c,{ui:!0}),h},sendToken:function(a,b,c){function d(a,b){return function(){a.remove(),"function"==typeof b&&b()}}var e,f;joint.util.isObject(b)?(e=b.duration,f="reverse"===b.direction):(e=b,f=!1),e=e||1e3;var g={dur:e+"ms",repeatCount:1,calcMode:"linear",fill:"freeze"};f&&(g.keyPoints="1;0",g.keyTimes="0;1");var h=V(a),i=this._V.connection;h.appendTo(this.paper.viewport).animateAlongPath(g,i),setTimeout(d(h,c),e)},findRoute:function(a){var b=joint.routers,c=this.model.get("router"),d=this.paper.options.defaultRouter;if(!c)if(this.model.get("manhattan"))c={name:"orthogonal"};else{if(!d)return a;c=d}var e=c.args||{},f=joint.util.isFunction(c)?c:b[c.name];if(!joint.util.isFunction(f))throw new Error('unknown router: "'+c.name+'"');var g=f.call(this,a||[],e,this);return g},getPathData:function(a){var b=joint.connectors,c=this.model.get("connector"),d=this.paper.options.defaultConnector;c||(c=this.model.get("smooth")?{name:"smooth"}:d||{});var e=joint.util.isFunction(c)?c:b[c.name],f=c.args||{};if(!joint.util.isFunction(e))throw new Error('unknown connector: "'+c.name+'"');var g=e.call(this,this._markerCache.sourcePoint,this._markerCache.targetPoint,a||this.model.get("vertices")||{},f,this);return g},getConnectionPoint:function(a,b,c){var d;if(joint.util.isEmpty(b)&&(b={x:0,y:0}),joint.util.isEmpty(c)&&(c={x:0,y:0}),b.id){var e,f=g.Rect("source"===a?this.sourceBBox:this.targetBBox);if(c.id){var h=g.Rect("source"===a?this.targetBBox:this.sourceBBox);e=h.intersectionWithLineFromCenterToPoint(f.center()),e=e||h.center()}else e=g.Point(c);var i=this.paper.options;if(i.perpendicularLinks||this.options.perpendicular){var j,k=f.origin(),l=f.corner();if(k.y<=e.y&&e.y<=l.y)switch(j=f.sideNearestToPoint(e)){case"left":d=g.Point(k.x,e.y);break;case"right":d=g.Point(l.x,e.y);break;default:d=f.center()}else if(k.x<=e.x&&e.x<=l.x)switch(j=f.sideNearestToPoint(e)){case"top":d=g.Point(e.x,k.y);break;case"bottom":d=g.Point(e.x,l.y);break;default:d=f.center()}else d=f.intersectionWithLineFromCenterToPoint(e),d=d||f.center()}else if(i.linkConnectionPoint){var m="target"===a?this.targetView:this.sourceView,n="target"===a?this.targetMagnet:this.sourceMagnet;d=i.linkConnectionPoint(this,m,n,e,a)}else d=f.intersectionWithLineFromCenterToPoint(e),d=d||f.center()}else d=g.Point(b);return d},getConnectionLength:function(){return this._V.connection.node.getTotalLength()},getPointAtLength:function(a){return this._V.connection.node.getPointAtLength(a)},_beforeArrowheadMove:function(){this._z=this.model.get("z"),this.model.toFront(),this.el.style.pointerEvents="none",this.paper.options.markAvailable&&this._markAvailableMagnets()},_afterArrowheadMove:function(){null!==this._z&&(this.model.set("z",this._z,{ui:!0}),this._z=null),this.el.style.pointerEvents="visiblePainted",this.paper.options.markAvailable&&this._unmarkAvailableMagnets()},_createValidateConnectionArgs:function(a){function b(a,b){return c[f]=a,c[f+1]=a.el===b?void 0:b,c}var c=[];c[4]=a,c[5]=this;var d,e=0,f=0;"source"===a?(e=2,d="target"):(f=2,d="source");var g=this.model.get(d);return g.id&&(c[e]=this.paper.findViewByModel(g.id),c[e+1]=g.selector&&c[e].el.querySelector(g.selector)),b},_markAvailableMagnets:function(){function a(a,b){var c=a.paper,d=c.options.validateConnection;return d.apply(c,this._validateConnectionArgs(a,b))}var b=this.paper,c=b.model.getElements();this._marked={};for(var d=0,e=c.length;d0){for(var i=0,j=h.length;i").addClass(joint.util.addClassNamePrefix("paper-background")),this.options.background&&this.drawBackground(this.options.background),this.$grid=$("
").addClass(joint.util.addClassNamePrefix("paper-grid")),this.options.drawGrid&&this.drawGrid(),this.$el.append(this.$background,this.$grid,this.svg),this},update:function(){return this.options.drawGrid&&this.drawGrid(),this._background&&this.updateBackgroundImage(this._background),this},_viewportMatrix:null,_viewportTransformString:null,matrix:function(a){var b=this.viewport;if(void 0===a){var c=b.getAttribute("transform");return(this._viewportTransformString||null)===c?a=this._viewportMatrix:(a=b.getCTM(),this._viewportMatrix=a,this._viewportTransformString=c),V.createSVGMatrix(a)}return a=V.createSVGMatrix(a),V(b).transform(a,{absolute:!0}),this._viewportMatrix=a,this._viewportTransformString=b.getAttribute("transform"),this},clientMatrix:function(){return V.createSVGMatrix(this.viewport.getScreenCTM())},_onSort:function(){this.model.hasActiveBatch("add")||this.sortViews()},_onBatchStop:function(a){var b=a&&a.batchName;"add"!==b||this.model.hasActiveBatch("add")||this.sortViews()},onRemove:function(){this.removeViews(),this.unbindDocumentEvents()},setDimensions:function(a,b){a=this.options.width=a||this.options.width,b=this.options.height=b||this.options.height,this.$el.css({width:Math.round(a),height:Math.round(b)}),this.trigger("resize",a,b)},setOrigin:function(a,b){return this.translate(a||0,b||0,{absolute:!0})},fitToContent:function(a,b,c,d){joint.util.isObject(a)?(d=a,a=d.gridWidth||1,b=d.gridHeight||1,c=d.padding||0):(d=d||{},a=a||1,b=b||1,c=c||0),c=joint.util.normalizeSides(c);var e=V(this.viewport).getBBox(),f=this.scale(),g=this.translate();e.x*=f.sx,e.y*=f.sy,e.width*=f.sx,e.height*=f.sy;var h=Math.max(Math.ceil((e.width+e.x)/a),1)*a,i=Math.max(Math.ceil((e.height+e.y)/b),1)*b,j=0,k=0;("negative"==d.allowNewOrigin&&e.x<0||"positive"==d.allowNewOrigin&&e.x>=0||"any"==d.allowNewOrigin)&&(j=Math.ceil(-e.x/a)*a,j+=c.left,h+=j),("negative"==d.allowNewOrigin&&e.y<0||"positive"==d.allowNewOrigin&&e.y>=0||"any"==d.allowNewOrigin)&&(k=Math.ceil(-e.y/b)*b,k+=c.top,i+=k),h+=c.right,i+=c.bottom,h=Math.max(h,d.minWidth||0),i=Math.max(i,d.minHeight||0),h=Math.min(h,d.maxWidth||Number.MAX_VALUE),i=Math.min(i,d.maxHeight||Number.MAX_VALUE);var l=h!=this.options.width||i!=this.options.height,m=j!=g.tx||k!=g.ty;m&&this.translate(j,k),l&&this.setDimensions(h,i)},scaleContentToFit:function(a){var b=this.getContentBBox();if(b.width&&b.height){a=a||{},joint.util.defaults(a,{padding:0,preserveAspectRatio:!0,scaleGrid:null,minScale:0,maxScale:Number.MAX_VALUE});var c,d=a.padding,e=a.minScaleX||a.minScale,f=a.maxScaleX||a.maxScale,h=a.minScaleY||a.minScale,i=a.maxScaleY||a.maxScale;if(a.fittingBBox)c=a.fittingBBox;else{var j=this.translate();c={x:j.tx,y:j.ty,width:this.options.width,height:this.options.height}}c=g.rect(c).moveAndExpand({x:d,y:d,width:-2*d,height:-2*d});var k=this.scale(),l=c.width/b.width*k.sx,m=c.height/b.height*k.sy;if(a.preserveAspectRatio&&(l=m=Math.min(l,m)),a.scaleGrid){var n=a.scaleGrid;l=n*Math.floor(l/n),m=n*Math.floor(m/n)}l=Math.min(f,Math.max(e,l)),m=Math.min(i,Math.max(h,m)),this.scale(l,m);var o=this.getContentBBox(),p=c.x-o.x,q=c.y-o.y;this.translate(p,q)}},getContentBBox:function(){var a=this.viewport.getBoundingClientRect(),b=this.clientMatrix(),c=this.translate();return g.rect({x:a.left-b.e+c.tx,y:a.top-b.f+c.ty,width:a.width,height:a.height})},getArea:function(){return this.paperToLocalRect({x:0,y:0,width:this.options.width,height:this.options.height})},getRestrictedArea:function(){var a;return a=joint.util.isFunction(this.options.restrictTranslate)?this.options.restrictTranslate.apply(this,arguments):this.options.restrictTranslate===!0?this.getArea():this.options.restrictTranslate||null},createViewForModel:function(a){var b,c,d=this.options.cellViewNamespace,e=a.get("type")+"View",f=joint.util.getByPath(d,e,".");a.isLink()?(b=this.options.linkView,c=joint.dia.LinkView):(b=this.options.elementView,c=joint.dia.ElementView);var g=b.prototype instanceof Backbone.View?f||b:b.call(this,a)||f||c;return new g({model:a,interactive:this.options.interactive})},onCellAdded:function(a,b,c){if(this.options.async&&c.async!==!1&&joint.util.isNumber(c.position)){if(this._asyncCells=this._asyncCells||[],this._asyncCells.push(a),0==c.position){if(this._frameId)throw new Error("another asynchronous rendering in progress");this.asyncRenderViews(this._asyncCells,c),delete this._asyncCells}}else this.renderView(a)},removeView:function(a){var b=this._views[a.id];return b&&(b.remove(),delete this._views[a.id]),b},renderView:function(a){var b=this._views[a.id]=this.createViewForModel(a);return V(this.viewport).append(b.el),b.paper=this,b.render(),$(b.el).find("image").on("dragstart",function(){return!1}),b},beforeRenderViews:function(a){return a.sort(function(a){return a.isLink()?1:-1}),a},afterRenderViews:function(){this.sortViews()},resetViews:function(a,b){this.removeViews();var c=a.models.slice();if(c=this.beforeRenderViews(c,b)||c,this.cancelRenderViews(),this.options.async)this.asyncRenderViews(c,b);else{for(var d=0,e=c.length;d(e.get("z")||0)?1:-1})},scale:function(a,b,c,d){if(void 0===a)return V.matrixToScale(this.matrix());void 0===b&&(b=a),void 0===c&&(c=0,d=0);var e=this.translate();if(c||d||e.tx||e.ty){var f=e.tx-c*(a-1),g=e.ty-d*(b-1);this.translate(f,g)}var h=this.matrix();return h.a=a||0,h.d=b||0,this.matrix(h),this.trigger("scale",a,b,c,d),this},rotate:function(a,b,c){if(void 0===a)return V.matrixToRotate(this.matrix());if(void 0===b){var d=this.viewport.getBBox();b=d.width/2,c=d.height/2}var e=this.matrix().translate(b,c).rotate(a).translate(-b,-c);return this.matrix(e),this},translate:function(a,b){if(void 0===a)return V.matrixToTranslate(this.matrix());var c=this.matrix();c.e=a||0,c.f=b||0,this.matrix(c);var d=this.translate(),e=this.options.origin;return e.x=d.tx,e.y=d.ty,this.trigger("translate",d.tx,d.ty),this.options.drawGrid&&this.drawGrid(),this},findView:function(a){for(var b=joint.util.isString(a)?this.viewport.querySelector(a):a instanceof $?a[0]:a;b&&b!==this.el&&b!==document;){var c=b.getAttribute("model-id");if(c)return this._views[c];b=b.parentNode}},findViewByModel:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a&&a.id;return this._views[b]},findViewsFromPoint:function(a){a=g.point(a);var b=this.model.getElements().map(this.findViewByModel,this);return b.filter(function(b){return b&&b.vel.getBBox({target:this.viewport}).containsPoint(a)},this)},findViewsInArea:function(a,b){b=joint.util.defaults(b||{},{strict:!1}),a=g.rect(a);var c=this.model.getElements().map(this.findViewByModel,this),d=b.strict?"containsRect":"intersect";return c.filter(function(b){return b&&a[d](b.vel.getBBox({target:this.viewport}))},this)},getModelById:function(a){return this.model.getCell(a)},snapToGrid:function(a,b){return this.clientToLocalPoint(a,b).snapToGrid(this.options.gridSize)},localToPaperPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix());return g.Point(d)},localToPaperRect:function(a,b,c,d){var e=g.Rect(a,b),f=V.transformRect(e,this.matrix()); -return g.Rect(f)},paperToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix().inverse());return g.Point(d)},paperToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.matrix().inverse());return g.Rect(f)},localToClientPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix());return g.Point(d)},localToClientRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix());return g.Rect(f)},clientToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix().inverse());return g.Point(d)},clientToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix().inverse());return g.Rect(f)},localToPagePoint:function(a,b){return this.localToPaperPoint(a,b).offset(this.pageOffset())},localToPageRect:function(a,b,c,d){return this.localToPaperRect(a,b,c,d).moveAndExpand(this.pageOffset())},pageToLocalPoint:function(a,b){var c=g.Point(a,b),d=c.difference(this.pageOffset());return this.paperToLocalPoint(d)},pageToLocalRect:function(a,b,c,d){var e=this.pageOffset(),f=g.Rect(a,b,c,d);return f.x-=e.x,f.y-=e.y,this.paperToLocalRect(f)},clientOffset:function(){var a=this.svg.getBoundingClientRect();return g.Point(a.left,a.top)},pageOffset:function(){return this.clientOffset().offset(window.scrollX,window.scrollY)},linkAllowed:function(a){var b;if(a instanceof joint.dia.Link)b=a;else{if(!(a instanceof joint.dia.LinkView))throw new Error("Must provide link model or view.");b=a.model}if(!this.options.multiLinks){var c=b.get("source"),d=b.get("target");if(c.id&&d.id){var e=b.getSourceElement();if(e){var f=this.model.getConnectedLinks(e,{outbound:!0,inbound:!1}),g=f.filter(function(a){var b=a.get("source"),e=a.get("target");return b&&b.id===c.id&&(!b.port||b.port===c.port)&&e&&e.id===d.id&&(!e.port||e.port===d.port)}).length;if(g>1)return!1}}}return!!(this.options.linkPinning||joint.util.has(b.get("source"),"id")&&joint.util.has(b.get("target"),"id"))},getDefaultLink:function(a,b){return joint.util.isFunction(this.options.defaultLink)?this.options.defaultLink.call(this,a,b):this.options.defaultLink.clone()},resolveHighlighter:function(a){a=a||{};var b=a.highlighter,c=this.options;if(void 0===b){var d=["embedding","connecting","magnetAvailability","elementAvailability"].find(function(b){return!!a[b]});b=d&&c.highlighting[d]||c.highlighting.default}if(!b)return!1;joint.util.isString(b)&&(b={name:b});var e=b.name,f=c.highlighterNamespace[e];if(!f)throw new Error('Unknown highlighter ("'+e+'")');if("function"!=typeof f.highlight)throw new Error('Highlighter ("'+e+'") is missing required highlight() method');if("function"!=typeof f.unhighlight)throw new Error('Highlighter ("'+e+'") is missing required unhighlight() method');return{highlighter:f,options:b.options||{},name:e}},onCellHighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){b.id||(b.id=V.uniqueId());var d=c.name+b.id+JSON.stringify(c.options);if(!this._highlights[d]){var e=c.highlighter;e.highlight(a,b,joint.util.assign({},c.options)),this._highlights[d]={cellView:a,magnetEl:b,opt:c.options,highlighter:e}}}},onCellUnhighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){var d=c.name+b.id+JSON.stringify(c.options),e=this._highlights[d];e&&(e.highlighter.unhighlight(e.cellView,e.magnetEl,e.opt),this._highlights[d]=null)}},mousedblclick:function(a){a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerdblclick(a,c.x,c.y):this.trigger("blank:pointerdblclick",a,c.x,c.y)}},mouseclick:function(a){if(this._mousemoved<=this.options.clickThreshold){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(this.guard(a,b))return;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerclick(a,c.x,c.y):this.trigger("blank:pointerclick",a,c.x,c.y)}},guard:function(a,b){return!(!this.options.guard||!this.options.guard(a,b))||(a.data&&void 0!==a.data.guarded?a.data.guarded:!(b&&b.model&&b.model instanceof joint.dia.Cell)&&(this.svg!==a.target&&this.el!==a.target&&!$.contains(this.svg,a.target)))},contextmenu:function(a){a=joint.util.normalizeEvent(a),this.options.preventContextMenu&&a.preventDefault();var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.contextmenu(a,c.x,c.y):this.trigger("blank:contextmenu",a,c.x,c.y)}},pointerdown:function(a){this.bindDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){this._mousemoved=0;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?(a.preventDefault(),this.sourceView=b,b.pointerdown(a,c.x,c.y)):(this.options.preventDefaultBlankAction&&a.preventDefault(),this.trigger("blank:pointerdown",a,c.x,c.y))}},pointermove:function(a){var b=this.sourceView;if(b){a.preventDefault();var c=++this._mousemoved;if(c>this.options.moveThreshold){a=joint.util.normalizeEvent(a);var d=this.snapToGrid({x:a.clientX,y:a.clientY});b.pointermove(a,d.x,d.y)}}},pointerup:function(a){this.unbindDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.snapToGrid({x:a.clientX,y:a.clientY});this.sourceView?(this.sourceView.pointerup(a,b.x,b.y),this.sourceView=null):this.trigger("blank:pointerup",a,b.x,b.y)},mousewheel:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=a.originalEvent,d=this.snapToGrid({x:c.clientX,y:c.clientY}),e=Math.max(-1,Math.min(1,c.wheelDelta||-c.detail));b?b.mousewheel(a,d.x,d.y,e):this.trigger("blank:mousewheel",a,d.x,d.y,e)}},cellMouseover:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(b){if(this.guard(a,b))return;b.mouseover(a)}},cellMouseout:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(b){if(this.guard(a,b))return;b.mouseout(a)}},cellMouseenter:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);b&&!this.guard(a,b)&&b.mouseenter(a)},cellMouseleave:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);b&&!this.guard(a,b)&&b.mouseleave(a)},cellEvent:function(a){a=joint.util.normalizeEvent(a);var b=a.currentTarget,c=b.getAttribute("event");if(c){var d=this.findView(b);if(d&&!this.guard(a,d)){var e=this.snapToGrid({x:a.clientX,y:a.clientY});d.event(a,c,e.x,e.y)}}},setGridSize:function(a){return this.options.gridSize=a,this.options.drawGrid&&this.drawGrid(),this},clearGrid:function(){return this.$grid&&this.$grid.css("backgroundImage","none"),this},_getGriRefs:function(){return this._gridCache||(this._gridCache={root:V("svg",{width:"100%",height:"100%"},V("defs")),patterns:{},add:function(a,b){V(this.root.node.childNodes[0]).append(b),this.patterns[a]=b,this.root.append(V("rect",{width:"100%",height:"100%",fill:"url(#"+a+")"}))},get:function(a){return this.patterns[a]},exist:function(a){return void 0!==this.patterns[a]}}),this._gridCache},setGrid:function(a){this.clearGrid(),this._gridCache=null,this._gridSettings=[];var b=Array.isArray(a)?a:[a||{}];return b.forEach(function(a){this._gridSettings.push.apply(this._gridSettings,this._resolveDrawGridOption(a))},this),this},_resolveDrawGridOption:function(a){var b=this.constructor.gridPatterns;if(joint.util.isString(a)&&Array.isArray(b[a]))return b[a].map(function(a){return joint.util.assign({},a)});var c=a||{args:[{}]},d=Array.isArray(c),e=c.name;if(d||e||c.markup||(e="dot"),e&&Array.isArray(b[e])){var f=b[e].map(function(a){return joint.util.assign({},a)}),g=Array.isArray(c.args)?c.args:[c.args||{}];joint.util.defaults(g[0],joint.util.omit(a,"args"));for(var h=0;h'),f=joint.util.toArray(d).map(function(a){return e({offset:a.offset,color:a.color,opacity:Number.isFinite(a.opacity)?a.opacity:1})}),g=["<"+c+">",f.join(""),""].join(""),h=joint.util.assign({id:b},a.attrs);V(g,h).appendTo(this.defs)}return b},defineMarker:function(a){if(!joint.util.isObject(a))throw new TypeError("dia.Paper: defineMarker() requires 1. argument to be an object.");var b=a.id;if(b||(b=this.svg.id+joint.util.hashCode(JSON.stringify(a))),!this.isDefined(b)){var c=joint.util.omit(a,"type","userSpaceOnUse"),d=V("marker",{id:b,orient:"auto",overflow:"visible",markerUnits:a.markerUnits||"userSpaceOnUse"},[V(a.type||"path",c)]);d.appendTo(this.defs)}return b}},{backgroundPatterns:{flipXy:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,-1,b.width,b.height),e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,1,b.width,0),e.drawImage(a,0,0,c,d),e.setTransform(1,0,0,-1,0,b.height),e.drawImage(a,0,0,c,d),b},flipX:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(2*c,0),e.scale(-1,1),e.drawImage(a,0,0,c,d),b},flipY:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(0,2*d),e.scale(1,-1),e.drawImage(a,0,0,c,d),b},watermark:function(a,b){b=b||{};var c=a.width,d=a.height,e=document.createElement("canvas");e.width=3*c,e.height=3*d;for(var f=e.getContext("2d"),h=joint.util.isNumber(b.watermarkAngle)?-b.watermarkAngle:-20,i=g.toRad(h),j=e.width/4,k=e.height/4,l=0;l<4;l++)for(var m=0;m<4;m++)(l+m)%2>0&&(f.setTransform(1,0,0,1,(2*l-1)*j,(2*m-1)*k),f.rotate(i),f.drawImage(a,-c/2,-d/2,c,d));return e}},gridPatterns:{dot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){V(a).attr({width:b.thickness*b.sx,height:b.thickness*b.sy,fill:b.color})}}],fixedDot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){var c=b.sx<=1?b.thickness*b.sx:b.thickness;V(a).attr({width:c,height:c,fill:b.color})}}],mesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}],doubleMesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}},{color:"#000000",thickness:3,scaleFactor:4,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}]}}),function(a,b,c){var d=function(b){var d=c.cloneDeep(b)||{};this.ports=[],this.groups={},this.portLayoutNamespace=a.layout.Port,this.portLabelLayoutNamespace=a.layout.PortLabel,this._init(d)};d.prototype={getPorts:function(){return this.ports},getGroup:function(a){return this.groups[a]||{}},getPortsByGroup:function(a){return this.ports.filter(function(b){return b.group===a})},getGroupPortsMetrics:function(a,b){var d=this.getGroup(a),e=this.getPortsByGroup(a),f=d.position||{},h=f.name,i=this.portLayoutNamespace;i[h]||(h="left");var j=f.args||{},k=e.map(function(a){return a&&a.position&&a.position.args}),l=i[h](k,b,j),m={ports:e,result:[]};return c.toArray(l).reduce(function(a,c,d){var e=a.ports[d];return a.result.push({portId:e.id,portTransformation:c,labelTransformation:this._getPortLabelLayout(e,g.Point(c),b),portAttrs:e.attrs,portSize:e.size,labelSize:e.label.size}),a}.bind(this),m),m.result},_getPortLabelLayout:function(a,b,c){var d=this.portLabelLayoutNamespace,e=a.label.position.name||"left";return d[e]?d[e](b,c,a.label.position.args):null},_init:function(a){if(c.isObject(a.groups))for(var b=Object.keys(a.groups),d=0,e=b.length;d0},hasPort:function(a){return this.getPortIndex(a)!==-1},getPorts:function(){return c.cloneDeep(this.prop("ports/items"))||[]},getPort:function(a){return c.cloneDeep(c.toArray(this.prop("ports/items")).find(function(b){return b.id&&b.id===a}))},getPortsPositions:function(a){var b=this._portSettingsData.getGroupPortsMetrics(a,g.Rect(this.size()));return b.reduce(function(a,b){var c=b.portTransformation;return a[b.portId]={x:c.x,y:c.y,angle:c.angle},a},{})},getPortIndex:function(a){var b=c.isObject(a)?a.id:a;return this._isValidPortId(b)?c.toArray(this.prop("ports/items")).findIndex(function(a){return a.id===b}):-1},addPort:function(a,b){if(!c.isObject(a)||Array.isArray(a))throw new Error("Element: addPort requires an object.");var d=c.assign([],this.prop("ports/items"));return d.push(a),this.prop("ports/items",d,b),this},portProp:function(a,b,d,e){var f=this.getPortIndex(a);if(f===-1)throw new Error("Element: unable to find port with id "+a);var g=Array.prototype.slice.call(arguments,1);return Array.isArray(b)?g[0]=["ports","items",f].concat(b):c.isString(b)?g[0]=["ports/items/",f,"/",b].join(""):(g=["ports/items/"+f],c.isPlainObject(b)&&(g.push(b),g.push(d))),this.prop.apply(this,g)},_validatePorts:function(){var b=this.get("ports")||{},d=[];b=b||{};var e=c.toArray(b.items);return e.forEach(function(a){"object"!=typeof a&&d.push("Element: invalid port ",a),this._isValidPortId(a.id)||(a.id=c.uuid())},this),a.util.uniq(e,"id").length!==e.length&&d.push("Element: found id duplicities in ports."),d},_isValidPortId:function(a){return null!==a&&void 0!==a&&!c.isObject(a)},addPorts:function(a,b){return a.length&&this.prop("ports/items",c.assign([],this.prop("ports/items")).concat(a),b),this},removePort:function(a,b){var d=b||{},e=c.assign([],this.prop("ports/items")),f=this.getPortIndex(a);return f!==-1&&(e.splice(f,1),d.rewrite=!0,this.prop("ports/items",e,d)),this},_createPortData:function(){var a=this._validatePorts();if(a.length>0)throw this.set("ports",this.previous("ports")),new Error(a.join(" "));var b;this._portSettingsData&&(b=this._portSettingsData.getPorts()),this._portSettingsData=new d(this.get("ports"));var c=this._portSettingsData.getPorts();if(b){var e=c.filter(function(a){if(!b.find(function(b){return b.id===a.id}))return a}),f=b.filter(function(a){if(!c.find(function(b){return b.id===a.id}))return a});f.length>0&&this.trigger("ports:remove",this,f),e.length>0&&this.trigger("ports:add",this,e)}}}),c.assign(a.dia.ElementView.prototype,{portContainerMarkup:'',portMarkup:'',portLabelMarkup:'',_portElementsCache:null,_initializePorts:function(){this._portElementsCache={},this.listenTo(this.model,"change:ports",function(){this._refreshPorts()})},_refreshPorts:function(){this._removePorts(),this._portElementsCache={},this._renderPorts()},_renderPorts:function(){for(var a=[],b=this._getContainerElement(),d=0,e=b.node.childNodes.length;d1)throw new Error("ElementView: Invalid port markup - multiple roots.");b.attr({port:a.id,"port-group":a.group});var d=V(this.portContainerMarkup).append(b).append(c);return this._portElementsCache[a.id]={portElement:d,portLabelElement:c},d},_updatePortGroup:function(a){for(var b=g.Rect(this.model.size()),c=this.model._portSettingsData.getGroupPortsMetrics(a,b),d=0,e=c.length;d'}),joint.shapes.basic.TextView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"change:attrs",this.resize)}}),joint.shapes.basic.Generic.define("basic.Text",{attrs:{text:{"font-size":18,fill:"#000000"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Circle",{size:{width:60,height:60},attrs:{circle:{fill:"#ffffff",stroke:"#000000",r:30,cx:30,cy:30},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Ellipse",{size:{width:60,height:40},attrs:{ellipse:{fill:"#ffffff",stroke:"#000000",rx:30,ry:20,cx:30,cy:20},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polygon",{size:{width:60,height:40},attrs:{polygon:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polyline",{size:{width:60,height:40},attrs:{polyline:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Image",{attrs:{text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Path",{size:{width:60,height:60},attrs:{path:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle",ref:"path","ref-x":.5,"ref-dy":10,fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Path.define("basic.Rhombus",{attrs:{path:{d:"M 30 0 L 60 30 30 60 0 30 z"},text:{"ref-y":.5,"ref-dy":null,"y-alignment":"middle"}}}),joint.shapes.basic.PortsModelInterface={initialize:function(){this.updatePortsAttrs(),this.on("change:inPorts change:outPorts",this.updatePortsAttrs,this),this.constructor.__super__.constructor.__super__.initialize.apply(this,arguments)},updatePortsAttrs:function(a){if(this._portSelectors){var b=joint.util.omit(this.get("attrs"),this._portSelectors);this.set("attrs",b,{silent:!0})}this._portSelectors=[];var c={};joint.util.toArray(this.get("inPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".inPorts","in");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),joint.util.toArray("outPorts").forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".outPorts","out");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),this.attr(c,{silent:!0}),this.processPorts(),this.trigger("process:ports")},getPortSelector:function(a){var b=".inPorts",c=this.get("inPorts").indexOf(a);if(c<0&&(b=".outPorts",c=this.get("outPorts").indexOf(a),c<0))throw new Error("getPortSelector(): Port doesn't exist.");return b+">g:nth-child("+(c+1)+")>.port-body"}},joint.shapes.basic.PortsViewInterface={initialize:function(){this.listenTo(this.model,"process:ports",this.update),joint.dia.ElementView.prototype.initialize.apply(this,arguments)},update:function(){this.renderPorts(),joint.dia.ElementView.prototype.update.apply(this,arguments)},renderPorts:function(){var a=this.$(".inPorts").empty(),b=this.$(".outPorts").empty(),c=joint.util.template(this.model.portMarkup),d=this.model.ports||[];d.filter(function(a){return"in"===a.type}).forEach(function(b,d){a.append(V(c({id:d,port:b})).node)}),d.filter(function(a){return"out"===a.type}).forEach(function(a,d){b.append(V(c({id:d,port:a})).node)})}},joint.shapes.basic.Generic.define("basic.TextBlock",{attrs:{rect:{fill:"#ffffff",stroke:"#000000",width:80,height:100},text:{fill:"#000000","font-size":14,"font-family":"Arial, helvetica, sans-serif"},".content":{text:"","ref-x":.5,"ref-y":.5,"y-alignment":"middle","x-alignment":"middle"}},content:""},{markup:['','',joint.env.test("svgforeignobject")?'
':'',""].join(""),initialize:function(){this.listenTo(this,"change:size",this.updateSize),this.listenTo(this,"change:content",this.updateContent),this.updateSize(this,this.get("size")),this.updateContent(this,this.get("content")),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateSize:function(a,b){this.attr({".fobj":joint.util.assign({},b),div:{style:joint.util.assign({},b)}})},updateContent:function(a,b){joint.env.test("svgforeignobject")?this.attr({".content":{html:b}}):this.attr({".content":{text:b}})},setForeignObjectSize:function(){this.updateSize.apply(this,arguments)},setDivContent:function(){this.updateContent.apply(this,arguments)}}),joint.shapes.basic.TextBlockView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.noSVGForeignObjectElement=!joint.env.test("svgforeignobject"),joint.env.test("svgforeignobject")||this.listenTo(this.model,"change:content change:size",function(a){this.updateContent(a)})},update:function(a,b){var c=this.model;if(joint.env.test("svgforeignobject"))joint.dia.ElementView.prototype.update.call(this,c,b);else{var d=joint.util.omit(b||c.get("attrs"),".content");joint.dia.ElementView.prototype.update.call(this,c,d),b&&!joint.util.has(b,".content")||this.updateContent(c,b)}},updateContent:function(a,b){var c=joint.util.merge({},(b||a.get("attrs"))[".content"]);c=joint.util.omit(c,"text");var d=joint.util.breakText(a.get("content"),a.get("size"),c,{svgDocument:this.paper.svg}),e=joint.util.setByPath({},".content",c,"/");e[".content"].text=d,joint.dia.ElementView.prototype.update.call(this,a,e)}}),joint.routers.manhattan=function(a,b,c,d){"use strict";function e(a){this.map={},this.options=a,this.mapGridSize=100}function f(){this.items=[],this.hash={},this.values={},this.OPEN=1,this.CLOSE=2}function g(b){return a.point(0===b.x?0:Math.abs(b.x)/b.x,0===b.y?0:Math.abs(b.y)/b.y)}function h(b,c,d,e){for(var f,h=[],i=g(e.difference(c)),j=c;f=b[j];){var k=g(j.difference(f));k.equals(i)||(h.unshift(j),i=k),j=f}var l=g(a.point(j).difference(d));return l.equals(i)||h.unshift(j),h}function i(a,b,c){var e=c.step,f=a.center(),g=d.isObject(c.directionMap)?Object.keys(c.directionMap):[],h=d.toArray(b);return g.reduce(function(b,d){if(h.includes(d)){var g=c.directionMap[d],i=g.x*a.width/2,j=g.y*a.height/2,k=f.clone().offset(i,j);a.containsPoint(k)&&k.offset(g.x*e,g.y*e),b.push(k.snapToGrid(e))}return b},[])}function j(b,c,d){var e=360/d;return Math.floor(a.normalizeAngle(b.theta(c)+e/2)/e)*e}function k(a,b){var c=Math.abs(a-b);return c>180?360-c:c}function l(a,b){for(var c=1/0,d=0,e=b.length;d0&&n.length>0){for(var r=new f,s={},t={},u=0,v=m.length;u0;){var E=r.pop(),F=a.point(E),G=t[E],H=I,I=s[E]?j(s[E],F,B):null!=g.previousDirAngle?g.previousDirAngle:j(o,F,B);if(D.indexOf(E)>=0&&(z=k(I,j(F,p,B)),F.equals(p)||z<180))return g.previousDirAngle=I,h(s,F,o,p);for(u=0;ug.maxAllowedDirectionChange)){var J=F.clone().offset(y.offsetX,y.offsetY),K=J.toString();if(!r.isClose(K)&&e.isPointAccessible(J)){var L=G+y.cost+g.penalties[z];(!r.isOpen(K)||L90){var h=e;e=f,f=h}var i=d%90<45?e:f,j=g.line(a,i),k=90*Math.ceil(d/90),l=g.point.fromPolar(j.squaredLength(),g.toRad(k+135),i),m=g.line(b,l),n=j.intersection(m);return n?[n.round(),b]:[b]}};return function(c,d,e){return joint.routers.manhattan(c,a.assign({},b,d),e)}}(joint.util),joint.routers.normal=function(a,b,c){return a},joint.routers.oneSide=function(a,b,c){var d,e,f,g=b.side||"bottom",h=b.padding||40,i=c.sourceBBox,j=c.targetBBox,k=i.center(),l=j.center();switch(g){case"bottom":f=1,d="y",e="height";break;case"top":f=-1,d="y",e="height";break;case"left":f=-1,d="x",e="width";break;case"right":f=1,d="x",e="width";break;default:throw new Error("Router: invalid side")}return k[d]+=f*(i[e]/2+h),l[d]+=f*(j[e]/2+h),f*(k[d]-l[d])>0?l[d]=k[d]:k[d]=l[d],[k].concat(a,l)},joint.routers.orthogonal=function(a){function b(a,b){return a.x==b.x?a.y>b.y?"N":"S":a.y==b.y?a.x>b.x?"W":"E":null}function c(a,b){return a["W"==b||"E"==b?"width":"height"]}function d(a,b){return g.rect(a).moveAndExpand({x:-b,y:-b,width:2*b,height:2*b})}function e(a){return g.rect(a.x,a.y,0,0)}function f(a,b){var c=Math.min(a.x,b.x),d=Math.min(a.y,b.y),e=Math.max(a.x+a.width,b.x+b.width),f=Math.max(a.y+a.height,b.y+b.height);return g.rect(c,d,e-c,f-d)}function h(a,b,c){var d=g.point(a.x,b.y);return c.containsPoint(d)&&(d=g.point(b.x,a.y)),d}function i(a,c,d){var e=g.point(a.x,c.y),f=g.point(c.x,a.y),h=b(a,e),i=b(a,f),j=o[d],k=h==d||h!=j&&(i==j||i!=d)?e:f;return{points:[k],direction:b(k,c)}}function j(a,c,d){var e=h(a,c,d);return{points:[e],direction:b(e,c)}}function k(d,e,f,i){var j,k={},l=[g.point(d.x,e.y),g.point(e.x,d.y)],m=l.filter(function(a){return!f.containsPoint(a)}),n=m.filter(function(a){return b(a,d)!=i});if(n.length>0)j=n.filter(function(a){return b(d,a)==i}).pop(),j=j||n[0],k.points=[j],k.direction=b(j,e);else{j=a.difference(l,m)[0];var o=g.point(e).move(j,-c(f,i)/2),p=h(o,d,f);k.points=[p,o],k.direction=b(o,e)}return k}function l(a,d,e,f){var h=j(d,a,f),k=h.points[0];if(e.containsPoint(k)){h=j(a,d,e);var l=h.points[0];if(f.containsPoint(l)){var m=g.point(a).move(l,-c(e,b(a,l))/2),n=g.point(d).move(k,-c(f,b(d,k))/2),o=g.line(m,n).midpoint(),p=j(a,o,e),q=i(o,d,p.direction);h.points=[p.points[0],q.points[0]],h.direction=q.direction}}return h}function m(a,c,e,i,j){var k,l,m,n={},o=d(f(e,i),1),q=o.center().distance(c)>o.center().distance(a),r=q?c:a,s=q?a:c;return j?(k=g.point.fromPolar(o.width+o.height,p[j],r),k=o.pointNearestToPoint(k).move(k,-1)):k=o.pointNearestToPoint(r).move(r,1),l=h(k,s,o),k.round().equals(l.round())?(l=g.point.fromPolar(o.width+o.height,g.toRad(k.theta(r))+Math.PI/2,s),l=o.pointNearestToPoint(l).move(s,1).round(),m=h(k,l,o),n.points=q?[l,m,k]:[k,m,l]):n.points=q?[l,k]:[k,l],n.direction=q?b(k,c):b(l,c),n}function n(c,f,h){var n=f.elementPadding||20,o=[],p=d(h.sourceBBox,n),q=d(h.targetBBox,n);c=a.toArray(c).map(g.point),c.unshift(p.center()),c.push(q.center());for(var r,s=0,t=c.length-1;sv)||"jumpover"!==d.name)}),y=x.map(function(a){return r.findViewByModel(a)}),z=d(a,b,f),A=y.map(function(a){return null==a?[]:a===this?z:d(a.sourcePoint,a.targetPoint,a.route)},this),B=z.reduce(function(a,b){var c=x.reduce(function(a,c,d){if(c!==u){var e=g(b,A[d]);a.push.apply(a,e)}return a},[]).sort(function(a,c){return h(b.start,a)-h(b.start,c)});return c.length>0?a.push.apply(a,i(b,c,o)):a.push(b),a},[]);return j(B,o,p)}}(_,g,joint.util),function(a,b,c,d){function e(a,b,d){var e=a.toJSON();return e.angle=b||0,c.util.defaults({},d,e)}function f(a,c,d){return a.map(function(a,b,c){var d=this.pointAt((b+.5)/c.length);return(a.dx||a.dy)&&d.offset(a.dx||0,a.dy||0),e(d.round(),0,a)},b.line(c,d))}function g(a,c,d,f){var g=c.center(),h=c.width/c.height,i=c.topMiddle(),j=b.Ellipse.fromRect(c);return a.map(function(a,b,c){var k=d+f(b,c.length),l=i.clone().rotate(g,-k).scale(h,1,g),m=a.compensateRotation?-j.tangentTheta(l):0;return(a.dx||a.dy)&&l.offset(a.dx||0,a.dy||0),a.dr&&l.move(g,a.dr),e(l.round(),m,a)})}function h(a,c){var e=c.x;d.isString(e)&&(e=parseFloat(e)/100*a.width);var f=c.y;return d.isString(f)&&(f=parseFloat(f)/100*a.height),b.point(e||0,f||0)}c.layout.Port={absolute:function(a,b,c){return a.map(h.bind(null,b))},fn:function(a,b,c){return c.fn(a,b,c)},line:function(a,b,c){var d=h(b,c.start||b.origin()),e=h(b,c.end||b.corner());return f(a,d,e)},left:function(a,b,c){return f(a,b.origin(),b.bottomLeft())},right:function(a,b,c){return f(a,b.topRight(),b.corner())},top:function(a,b,c){return f(a,b.origin(),b.topRight())},bottom:function(a,b,c){return f(a,b.bottomLeft(),b.corner())},ellipseSpread:function(a,b,c){var d=c.startAngle||0,e=c.step||360/a.length;return g(a,b,d,function(a){return a*e})},ellipse:function(a,b,c){var d=c.startAngle||0,e=c.step||20;return g(a,b,d,function(a,b){return(a+.5-b/2)*e})}}}(_,g,joint,joint.util),function(a,b,c,d){function e(a,b){return d.defaultsDeep({},a,b,{x:0,y:0,angle:0,attrs:{".":{y:"0","text-anchor":"start"}}})}function f(a,b,c,f){f=d.defaults({},f,{offset:15});var h,i,j,k,l=b.center().theta(a),m=g(b),n=f.offset,o=0;lm[2]?(j=".3em",h=n,i=0,k="start"):lo[2]?(k=".3em",i=-m,j=0,l="end"):h-270&&i<-90?(g="start",j=i-180):g="end";var m=Math.round;return e({x:m(k.x),y:m(k.y),angle:c?j:0,attrs:{".":{y:l,"text-anchor":g}}})}c.layout.PortLabel={manual:function(a,b,c){return e(c,a)},left:function(a,b,c){return e(c,{x:-15,attrs:{".":{y:".3em","text-anchor":"end"}}})},right:function(a,b,c){return e(c,{x:15,attrs:{".":{y:".3em","text-anchor":"start"}}})},top:function(a,b,c){return e(c,{y:-15,attrs:{".":{"text-anchor":"middle"}}})},bottom:function(a,b,c){return e(c,{y:15,attrs:{".":{y:".6em","text-anchor":"middle"}}})},outsideOriented:function(a,b,c){return f(a,b,!0,c)},outside:function(a,b,c){return f(a,b,!1,c)},insideOriented:function(a,b,c){return h(a,b,!0,c)},inside:function(a,b,c){return h(a,b,!1,c)},radial:function(a,b,c){return i(a.difference(b.center()),!1,c)},radialOriented:function(a,b,c){return i(a.difference(b.center()),!0,c)}}}(_,g,joint,joint.util),joint.highlighters.addClass={className:joint.util.addClassNamePrefix("highlighted"),highlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).addClass(e)},unhighlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).removeClass(e)}},joint.highlighters.opacity={highlight:function(a,b){V(b).addClass(joint.util.addClassNamePrefix("highlight-opacity"))},unhighlight:function(a,b){V(b).removeClass(joint.util.addClassNamePrefix("highlight-opacity"))}},joint.highlighters.stroke={defaultOptions:{padding:3,rx:0,ry:0,attrs:{"stroke-width":3,stroke:"#FEB663"}},_views:{},getHighlighterId:function(a,b){return a.id+JSON.stringify(b)},removeHighlighter:function(a){this._views[a]&&(this._views[a].remove(),this._views[a]=null)},highlight:function(a,b,c){var d=this.getHighlighterId(b,c);if(!this._views[d]){var e,f=joint.util.defaults(c||{},this.defaultOptions),g=V(b);try{var h=g.convertToPathData()}catch(a){e=g.bbox(!0),h=V.rectToPath(joint.util.assign({},f,e))}var i=V("path").attr({d:h,"pointer-events":"none","vector-effect":"non-scaling-stroke",fill:"none"}).attr(f.attrs),j=g.getTransformToElement(a.el),k=f.padding;if(k){e||(e=g.bbox(!0));var l=e.x+e.width/2,m=e.y+e.height/2;e=V.transformRect(e,j);var n=Math.max(e.width,1),o=Math.max(e.height,1),p=(n+k)/n,q=(o+k)/o,r=V.createSVGMatrix({a:p,b:0,c:0,d:q,e:l-p*l,f:m-q*m});j=j.multiply(r)}i.transform(j);var s=this._views[d]=new joint.mvc.View({svgElement:!0,className:"highlight-stroke",el:i.node}),t=this.removeHighlighter.bind(this,d),u=a.model;s.listenTo(u,"remove",t),s.listenTo(u.graph,"reset",t),a.vel.append(i)}},unhighlight:function(a,b,c){this.removeHighlighter(this.getHighlighterId(b,c))}}; +var g=function(){function a(a,b){return b.unshift(null),new(Function.prototype.bind.apply(a,b))}function b(a){var b,c,d=[];for(c=arguments.length,b=1;b=1)return[new q(b,c,d,e),new q(e,e,e,e)];var f=this.getSkeletonPoints(a),g=f.startControlPoint1,h=f.startControlPoint2,i=f.divider,j=f.dividerControlPoint1,k=f.dividerControlPoint2;return[new q(b,g,h,i),new q(i,j,k,e)]},endpointDistance:function(){return this.start.distance(this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.controlPoint1.x===a.controlPoint1.x&&this.controlPoint1.y===a.controlPoint1.y&&this.controlPoint2.x===a.controlPoint2.x&&this.controlPoint2.y===a.controlPoint2.y&&this.end.x===a.end.x&&this.end.y===a.end.y},getSkeletonPoints:function(a){var b=this.start,c=this.controlPoint1,d=this.controlPoint2,e=this.end;if(a<=0)return{startControlPoint1:b.clone(),startControlPoint2:b.clone(),divider:b.clone(),dividerControlPoint1:c.clone(),dividerControlPoint2:d.clone()};if(a>=1)return{startControlPoint1:c.clone(),startControlPoint2:d.clone(),divider:e.clone(),dividerControlPoint1:e.clone(),dividerControlPoint2:e.clone()};var f=new s(b,c).pointAt(a),g=new s(c,d).pointAt(a),h=new s(d,e).pointAt(a),i=new s(f,g).pointAt(a),j=new s(g,h).pointAt(a),k=new s(i,j).pointAt(a),l={startControlPoint1:f,startControlPoint2:i,divider:k,dividerControlPoint1:j,dividerControlPoint2:h};return l},getSubdivisions:function(a){a=a||{};var b=void 0===a.precision?this.PRECISION:a.precision,c=[new q(this.start,this.controlPoint1,this.controlPoint2,this.end)];if(0===b)return c;for(var d=this.endpointDistance(),e=p(10,-b),f=0;;){f+=1;for(var g=[],h=c.length,i=0;i1&&r=1)return this.end.clone();var c=this.tAt(a,b);return this.pointAtT(c)},pointAtLength:function(a,b){var c=this.tAtLength(a,b);return this.pointAtT(c)},pointAtT:function(a){return a<=0?this.start.clone():a>=1?this.end.clone():this.getSkeletonPoints(a).divider},PRECISION:3,scale:function(a,b,c){return this.start.scale(a,b,c),this.controlPoint1.scale(a,b,c),this.controlPoint2.scale(a,b,c),this.end.scale(a,b,c),this},tangentAt:function(a,b){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var c=this.tAt(a,b);return this.tangentAtT(c)},tangentAtLength:function(a,b){if(!this.isDifferentiable())return null;var c=this.tAtLength(a,b);return this.tangentAtT(c)},tangentAtT:function(a){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var b=this.getSkeletonPoints(a),c=b.startControlPoint2,d=b.dividerControlPoint1,e=b.divider,f=new s(c,d);return f.translate(e.x-c.x,e.y-c.y),f},tAt:function(a,b){if(a<=0)return 0;if(a>=1)return 1;b=b||{};var c=void 0===b.precision?this.PRECISION:b.precision,d=void 0===b.subdivisions?this.getSubdivisions({precision:c}):b.subdivisions,e={precision:c,subdivisions:d},f=this.length(e),g=f*a;return this.tAtLength(g,e)},tAtLength:function(a,b){var c=!0;a<0&&(c=!1,a=-a),b=b||{};for(var d,e,f,g,h,i=void 0===b.precision?this.PRECISION:b.precision,j=void 0===b.subdivisions?this.getSubdivisions({precision:i}):b.subdivisions,k={precision:i,subdivisions:j},l=0,m=j.length,n=1/m,o=c?0:m-1;c?o=0;c?o++:o--){var q=j[o],r=q.endpointDistance();if(a<=l+r){d=q,e=o*n,f=(o+1)*n,g=c?a-l:r+l-a,h=c?r+l-a:a-l;break}l+=r}if(!d)return c?1:0;for(var s=this.length(k),t=p(10,-i);;){var u;if(u=0!==s?g/s:0,ui.x+g/2,m=ei.x?f-d:f+d,c=g*g/(e-j)-g*g*(f-k)*(b-k)/(h*h*(e-j))+j):(c=f>i.y?e+d:e-d,b=h*h/(f-k)-h*h*(e-j)*(c-j)/(g*g*(f-k))+k),new u(c,b).theta(a)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLine:function(a){var b=[],c=a.start,d=a.end,e=this.a,f=this.b,g=a.vector(),i=c.difference(new u(this)),j=new u(g.x/(e*e),g.y/(f*f)),k=new u(i.x/(e*e),i.y/(f*f)),l=g.dot(j),m=g.dot(k),n=i.dot(k)-1,o=m*m-l*n;if(o<0)return null;if(o>0){var p=h(o),q=(-m-p)/l,r=(-m+p)/l;if((q<0||10){if(f>d||g>d)return null}else if(f=1?c.clone():b.lerp(c,a)},pointAtLength:function(a){var b=this.start,c=this.end;fromStart=!0,a<0&&(fromStart=!1,a=-a);var d=this.length();return a>=d?fromStart?c.clone():b.clone():this.pointAt((fromStart?a:d-a)/d)},pointOffset:function(a){a=new c.Point(a);var b=this.start,d=this.end,e=(d.x-b.x)*(a.y-b.y)-(d.y-b.y)*(a.x-b.x);return e/this.length()},rotate:function(a,b){return this.start.rotate(a,b),this.end.rotate(a,b),this},round:function(a){var b=p(10,a||0);return this.start.x=l(this.start.x*b)/b,this.start.y=l(this.start.y*b)/b,this.end.x=l(this.end.x*b)/b,this.end.y=l(this.end.y*b)/b,this},scale:function(a,b,c){return this.start.scale(a,b,c),this.end.scale(a,b,c),this},setLength:function(a){var b=this.length();if(!b)return this;var c=a/b;return this.scale(c,c,this.start)},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},tangentAt:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAt(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},tangentAtLength:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAtLength(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},translate:function(a,b){return this.start.translate(a,b),this.end.translate(a,b),this},vector:function(){return new u(this.end.x-this.start.x,this.end.y-this.start.y)},toString:function(){return this.start.toString()+" "+this.end.toString()}},s.prototype.intersection=s.prototype.intersect;var t=c.Path=function(a){if(!(this instanceof t))return new t(a);if("string"==typeof a)return new t.parse(a);this.segments=[];var b,c;if(a){if(Array.isArray(a)&&0!==a.length)if(c=a.length,a[0].isSegment)for(b=0;b=c||a<0)throw new Error("Index out of range.");return b[a]},getSegmentSubdivisions:function(a){var b=this.segments,c=b.length;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=[],f=0;fd||a<0)throw new Error("Index out of range.");var e,f=null,g=null;if(0!==d&&(a>=1?(f=c[a-1],g=f.nextSegment):g=c[0]),Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");for(var h=b.length,i=0;i=d?(e=d-1,f=1):f<0?f=0:f>1&&(f=1),b=b||{};for(var g,h=void 0===b.precision?this.PRECISION:b.precision,i=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:h}):b.segmentSubdivisions,j=0,k=0;k=1)return this.end.clone();b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.pointAtLength(i,g)},pointAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;if(0===a)return this.start.clone();var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isVisible){if(a<=i+m)return k.pointAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f)return e?f.end:f.start;var n=c[d-1];return n.end.clone()},pointAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].pointAtT(0);if(d>=c)return b[c-1].pointAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].pointAtT(e)},prepareSegment:function(a,b,c){a.previousSegment=b,a.nextSegment=c,b&&(b.nextSegment=a),c&&(c.previousSegment=a);var d=a;return a.isSubpathStart&&(a.subpathStartSegment=a,d=c),d&&this.updateSubpathStartSegment(d),a},PRECISION:3,removeSegment:function(a){var b=this.segments,c=b.length;if(0===c)throw new Error("Path has no segments.");if(a<0&&(a=c+a),a>=c||a<0)throw new Error("Index out of range.");var d=b.splice(a,1)[0],e=d.previousSegment,f=d.nextSegment;e&&(e.nextSegment=f),f&&(f.previousSegment=e),d.isSubpathStart&&f&&this.updateSubpathStartSegment(f)},replaceSegment:function(a,b){var c=this.segments,d=c.length;if(0===d)throw new Error("Path has no segments.");if(a<0&&(a=d+a),a>=d||a<0)throw new Error("Index out of range.");var e,f=c[a],g=f.previousSegment,h=f.nextSegment,i=f.isSubpathStart;if(Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");c.splice(a,1);for(var j=b.length,k=0;k1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.segmentIndexAtLength(i,g)},toPoints:function(a){var b=this.segments,c=b.length;if(0===c)return null;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=void 0===a.segmentSubdivisions?this.getSegmentSubdivisions({precision:d}):a.segmentSubdivisions,f=[],g=[],h=0;h0){var k=j.map(function(a){return a.start});Array.prototype.push.apply(g,k)}else g.push(i.start)}else g.length>0&&(g.push(b[h-1].end),f.push(g),g=[])}return g.length>0&&(g.push(this.end),f.push(g)),f},toPolylines:function(a){var b=[],c=this.toPoints(a);if(!c)return null;for(var d=0,e=c.length;d=0;e?j++:j--){var k=c[j],l=g[j],m=k.length({precision:f,subdivisions:l});if(k.isVisible){if(a<=i+m)return j;h=j}i+=m}return h},tangentAt:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;a<0&&(a=0),a>1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.tangentAtLength(i,g)},tangentAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isDifferentiable()){if(a<=i+m)return k.tangentAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f){var n=e?1:0;return f.tangentAtT(n)}return null},tangentAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].tangentAtT(0);if(d>=c)return b[c-1].tangentAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].tangentAtT(e)},translate:function(a,b){for(var c=this.segments,d=c.length,e=0;e=0;c--){var d=a[c];if(d.isVisible)return d.end}return a[b-1].end}});var u=c.Point=function(a,b){if(!(this instanceof u))return new u(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseFloat(c[0]),b=parseFloat(c[1])}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};u.fromPolar=function(a,b,c){c=c&&new u(c)||new u(0,0);var d=e(a*f(b)),h=e(a*g(b)),i=x(z(b));return i<90?h=-h:i<180?(d=-d,h=-h):i<270&&(d=-d),new u(c.x+d,c.y+h)},u.random=function(a,b,c,d){return new u(m(o()*(b-a+1)+a),m(o()*(d-c+1)+c))},u.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=i(j(this.x,a.x),a.x+a.width),this.y=i(j(this.y,a.y),a.y+a.height),this)},bearing:function(a){return new s(this,a).bearing()},changeInAngle:function(a,b,c){return this.clone().offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return new u(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),new u(this.x-(a||0),this.y-(b||0))},distance:function(a){return new s(this,a).length()},squaredDistance:function(a){return new s(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return h(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return e(a.x-this.x)+e(a.y-this.y)},move:function(a,b){var c=A(new u(a).theta(this)),d=this.offset(f(c)*b,-g(c)*b);return d},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return new u(a).move(this,this.distance(a))},rotate:function(a,b){a=a||new c.Point(0,0),b=A(x(-b));var d=f(b),e=g(b),h=d*(this.x-a.x)-e*(this.y-a.y)+a.x,i=e*(this.x-a.x)+d*(this.y-a.y)+a.y;return this.x=h,this.y=i,this},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this},scale:function(a,b,c){return c=c&&new u(c)||new u(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=y(this.x,a),this.y=y(this.y,b||a),this},theta:function(a){a=new u(a);var b=-(a.y-this.y),c=a.x-this.x,d=k(b,c);return d<0&&(d=2*n+d),180*d/n},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=new u(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&new u(a)||new u(0,0);var b=this.x,c=this.y;return this.x=h((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=A(a.theta(new u(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN},lerp:function(a,b){var c=this.x,d=this.y;return new u((1-b)*c+b*a.x,(1-b)*d+b*a.y)}},u.prototype.translate=u.prototype.offset;var v=c.Rect=function(a,b,c,d){return this instanceof v?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new v(a,b,c,d)};v.fromEllipse=function(a){return a=new r(a),new v(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},v.prototype={bbox:function(a){if(!a)return this.clone();var b=A(a||0),c=e(g(b)),d=e(f(b)),h=this.width*d+this.height*c,i=this.width*c+this.height*d;return new v(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return new u(this.x,this.y+this.height)},bottomLine:function(){return new s(this.bottomLeft(),this.bottomRight())},bottomMiddle:function(){ +return new u(this.x+this.width/2,this.y+this.height)},center:function(){return new u(this.x+this.width/2,this.y+this.height/2)},clone:function(){return new v(this)},containsPoint:function(a){return a=new u(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=new v(this).normalize(),c=new v(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return new u(this.x+this.width,this.y+this.height)},equals:function(a){var b=new v(this).normalize(),c=new v(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=j(b.x,d.x),g=j(b.y,d.y);return new v(f,g,i(c.x,e.x)-f,i(c.y,e.y)-g)},intersectionWithLine:function(a){var b,c,d=this,e=[d.topLine(),d.rightLine(),d.bottomLine(),d.leftLine()],f=[],g=[],h=e.length;for(c=0;c0?f:null},intersectionWithLineFromCenterToPoint:function(a,b){a=new u(a);var c,d=new u(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[this.topLine(),this.rightLine(),this.bottomLine(),this.leftLine()],f=new s(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return new s(this.topLeft(),this.bottomLeft())},leftMiddle:function(){return new u(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return u.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return new u(this.x,this.y)},pointNearestToPoint:function(a){if(a=new u(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return new u(this.x+this.width,a.y);case"left":return new u(this.x,a.y);case"bottom":return new u(a.x,this.y+this.height);case"top":return new u(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return new s(this.topRight(),this.bottomRight())},rightMiddle:function(){return new u(this.x+this.width,this.y+this.height/2)},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this.width=l(this.width*b)/b,this.height=l(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(a,b){a=new v(a),b||(b=a.center());var c,d,e,f,g,h,j,k,l=b.x,m=b.y;c=d=e=f=g=h=j=k=1/0;var n=a.topLeft();n.xl&&(d=(this.x+this.width-l)/(o.x-l)),o.y>m&&(h=(this.y+this.height-m)/(o.y-m));var p=a.topRight();p.x>l&&(e=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:i(c,d,e,f),sy:i(g,h,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return i(c.sx,c.sy)},sideNearestToPoint:function(a){a=new u(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cb&&(b=i),jd&&(d=j)}return new v(a,c,b-a,d-c)},clone:function(){var a=this.points,b=a.length;if(0===b)return new w;for(var c=[],d=0;df.x&&(f=c[a]);var g=[];for(a=0;a2){var j=g[g.length-1];g.unshift(j)}for(var k,l,m,n,o,p,q={},r=[];0!==g.length;)if(k=g.pop(),l=k[0],!q.hasOwnProperty(k[0]+"@@"+k[1]))for(var s=!1;!s;)if(r.length<2)r.push(k),s=!0;else{m=r.pop(),n=m[0],o=r.pop(),p=o[0];var t=p.cross(n,l);if(t<0)r.push(o),r.push(m),r.push(k),s=!0;else if(0===t){var u=1e-10,v=n.angleBetween(p,l);e(v-180)2&&r.pop();var x,y=-1;for(b=r.length,a=0;a0){var B=r.slice(y),C=r.slice(0,y);A=B.concat(C)}else A=r;var D=[];for(b=A.length,a=0;a=1)return b[c-1].clone();var d=this.length(),e=d*a;return this.pointAtLength(e)},pointAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return b[0].clone();var d=!0;a<0&&(d=!1,a=-a);for(var e=0,f=c-1,g=d?0:f-1;d?g=0;d?g++:g--){var h=b[g],i=b[g+1],j=new s(h,i),k=h.distance(i);if(a<=e+k)return j.pointAtLength((d?1:-1)*(a-e));e+=k}var l=d?b[c-1]:b[0];return l.clone()},scale:function(a,b,c){var d=this.points,e=d.length;if(0===e)return this;for(var f=0;f1&&(a=1);var d=this.length(),e=d*a;return this.tangentAtLength(e)},tangentAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return null;var d=!0;a<0&&(d=!1,a=-a);for(var e,f=0,g=c-1,h=d?0:g-1;d?h=0;d?h++:h--){var i=b[h],j=b[h+1],k=new s(i,j),l=i.distance(j);if(k.isDifferentiable()){if(a<=f+l)return k.tangentAtLength((d?1:-1)*(a-f));e=k}f+=l}if(e){var m=d?1:0;return e.tangentAt(m)}return null},intersectionWithLine:function(a){for(var b=new s(a),c=[],d=this.points,e=0,f=d.length-1;e0?c:null},translate:function(a,b){var c=this.points,d=c.length;if(0===d)return this;for(var e=0;e=1?b:b*a},pointAtT:function(a){if(this.pointAt)return this.pointAt(a);throw new Error("Neither pointAtT() nor pointAt() function is implemented.")},previousSegment:null,subpathStartSegment:null,tangentAtT:function(a){if(this.tangentAt)return this.tangentAt(a);throw new Error("Neither tangentAtT() nor tangentAt() function is implemented.")},bbox:function(){throw new Error("Declaration missing for virtual function.")},clone:function(){throw new Error("Declaration missing for virtual function.")},closestPoint:function(){throw new Error("Declaration missing for virtual function.")},closestPointLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointNormalizedLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointTangent:function(){throw new Error("Declaration missing for virtual function.")},equals:function(){throw new Error("Declaration missing for virtual function.")},getSubdivisions:function(){throw new Error("Declaration missing for virtual function.")},isDifferentiable:function(){throw new Error("Declaration missing for virtual function.")},length:function(){throw new Error("Declaration missing for virtual function.")},pointAt:function(){throw new Error("Declaration missing for virtual function.")},pointAtLength:function(){throw new Error("Declaration missing for virtual function.")},scale:function(){throw new Error("Declaration missing for virtual function.")},tangentAt:function(){throw new Error("Declaration missing for virtual function.")},tangentAtLength:function(){throw new Error("Declaration missing for virtual function.")},translate:function(){throw new Error("Declaration missing for virtual function.")},serialize:function(){throw new Error("Declaration missing for virtual function.")},toString:function(){throw new Error("Declaration missing for virtual function.")}},C=function(){for(var b=[],c=arguments.length,d=0;d0)throw new Error("Closepath constructor expects no arguments.");return this},J={clone:function(){return new I},getSubdivisions:function(){return[]},isDifferentiable:function(){return!(!this.previousSegment||!this.subpathStartSegment)&&!this.start.equals(this.end)},scale:function(){return this},translate:function(){return this},type:"Z",serialize:function(){return this.type},toString:function(){return this.type+" "+this.start+" "+this.end}};Object.defineProperty(J,"start",{configurable:!0,enumerable:!0,get:function(){if(!this.previousSegment)throw new Error("Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)");return this.previousSegment.end}}),Object.defineProperty(J,"end",{configurable:!0,enumerable:!0,get:function(){if(!this.subpathStartSegment)throw new Error("Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)");return this.subpathStartSegment.end}}),I.prototype=b(B,s.prototype,J);var K=t.segmentTypes={L:C,C:E,M:G,Z:I,z:I};return t.regexSupportedData=new RegExp("^[\\s\\d"+Object.keys(K).join("")+",.]*$"),t.isDataSupported=function(a){return"string"==typeof a&&this.regexSupportedData.test(a)},c}(); +var V,Vectorizer;V=Vectorizer=function(){"use strict";function a(a,b){a||(a={});var c=q("textPath"),d=a.d;if(d&&void 0===a["xlink:href"]){var e=q("path").attr("d",d).appendTo(b.defs());c.attr("xlink:href","#"+e.id)}return q.isObject(a)&&c.attr(a),c.node}function b(a,b,c){c||(c={});for(var d=c.includeAnnotationIndices,e=c.eol,f=c.lineHeight,g=c.baseSize,h=0,i={},j=b.length-1,k=0;k<=j;k++){var l=b[k],m=null;if(q.isObject(l)){var n=l.attrs,o=q("tspan",n),p=o.node,r=l.t;e&&k===j&&(r+=e),p.textContent=r;var s=n.class;s&&o.addClass(s),d&&o.attr("annotations",l.annotations),m=parseFloat(n["font-size"]),void 0===m&&(m=g),m&&m>h&&(h=m)}else e&&k===j&&(l+=e),p=document.createTextNode(l||" "),g&&g>h&&(h=g);a.appendChild(p)}return h&&(i.maxFontSize=h),f?i.lineHeight=f:h&&(i.lineHeight=1.2*h),i}function c(a,b){var c=parseFloat(a);return s.test(a)?c*b:c}function d(a,b,d,e){if(!Array.isArray(b))return 0;var f=b.length;if(!f)return 0;for(var g=b[0],h=c(g.maxFontSize,d)||d,i=0,j=c(e,d),k=1;k1){var e,g,h=[];for(e=0,g=d.childNodes.length;e0&&D.setAttribute("dy",B),(y>0||g)&&D.setAttribute("x",j),D.className.baseVal=C,r.appendChild(D),v+=E.length+1}if(i)if(l)B=d(h,x,p,o);else if("top"===h)B="0.8em";else{var I;switch(z>0?(I=parseFloat(o)||1,I*=z,s.test(o)||(I/=p)):I=0,h){case"middle":B=.3-I/2+"em";break;case"bottom":B=-I-.3+"em"}}else 0===h?B="0em":h?B=h:(B=0,null===this.attr("y")&&this.attr("y",u||"0.8em"));return r.firstChild.setAttribute("dy",B),this.append(r),this},r.removeAttr=function(a){var b=q.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},r.attr=function(a,b){if(q.isUndefined(a)){for(var c=this.node.attributes,d={},e=0;e1&&k.push(k[0]),new g.Polyline(k);case"PATH":return l=this.attr("d"),g.Path.isDataSupported(l)||(l=q.normalizePathData(l)),new g.Path(l);case"LINE":return x1=parseFloat(this.attr("x1"))||0,y1=parseFloat(this.attr("y1"))||0,x2=parseFloat(this.attr("x2"))||0,y2=parseFloat(this.attr("y2"))||0,new g.Line({x:x1,y:y1},{x:x2,y:y2})}return this.getBBox()},r.findIntersection=function(a,b){var c=this.svg().node;b=b||c;var d=this.getBBox({target:b}),e=d.center();if(d.intersectionWithLineFromCenterToPoint(a)){var f,h=this.tagName();if("RECT"===h){var i=new g.Rect(parseFloat(this.attr("x")||0),parseFloat(this.attr("y")||0),parseFloat(this.attr("width")),parseFloat(this.attr("height"))),j=this.getTransformToElement(b),k=q.decomposeMatrix(j),l=c.createSVGTransform();l.setRotate(-k.rotation,e.x,e.y);var m=q.transformRect(i,l.matrix.multiply(j));f=new g.Rect(m).intersectionWithLineFromCenterToPoint(a,k.rotation)}else if("PATH"===h||"POLYGON"===h||"POLYLINE"===h||"CIRCLE"===h||"ELLIPSE"===h){var n,o,p,r,s,t,u="PATH"===h?this:this.convertToPath(),v=u.sample(),w=1/0,x=[];for(n=0;n'+(a||"")+"",c=q.parseXML(b,{async:!1});return c.documentElement},q.idCounter=0,q.uniqueId=function(){return"v-"+ ++q.idCounter},q.toNode=function(a){return q.isV(a)?a.node:a.nodeName&&a||a[0]},q.ensureId=function(a){return a=q.toNode(a),a.id||(a.id=q.uniqueId())},q.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},q.isUndefined=function(a){return"undefined"==typeof a},q.isString=function(a){return"string"==typeof a},q.isObject=function(a){return a&&"object"==typeof a},q.isArray=Array.isArray,q.parseXML=function(a,b){b=b||{};var c;try{var d=new DOMParser;q.isUndefined(b.async)||(d.async=b.async),c=d.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},q.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var b=a.split(":");return{ns:f[b[0]],local:b[1]}}return{ns:null,local:a}},q.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,q.transformSeparatorRegex=/[ ,]+/,q.transformationListRegex=/^(\w+)\((.*)\)/,q.transformStringToMatrix=function(a){var b=q.createSVGMatrix(),c=a&&a.match(q.transformRegex);if(!c)return b;for(var d=0,e=c.length;d=0){var f=q.transformStringToMatrix(a),g=q.decomposeMatrix(f);b=[g.translateX,g.translateY],d=[g.scaleX,g.scaleY],c=[g.rotation];var h=[];0===b[0]&&0===b[0]||h.push("translate("+b+")"),1===d[0]&&1===d[1]||h.push("scale("+d+")"),0!==c[0]&&h.push("rotate("+c+")"),a=h.join(" ")}else{var i=a.match(/translate\((.*?)\)/);i&&(b=i[1].split(e));var j=a.match(/rotate\((.*?)\)/);j&&(c=j[1].split(e));var k=a.match(/scale\((.*?)\)/);k&&(d=k[1].split(e))}}var l=d&&d[0]?parseFloat(d[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:l,sy:d&&d[1]?parseFloat(d[1]):l}}},q.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},q.decomposeMatrix=function(a){var b=q.deltaTransformPoint(a,{x:0,y:1}),c=q.deltaTransformPoint(a,{x:1,y:0}),d=180/j*k(b.y,b.x)-90,e=180/j*k(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:l(a.a*a.a+a.b*a.b),scaleY:l(a.c*a.c+a.d*a.d),skewX:d,skewY:e,rotation:d}},q.matrixToScale=function(a){var b,c,d,e;return a?(b=q.isUndefined(a.a)?1:a.a,e=q.isUndefined(a.d)?1:a.d,c=a.b,d=a.c):b=e=1,{sx:c?l(b*b+c*c):b,sy:d?l(d*d+e*e):e}},q.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=q.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(k(b.y,b.x))-90)}},q.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},q.isV=function(a){return a instanceof q},q.isVElement=q.isV;var t=q("svg").node;return q.createSVGMatrix=function(a){var b=t.createSVGMatrix();for(var c in a)b[c]=a[c];return b},q.createSVGTransform=function(a){return q.isUndefined(a)?t.createSVGTransform():(a instanceof SVGMatrix||(a=q.createSVGMatrix(a)),t.createSVGTransformFromMatrix(a))},q.createSVGPoint=function(a,b){var c=t.createSVGPoint();return c.x=a,c.y=b,c},q.transformRect=function(a,b){var c=t.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var e=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var f=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var h=c.matrixTransform(b),i=m(d.x,e.x,f.x,h.x),j=n(d.x,e.x,f.x,h.x),k=m(d.y,e.y,f.y,h.y),l=n(d.y,e.y,f.y,h.y);return new g.Rect(i,k,j-i,l-k)},q.transformPoint=function(a,b){return new g.Point(q.createSVGPoint(a.x,a.y).matrixTransform(b))},q.transformLine=function(a,b){return new g.Line(q.transformPoint(a.start,b),q.transformPoint(a.end,b))},q.transformPolyline=function(a,b){var c=a instanceof g.Polyline?a.points:a;q.isArray(c)||(c=[]);for(var d=[],e=0,f=c.length;e=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L"+f*q+","+f*r+"A"+f+","+f+" 0 "+l+",0 "+f*m+","+f*n+"Z":"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L0,0Z"},q.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?q.isObject(a[c])&&q.isObject(b[c])?a[c]=q.mergeAttrs(a[c],b[c]):q.isObject(a[c])?a[c]=q.mergeAttrs(a[c],q.styleToObject(b[c])):q.isObject(b[c])?a[c]=q.mergeAttrs(q.styleToObject(a[c]),b[c]):a[c]=q.mergeAttrs(q.styleToObject(a[c]),q.styleToObject(b[c])):a[c]=b[c];return a},q.annotateString=function(a,b,c){b=b||[],c=c||{};for(var d,e,f,g=c.offset||0,h=[],i=[],j=0;j=m&&j=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},q.convertLineToPathData=function(a){a=q(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},q.convertPolygonToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)+" Z"},q.convertPolylineToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)},q.svgPointsToPath=function(a){for(var b=0,c=a.length;b1&&(z=o(z),d*=z,e*=z);var A=d*d,B=e*e,C=(g==h?-1:1)*o(p((A*B-A*y*y-B*x*x)/(A*y*y+B*x*x))),D=C*d*y/e+(a+i)/2,E=C*-e*x/d+(c+q)/2,F=n(((c-E)/e).toFixed(9)),G=n(((q-E)/e).toFixed(9));F=aG&&(F-=2*j),(!h&&G)>F&&(G-=2*j)}var H=G-F;if(p(H)>t){var I=G,J=i,K=q;G=F+t*((h&&G)>F?1:-1),i=D+d*l(G),q=E+e*k(G),v=b(i,q,d,e,f,0,h,J,K,[G,I,D,E])}H=G-F;var L=l(F),M=k(F),N=l(G),O=k(G),P=m(H/4),Q=4/3*(d*P),R=4/3*(e*P),S=[a,c],T=[a+Q*M,c-R*L],U=[i+Q*O,q-R*N],V=[i,q];if(T[0]=2*S[0]-T[0],T[1]=2*S[1]-T[1],r)return[T,U,V].concat(v);v=[T,U,V].concat(v).join().split(",");for(var W=[],X=v.length,Y=0;Y2&&(c.push([d].concat(f.splice(0,2))),g="l",d="m"===d?"l":"L");f.length>=b[g]&&(c.push([d].concat(f.splice(0,b[g]))),b[g]););}),c}function d(a){if(Array.isArray(a)&&Array.isArray(a&&a[0])||(a=c(a)),!a||!a.length)return[["M",0,0]];for(var b,d=[],e=0,f=0,g=0,h=0,i=0,j=a.length,k=i;k7){a[b].shift();for(var c=a[b];c.length;)i[b]="A",a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),l=g.length}}for(var g=d(c),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i=[],j="",k="",l=g.length,m=0;m0&&(k=i[m-1])),g[m]=e(g[m],h,k),"A"!==i[m]&&"C"===j&&(i[m]="C"),f(g,m);var n=g[m],o=n.length;h.x=n[o-2],h.y=n[o-1],h.bx=parseFloat(n[o-4])||h.x,h.by=parseFloat(n[o-3])||h.y}return g[0][0]&&"M"===g[0][0]||g.unshift(["M",0,0]),g}var f="\t\n\v\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029",g=new RegExp("([a-z])["+f+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+f+"]*,?["+f+"]*)+)","ig"),h=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+f+"]*,?["+f+"]*","ig"),i=Math,j=i.PI,k=i.sin,l=i.cos,m=i.tan,n=i.asin,o=i.sqrt,p=i.abs;return function(a){return e(a).join(",").split(",").join(" ")}}(),q.namespace=f,q}(); +var joint={version:"2.1.0",config:{classNamePrefix:"joint-",defaultTheme:"default"},dia:{},ui:{},layout:{},shapes:{},format:{},connectors:{},highlighters:{},routers:{},anchors:{},connectionPoints:{},connectionStrategies:{},linkTools:{},mvc:{views:{}},setTheme:function(a,b){b=b||{},joint.util.invoke(joint.mvc.views,"setTheme",a,b),joint.mvc.View.prototype.defaultTheme=a},env:{_results:{},_tests:{svgforeignobject:function(){return!!document.createElementNS&&/SVGForeignObject/.test({}.toString.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))}},addTest:function(a,b){return joint.env._tests[a]=b},test:function(a){var b=joint.env._tests[a];if(!b)throw new Error('Test not defined ("'+a+'"). Use `joint.env.addTest(name, fn) to add a new test.`');var c=joint.env._results[a];if("undefined"!=typeof c)return c;try{c=b()}catch(a){c=!1}return joint.env._results[a]=c,c}},util:{hashCode:function(a){var b=0;if(0==a.length)return b;for(var c=0;c0){var f=joint.util.getByPath(a,d,c);f&&delete f[e]}else delete a[e];return a},flattenObject:function(a,b,c){b=b||"/";var d={};for(var e in a)if(a.hasOwnProperty(e)){var f="object"==typeof a[e];if(f&&c&&c(a[e])&&(f=!1),f){var g=this.flattenObject(a[e],b,c);for(var h in g)g.hasOwnProperty(h)&&(d[e+b+h]=g[h])}else d[e]=a[e]}return d},uuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"==a?b:3&b|8;return c.toString(16)})},guid:function(a){return this.guid.id=this.guid.id||1,a.id=void 0===a.id?"j_"+this.guid.id++:a.id,a.id},toKebabCase:function(a){return a.replace(/[A-Z]/g,"-$&").toLowerCase()},mixin:_.assign,supplement:_.defaults,deepMixin:_.mixin,deepSupplement:_.defaultsDeep,normalizeEvent:function(a){var b=a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0];if(b){for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return a},nextFrame:function(){var a;if("undefined"!=typeof window&&(a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame),!a){var b=0;a=function(a){var c=(new Date).getTime(),d=Math.max(0,16-(c-b)),e=setTimeout(function(){a(c+d)},d);return b=c+d,e}}return function(b,c){return a(c?b.bind(c):b)}}(),cancelFrame:function(){var a,b="undefined"!=typeof window;return b&&(a=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame),a=a||clearTimeout,b?a.bind(window):a}(),shapePerimeterConnectionPoint:function(a,b,c,d){var e,f;if(!c){var g=b.$(".scalable")[0],h=b.$(".rotatable")[0];g&&g.firstChild?c=g.firstChild:h&&h.firstChild&&(c=h.firstChild)}return c?(f=V(c).findIntersection(d,a.paper.viewport),f||(e=V(c).getBBox({target:a.paper.viewport}))):(e=b.model.getBBox(),f=e.intersectionWithLineFromCenterToPoint(d)),f||e.center()},isPercentage:function(a){return joint.util.isString(a)&&"%"===a.slice(-1)},parseCssNumeric:function(a,b){b=b||[];var c={value:parseFloat(a)};if(Number.isNaN(c.value))return null;var d=b.join("|");if(joint.util.isString(a)){var e=new RegExp("(\\d+)("+d+")$").exec(a);if(!e)return null;e[2]&&(c.unit=e[2])}return c},breakText:function(a,b,c,d){d=d||{},c=c||{};var e=b.width,f=b.height,g=d.svgDocument||V("svg").node,h=V("tspan").node,i=V("text").attr(c).append(h).node,j=document.createTextNode("");i.style.opacity=0,i.style.display="block",h.style.display="block",h.appendChild(j),g.appendChild(i),d.svgDocument||document.body.appendChild(g);for(var k,l,m=d.separator||" ",n=d.eol||"\n",o=a.split(m),p=[],q=[],r=0,s=0,t=o.length;r=0)if(u.length>1){for(var v=u.split(n),w=0,x=v.length-1;wf){q.splice(Math.floor(f/l));break}}}}return d.svgDocument?g.removeChild(i):document.body.removeChild(g),q.join(n)},sanitizeHTML:function(a){var b=$($.parseHTML("
"+a+"
",null,!1));return b.find("*").each(function(){var a=this;$.each(a.attributes,function(){var b=this,c=b.name,d=b.value;0!==c.indexOf("on")&&0!==d.indexOf("javascript:")||$(a).removeAttr(c)})}),b.html()},downloadBlob:function(a,b){if(window.navigator.msSaveBlob)window.navigator.msSaveBlob(a,b);else{var c=window.URL.createObjectURL(a),d=document.createElement("a");d.href=c,d.download=b,document.body.appendChild(d),d.click(),document.body.removeChild(d),window.URL.revokeObjectURL(c)}},downloadDataUri:function(a,b){var c=joint.util.dataUriToBlob(a);joint.util.downloadBlob(c,b)},dataUriToBlob:function(a){a=a.replace(/\s/g,""),a=decodeURIComponent(a);var b,c=a.indexOf(","),d=a.slice(0,c),e=d.split(":")[1].split(";")[0],f=a.slice(c+1);b=d.indexOf("base64")>=0?atob(f):unescape(encodeURIComponent(f));for(var g=new window.Uint8Array(b.length),h=0;h=1)return 1;var b=a*a,c=b*a;return 4*(a<.5?c:3*(a-b)+c-.75)},exponential:function(a){return Math.pow(2,10*(a-1))},bounce:function(a){for(var b=0,c=1;1;b+=c,c/=2)if(a>=(7-4*b)/11){var d=(11-6*b-11*a)/4;return-d*d+c*c}},reverse:function(a){return function(b){return 1-a(1-b)}},reflect:function(a){return function(b){return.5*(b<.5?a(2*b):2-a(2-2*b))}},clamp:function(a,b,c){return b=b||0,c=c||1,function(d){var e=a(d);return ec?c:e}},back:function(a){return a||(a=1.70158),function(b){return b*b*((a+1)*b-a)}},elastic:function(a){return a||(a=1.5),function(b){return Math.pow(2,10*(b-1))*Math.cos(20*Math.PI*a/3*b)}}},interpolate:{number:function(a,b){var c=b-a;return function(b){return a+c*b}},object:function(a,b){var c=Object.keys(a);return function(d){var e,f,g={};for(e=c.length-1;e!=-1;e--)f=c[e],g[f]=a[f]+(b[f]-a[f])*d;return g}},hexColor:function(a,b){var c=parseInt(a.slice(1),16),d=parseInt(b.slice(1),16),e=255&c,f=(255&d)-e,g=65280&c,h=(65280&d)-g,i=16711680&c,j=(16711680&d)-i;return function(a){var b=e+f*a&255,c=g+h*a&65280,d=i+j*a&16711680;return"#"+(1<<24|b|c|d).toString(16).slice(1)}},unit:function(a,b){var c=/(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/,d=c.exec(a),e=c.exec(b),f=e[1].indexOf("."),g=f>0?e[1].length-f-1:0;a=+d[1];var h=+e[1]-a,i=d[2];return function(b){return(a+h*b).toFixed(g)+i}}},filter:{outline:function(a){var b='',c=Number.isFinite(a.margin)?a.margin:2,d=Number.isFinite(a.width)?a.width:1;return joint.util.template(b)({color:a.color||"blue",opacity:Number.isFinite(a.opacity)?a.opacity:1,outerRadius:c+d,innerRadius:c})},highlight:function(a){var b='';return joint.util.template(b)({color:a.color||"red",width:Number.isFinite(a.width)?a.width:1,blur:Number.isFinite(a.blur)?a.blur:0,opacity:Number.isFinite(a.opacity)?a.opacity:1})},blur:function(a){var b=Number.isFinite(a.x)?a.x:2;return joint.util.template('')({stdDeviation:Number.isFinite(a.y)?[b,a.y]:b})},dropShadow:function(a){var b="SVGFEDropShadowElement"in window?'':'';return joint.util.template(b)({dx:a.dx||0,dy:a.dy||0,opacity:Number.isFinite(a.opacity)?a.opacity:1,color:a.color||"black",blur:Number.isFinite(a.blur)?a.blur:4})},grayscale:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},sepia:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},saturate:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:1-b})},hueRotate:function(a){return joint.util.template('')({angle:a.angle||0})},invert:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:1-b})},brightness:function(a){return joint.util.template('')({amount:Number.isFinite(a.amount)?a.amount:1})},contrast:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:.5-b/2})}},format:{number:function(a,b,c){function d(a){for(var b=a.length,d=[],e=0,f=c.grouping[0];b>0&&f>0;)d.push(a.substring(b-=f,b+f)),f=c.grouping[e=(e+1)%c.grouping.length];return d.reverse().join(c.thousands)}c=c||{currency:["$",""],decimal:".",thousands:",",grouping:[3]};var e=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,f=e.exec(a),g=f[1]||" ",h=f[2]||">",i=f[3]||"",j=f[4]||"",k=f[5],l=+f[6],m=f[7],n=f[8],o=f[9],p=1,q="",r="",s=!1;switch(n&&(n=+n.substring(1)),(k||"0"===g&&"="===h)&&(k=g="0",h="=",m&&(l-=Math.floor((l-1)/4))),o){case"n":m=!0,o="g";break;case"%":p=100,r="%",o="f";break;case"p":p=100,r="%",o="r";break;case"b":case"o":case"x":case"X":"#"===j&&(q="0"+o.toLowerCase());break;case"c":case"d":s=!0,n=0;break;case"s":p=-1,o="r"}"$"===j&&(q=c.currency[0],r=c.currency[1]),"r"!=o||n||(o="g"),null!=n&&("g"==o?n=Math.max(1,Math.min(21,n)):"e"!=o&&"f"!=o||(n=Math.max(0,Math.min(20,n))));var t=k&&m;if(s&&b%1)return"";var u=b<0||0===b&&1/b<0?(b=-b,"-"):i,v=r;if(p<0){var w=this.prefix(b,n);b=w.scale(b),v=w.symbol+r}else b*=p;b=this.convert(o,b,n);var x=b.lastIndexOf("."),y=x<0?b:b.substring(0,x),z=x<0?"":c.decimal+b.substring(x+1);!k&&m&&c.grouping&&(y=d(y));var A=q.length+y.length+z.length+(t?0:u.length),B=A"===h?B+u+b:"^"===h?B.substring(0,A>>=1)+u+b+B.substring(A):u+(t?b:B+b))+v},string:function(a,b){for(var c,d="{",e=!1,f=[];(c=a.indexOf(d))!==-1;){var g,h,i;if(g=a.slice(0,c),e){h=g.split(":"),i=h.shift().split("."),g=b;for(var j=0;j8?function(a){return a/c}:function(a){return a*c},symbol:a}}),d=0;return a&&(a<0&&(a*=-1),b&&(a=this.round(a,this.precision(a,b))),d=1+Math.floor(1e-12+Math.log(a)/Math.LN10),d=Math.max(-24,Math.min(24,3*Math.floor((d<=0?d+1:d-1)/3)))),c[8+d/3]}},template:function(a){var b=/<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g;return function(c){return c=c||{},a.replace(b,function(a){for(var b=Array.from(arguments),d=b.slice(1,4).find(function(a){return!!a}),e=d.split("."),f=c[e.shift()];void 0!==f&&e.length;)f=f[e.shift()];return void 0!==f?f:""})}},toggleFullScreen:function(a){function b(a,b){for(var c=["webkit","moz","ms","o",""],d=0;d0&&b[0]||[],e=c>1&&b[c-1]||{};return Array.isArray(d)||(e instanceof joint.dia.Cell?d=b:d instanceof joint.dia.Cell&&(b.length>1&&b.pop(),d=b)),e instanceof joint.dia.Cell&&(e={}),a.call(this,d,e)}}},parseDOMJSON:function(a,b){for(var c={},d=V.namespace.xmlns,e=b||d,f=document.createDocumentFragment(),g=[a,f,e];g.length>0;){e=g.pop();for(var h=g.pop(),i=g.pop(),j=0,k=i.length;j0);return this.stopBatch("clear"),this},_prepareCell:function(a,b){var c;if(a instanceof Backbone.Model?(c=a.attributes,a.graph||b&&b.dry||(a.graph=this)):c=a,!joint.util.isString(c.type))throw new TypeError("dia.Graph: cell type must be a string.");return a},minZIndex:function(){var a=this.get("cells").first();return a?a.get("z")||0:0},maxZIndex:function(){var a=this.get("cells").last();return a?a.get("z")||0:0},addCell:function(a,b){return Array.isArray(a)?this.addCells(a,b):(a instanceof Backbone.Model?a.has("z")||a.set("z",this.maxZIndex()+1):void 0===a.z&&(a.z=this.maxZIndex()+1),this.get("cells").add(this._prepareCell(a,b),b||{}),this)},addCells:function(a,b){return a.length&&(a=joint.util.flattenDeep(a),b.position=a.length,this.startBatch("add"),a.forEach(function(a){b.position--,this.addCell(a,b)},this),this.stopBatch("add")),this},resetCells:function(a,b){var c=joint.util.toArray(a).map(function(a){return this._prepareCell(a,b)},this);return this.get("cells").reset(c,b),this},removeCells:function(a,b){return a.length&&(this.startBatch("remove"),joint.util.invoke(a,"remove",b),this.stopBatch("remove")),this},_removeCell:function(a,b,c){c=c||{},c.clear||(c.disconnectLinks?this.disconnectLinks(a,c):this.removeLinks(a,c)),this.get("cells").remove(a,{silent:!0}),a.graph===this&&(a.graph=null)},getCell:function(a){return this.get("cells").get(a)},getCells:function(){return this.get("cells").toArray()},getElements:function(){return Object.keys(this._nodes).map(this.getCell,this)},getLinks:function(){return Object.keys(this._edges).map(this.getCell,this)},getFirstCell:function(){return this.get("cells").first()},getLastCell:function(){return this.get("cells").last()},getConnectedLinks:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=[],f={};if(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),b.deep){var g=a.getEmbeddedCells({deep:!0}),h={};g.forEach(function(a){a.isLink()&&(h[a.id]=!0)}),g.forEach(function(a){a.isLink()||(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)))},this)}return e},getNeighbors:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=this.getConnectedLinks(a,b).reduce(function(e,f){var g=f.get("source"),h=f.get("target"),i=f.hasLoop(b);if(c&&joint.util.has(g,"id")&&!e[g.id]){var j=this.getCell(g.id);!i&&(!j||j===a||b.deep&&j.isEmbeddedIn(a))||(e[g.id]=j)}if(d&&joint.util.has(h,"id")&&!e[h.id]){var k=this.getCell(h.id);!i&&(!k||k===a||b.deep&&k.isEmbeddedIn(a))||(e[h.id]=k)}return e}.bind(this),{});return joint.util.toArray(e)},getCommonAncestor:function(){var a=Array.from(arguments).map(function(a){for(var b=[],c=a.get("parent");c;)b.push(c),c=this.getCell(c).get("parent");return b},this);a=a.sort(function(a,b){return a.length-b.length});var b=joint.util.toArray(a.shift()).find(function(b){return a.every(function(a){return a.includes(b)})});return this.getCell(b)},getSuccessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{outbound:!0})),c},cloneCells:function(a){a=joint.util.uniq(a);var b=joint.util.toArray(a).reduce(function(a,b){return a[b.id]=b.clone(),a},{});return joint.util.toArray(a).forEach(function(a){var c=b[a.id];if(c.isLink()){var d=c.get("source"),e=c.get("target");d.id&&b[d.id]&&c.prop("source/id",b[d.id].id),e.id&&b[e.id]&&c.prop("target/id",b[e.id].id)}var f=a.get("parent");f&&b[f]&&c.set("parent",b[f].id);var g=joint.util.toArray(a.get("embeds")).reduce(function(a,c){return b[c]&&a.push(b[c].id),a},[]);joint.util.isEmpty(g)||c.set("embeds",g)}),b},cloneSubgraph:function(a,b){var c=this.getSubgraph(a,b);return this.cloneCells(c)},getSubgraph:function(a,b){b=b||{};var c=[],d={},e=[],f=[];return joint.util.toArray(a).forEach(function(a){if(d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a)),b.deep){var g=a.getEmbeddedCells({deep:!0});g.forEach(function(a){d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a))})}}),f.forEach(function(a){var b=a.get("source"),f=a.get("target");if(b.id&&!d[b.id]){var g=this.getCell(b.id);c.push(g),d[g.id]=g,e.push(g)}if(f.id&&!d[f.id]){var h=this.getCell(f.id);c.push(this.getCell(f.id)), +d[h.id]=h,e.push(h)}},this),e.forEach(function(a){var e=this.getConnectedLinks(a,b);e.forEach(function(a){var b=a.get("source"),e=a.get("target");!d[a.id]&&b.id&&d[b.id]&&e.id&&d[e.id]&&(c.push(a),d[a.id]=a)})},this),c},getPredecessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{inbound:!0})),c},search:function(a,b,c){c=c||{},c.breadthFirst?this.bfs(a,b,c):this.dfs(a,b,c)},bfs:function(a,b,c){c=c||{};var d={},e={},f=[];for(f.push(a),e[a.id]=0;f.length>0;){var g=f.shift();if(!d[g.id]){if(d[g.id]=!0,b(g,e[g.id])===!1)return;this.getNeighbors(g,c).forEach(function(a){e[a.id]=e[g.id]+1,f.push(a)})}}},dfs:function(a,b,c,d,e){c=c||{};var f=d||{},g=e||0;b(a,g)!==!1&&(f[a.id]=!0,this.getNeighbors(a,c).forEach(function(a){f[a.id]||this.dfs(a,b,c,f,g+1)},this))},getSources:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._in[c]&&!joint.util.isEmpty(this._in[c])||a.push(this.getCell(c))}.bind(this)),a},getSinks:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._out[c]&&!joint.util.isEmpty(this._out[c])||a.push(this.getCell(c))}.bind(this)),a},isSource:function(a){return!this._in[a.id]||joint.util.isEmpty(this._in[a.id])},isSink:function(a){return!this._out[a.id]||joint.util.isEmpty(this._out[a.id])},isSuccessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{outbound:!0}),c},isPredecessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{inbound:!0}),c},isNeighbor:function(a,b,c){c=c||{};var d=c.inbound,e=c.outbound;void 0===d&&void 0===e&&(d=e=!0);var f=!1;return this.getConnectedLinks(a,c).forEach(function(a){var c=a.get("source"),g=a.get("target");return d&&joint.util.has(c,"id")&&c.id===b.id?(f=!0,!1):e&&joint.util.has(g,"id")&&g.id===b.id?(f=!0,!1):void 0}),f},disconnectLinks:function(a,b){this.getConnectedLinks(a).forEach(function(c){c.set(c.get("source").id===a.id?"source":"target",{x:0,y:0},b)})},removeLinks:function(a,b){joint.util.invoke(this.getConnectedLinks(a),"remove",b)},findModelsFromPoint:function(a){return this.getElements().filter(function(b){return b.getBBox().containsPoint(a)})},findModelsInArea:function(a,b){a=g.rect(a),b=joint.util.defaults(b||{},{strict:!1});var c=b.strict?"containsRect":"intersect";return this.getElements().filter(function(b){return a[c](b.getBBox())})},findModelsUnderElement:function(a,b){b=joint.util.defaults(b||{},{searchBy:"bbox"});var c=a.getBBox(),d="bbox"===b.searchBy?this.findModelsInArea(c):this.findModelsFromPoint(c[b.searchBy]());return d.filter(function(b){return a.id!==b.id&&!b.isEmbeddedIn(a)})},getBBox:function(a,b){return this.getCellsBBox(a||this.getElements(),b)},getCellsBBox:function(a,b){return joint.util.toArray(a).reduce(function(a,c){return c.isLink()?a:a?a.union(c.getBBox(b)):c.getBBox(b)},null)},translate:function(a,b,c){var d=this.getCells().filter(function(a){return!a.isEmbedded()});return joint.util.invoke(d,"translate",a,b,c),this},resize:function(a,b,c){return this.resizeCells(a,b,this.getCells(),c)},resizeCells:function(a,b,c,d){var e=this.getCellsBBox(c);if(e){var f=Math.max(a/e.width,0),g=Math.max(b/e.height,0);joint.util.invoke(c,"scale",f,g,e.origin(),d)}return this},startBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)+1,this.trigger("batch:start",joint.util.assign({},b,{batchName:a}))},stopBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)-1,this.trigger("batch:stop",joint.util.assign({},b,{batchName:a}))},hasActiveBatch:function(a){return 0===arguments.length?joint.util.toArray(this._batches).some(function(a){return a>0}):Array.isArray(a)?a.some(function(a){return!!this._batches[a]},this):!!this._batches[a]}},{validations:{multiLinks:function(a,b){var c=b.get("source"),d=b.get("target");if(c.id&&d.id){var e=b.getSourceElement();if(e){var f=a.getConnectedLinks(e,{outbound:!0}),g=f.filter(function(a){var b=a.get("source"),e=a.get("target");return b&&b.id===c.id&&(!b.port||b.port===c.port)&&e&&e.id===d.id&&(!e.port||e.port===d.port)});if(g.length>1)return!1}}return!0},linkPinning:function(a,b){return b.source().id&&b.target().id}}}),joint.util.wrapWith(joint.dia.Graph.prototype,["resetCells","addCells","removeCells"],"cells"),function(a,b,c,d,e){function f(a,b){return function(c,d){var f=e.isPercentage(c);c=parseFloat(c),f&&(c/=100);var g={};if(isFinite(c)){var h=f||c>=0&&c<=1?c*d[b]:Math.max(c+d[b],0);g[a]=h}return g}}function g(a,b,d){return function(f,g){var h=e.isPercentage(f);f=parseFloat(f),h&&(f/=100);var i;if(isFinite(f)){var j=g[d]();i=h||f>0&&f<1?j[a]+g[b]*f:j[a]+f}var k=c.Point();return k[a]=i||0,k}}function h(a,b,d){return function(f,g){var h;h="middle"===f?g[b]/2:f===d?g[b]:isFinite(f)?f>-1&&f<1?-g[b]*f:-f:e.isPercentage(f)?g[b]*parseFloat(f)/100:0;var i=c.Point();return i[a]=-(g[a]+h),i}}function i(a,b){var c="joint-shape",e=b&&b.resetOffset;return function(b,f,g){var h=d(g),i=h.data(c);if(!i||i.value!==b){var j=a(b);i={value:b,shape:j,shapeBBox:j.bbox()},h.data(c,i)}var k=i.shape.clone(),l=i.shapeBBox.clone(),m=l.origin(),n=f.origin();l.x=n.x,l.y=n.y;var o=f.maxRectScaleToFit(l,n),p=0===l.width||0===f.width?1:o.sx,q=0===l.height||0===f.height?1:o.sy;return k.scale(p,q,m),e&&k.translate(-m.x,-m.y),k}}function j(a){function d(a){return new c.Path(b.normalizePathData(a))}var e=i(d,a);return function(a,b,c){var d=e(a,b,c);return{d:d.serialize()}}}function k(a){var b=i(c.Polyline,a);return function(a,c,d){var e=b(a,c,d);return{points:e.serialize()}}}function l(a,b){var d=new c.Point(1,0);return function(c){var e,f,g=this[a](c);return g?(f=b.rotate?g.vector().vectorAngle(d):0,e=g.start):(e=this.path.start,f=0),0===f?{transform:"translate("+e.x+","+e.y+")"}:{transform:"translate("+e.x+","+e.y+") rotate("+f+")"}}}function m(a,b,c){return void 0!==c.text}function n(){return this instanceof a.dia.LinkView}function o(a){var b={},c=a.stroke;"string"==typeof c&&(b.stroke=c,b.fill=c);var d=a.strokeOpacity;return void 0===d&&(d=a["stroke-opacity"]),void 0===d&&(d=a.opacity),void 0!==d&&(b["stroke-opacity"]=d,b["fill-opacity"]=d),b}var p=a.dia.attributes={xlinkHref:{set:"xlink:href"},xlinkShow:{set:"xlink:show"},xlinkRole:{set:"xlink:role"},xlinkType:{set:"xlink:type"},xlinkArcrole:{set:"xlink:arcrole"},xlinkTitle:{set:"xlink:title"},xlinkActuate:{set:"xlink:actuate"},xmlSpace:{set:"xml:space"},xmlBase:{set:"xml:base"},xmlLang:{set:"xml:lang"},preserveAspectRatio:{set:"preserveAspectRatio"},requiredExtension:{set:"requiredExtension"},requiredFeatures:{set:"requiredFeatures"},systemLanguage:{set:"systemLanguage"},externalResourcesRequired:{set:"externalResourceRequired"},filter:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineFilter(a)+")"}},fill:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},stroke:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},sourceMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),a),{"marker-start":"url(#"+this.paper.defineMarker(a)+")"}}},targetMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),{transform:"rotate(180)"},a),{"marker-end":"url(#"+this.paper.defineMarker(a)+")"}}},vertexMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),a),{"marker-mid":"url(#"+this.paper.defineMarker(a)+")"}}},text:{qualify:function(a,b,c){return!c.textWrap||!e.isPlainObject(c.textWrap)},set:function(c,f,g,h){var i=d(g),j="joint-text",k=i.data(j),l=a.util.pick(h,"lineHeight","annotations","textPath","x","textVerticalAnchor","eol"),m=l.fontSize=h["font-size"]||h.fontSize,n=JSON.stringify([c,l]);if(void 0===k||k!==n){m&&g.setAttribute("font-size",m);var o=l.textPath;if(e.isObject(o)){var p=o.selector;if("string"==typeof p){var q=this.findBySelector(p)[0];q instanceof SVGPathElement&&(l.textPath=e.assign({"xlink:href":"#"+q.id},o))}}b(g).text(""+c,l),i.data(j,n)}}},textWrap:{qualify:e.isPlainObject,set:function(b,c,d,f){var g=b.width||0;e.isPercentage(g)?c.width*=parseFloat(g)/100:g<=0?c.width+=g:c.width=g;var h=b.height||0;e.isPercentage(h)?c.height*=parseFloat(h)/100:h<=0?c.height+=h:c.height=h;var i=b.text;if(void 0===i&&(i=attr.text),void 0!==i)var j=a.util.breakText(""+i,c,{"font-weight":f["font-weight"]||f.fontWeight,"font-size":f["font-size"]||f.fontSize,"font-family":f["font-family"]||f.fontFamily,lineHeight:f.lineHeight},{svgDocument:this.paper.svg});a.dia.attributes.text.set.call(this,j,c,d,f)}},title:{qualify:function(a,b){return b instanceof SVGElement},set:function(a,b,c){var e=d(c),f="joint-title",g=e.data(f);if(void 0===g||g!==a){e.data(f,a);var h=c.firstChild;if(h&&"TITLE"===h.tagName.toUpperCase())h.textContent=a;else{var i=document.createElementNS(c.namespaceURI,"title");i.textContent=a,c.insertBefore(i,h)}}}},lineHeight:{qualify:m},textVerticalAnchor:{qualify:m},textPath:{qualify:m},annotations:{qualify:m},port:{set:function(a){return null===a||void 0===a.id?a:a.id}},style:{qualify:e.isPlainObject,set:function(a,b,c){d(c).css(a)}},html:{set:function(a,b,c){d(c).html(a+"")}},ref:{},refX:{position:g("x","width","origin")},refY:{position:g("y","height","origin")},refDx:{position:g("x","width","corner")},refDy:{position:g("y","height","corner")},refWidth:{set:f("width","width")},refHeight:{set:f("height","height")},refRx:{set:f("rx","width")},refRy:{set:f("ry","height")},refRInscribed:{set:function(a){var b=f(a,"width"),c=f(a,"height");return function(a,d){var e=d.height>d.width?b:c;return e(a,d)}}("r")},refRCircumscribed:{set:function(a,b){var c=e.isPercentage(a);a=parseFloat(a),c&&(a/=100);var d,f=Math.sqrt(b.height*b.height+b.width*b.width);return isFinite(a)&&(d=c||a>=0&&a<=1?a*f:Math.max(a+f,0)),{r:d}}},refCx:{set:f("cx","width")},refCy:{set:f("cy","height")},xAlignment:{offset:h("x","width","right")},yAlignment:{offset:h("y","height","bottom")},resetOffset:{offset:function(a,b){return a?{x:-b.x,y:-b.y}:{x:0,y:0}}},refDResetOffset:{set:j({resetOffset:!0})},refDKeepOffset:{set:j({resetOffset:!1})},refPointsResetOffset:{set:k({resetOffset:!0})},refPointsKeepOffset:{set:k({resetOffset:!1})},connection:{qualify:n,set:function(){return{d:this.getSerializedConnection()}}},atConnectionLengthKeepGradient:{qualify:n,set:l("getTangentAtLength",{rotate:!0})},atConnectionLengthIgnoreGradient:{qualify:n,set:l("getTangentAtLength",{rotate:!1})},atConnectionRatioKeepGradient:{qualify:n,set:l("getTangentAtRatio",{rotate:!0})},atConnectionRatioIgnoreGradient:{qualify:n,set:l("getTangentAtRatio",{rotate:!1})}};p.refR=p.refRInscribed,p.refD=p.refDResetOffset,p.refPoints=p.refPointsResetOffset,p.atConnectionLength=p.atConnectionLengthKeepGradient,p.atConnectionRatio=p.atConnectionRatioKeepGradient,p.refX2=p.refX,p.refY2=p.refY,p["ref-x"]=p.refX,p["ref-y"]=p.refY,p["ref-dy"]=p.refDy,p["ref-dx"]=p.refDx,p["ref-width"]=p.refWidth,p["ref-height"]=p.refHeight,p["x-alignment"]=p.xAlignment,p["y-alignment"]=p.yAlignment}(joint,V,g,$,joint.util),function(a,b){var c=a.mvc.View.extend({name:null,tagName:"g",className:"tool",svgElement:!0,_visible:!0,init:function(){var a=this.name;a&&this.vel.attr("data-tool-name",a)},configure:function(a,b){return this.relatedView=a,this.paper=a.paper,this.parentView=b,this.simulateRelatedView(this.el),this},simulateRelatedView:function(a){a&&a.setAttribute("model-id",this.relatedView.model.id)},getName:function(){return this.name},show:function(){this.el.style.display="",this._visible=!0},hide:function(){this.el.style.display="none",this._visible=!1},isVisible:function(){return!!this._visible},focus:function(){var a=this.options.focusOpacity;isFinite(a)&&(this.el.style.opacity=a),this.parentView.focusTool(this)},blur:function(){this.el.style.opacity="",this.parentView.blurTool(this)},update:function(){}}),d=a.mvc.View.extend({tagName:"g",className:"tools",svgElement:!0,tools:null,options:{tools:null,relatedView:null,name:null,component:!1},configure:function(d){d=b.assign(this.options,d);var e=d.tools;if(!Array.isArray(e))return this;var f=d.relatedView;if(!(f instanceof a.dia.CellView))return this;for(var g=this.tools=[],h=0,i=e.length;h0;){var d=c.shift();b.push(d),c.push.apply(c,d.getEmbeddedCells())}}else b=this.getEmbeddedCells(),b.forEach(function(c){b.push.apply(b,c.getEmbeddedCells(a))});else b=joint.util.toArray(this.get("embeds")).map(this.graph.getCell,this.graph);return b}return[]},isEmbeddedIn:function(a,b){var c=joint.util.isString(a)?a:a.id,d=this.parent();if(b=joint.util.defaults({deep:!0},b),this.graph&&b.deep){for(;d;){if(d===c)return!0;d=this.graph.getCell(d).parent()}return!1}return d===c},isEmbedded:function(){return!!this.parent()},clone:function(a){if(a=a||{},a.deep)return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null,[this].concat(this.getEmbeddedCells({deep:!0}))));var b=Backbone.Model.prototype.clone.apply(this,arguments);return b.set("id",joint.util.uuid()),b.unset("embeds"),b.unset("parent"),b},prop:function(a,b,c){var d="/",e=joint.util.isString(a);if(e||Array.isArray(a)){if(arguments.length>1){var f,g;e?(f=a,g=f.split("/")):(f=a.join(d),g=a.slice());var h=g[0],i=g.length;if(c=c||{},c.propertyPath=f,c.propertyValue=b,c.propertyPathArray=g,1===i)return this.set(h,b,c);for(var j={},k=j,l=h,m=1;m0)},getSelector:function(a,b){if(a===this.el)return b;var c;if(a){var d=V(a).index()+1;c=a.tagName+":nth-child("+d+")",b&&(c+=" > "+b),c=this.getSelector(a.parentNode,c)}return c},getLinkEnd:function(a,b,c,d,e){var f=this.model,h=f.id,i=this.findAttribute("port",a),j=a.getAttribute("joint-selector"),k={id:h};null!=j&&(k.magnet=j),null!=i?(k.port=i,f.hasPort(i)||j||(k.selector=this.getSelector(a))):null==j&&this.el!==a&&(k.selector=this.getSelector(a));var l=this.paper,m=l.options.connectionStrategy;if("function"==typeof m){var n=m.call(l,k,this,a,new g.Point(b,c),d,e);n&&(k=n)}return k},getMagnetFromLinkEnd:function(a){var b,c=this.el,d=a.port,e=a.magnet;return b=null!=d&&this.model.hasPort(d)?this.findPortNode(d,e)||c:this.findBySelector(e||a.selector,c,this.selectors)[0]},findAttribute:function(a,b){if(!b)return null;var c=b.getAttribute(a);if(null===c){if(b===this.el)return null;for(var d=b.parentNode;d&&d!==this.el&&1===d.nodeType&&(c=d.getAttribute(a),null===c);)d=d.parentNode}return c},getAttributeDefinition:function(a){return this.model.constructor.getAttributeDefinition(a)},setNodeAttributes:function(a,b){joint.util.isEmpty(b)||(a instanceof SVGElement?V(a).attr(b):$(a).attr(b))},processNodeAttributes:function(a,b){var c,d,e,f,g,h,i,j,k,l=[];for(c in b)b.hasOwnProperty(c)&&(d=b[c],e=this.getAttributeDefinition(c),!e||joint.util.isFunction(e.qualify)&&!e.qualify.call(this,d,a,b)?(h||(h={}),h[joint.util.toKebabCase(c)]=d):(joint.util.isString(e.set)&&(h||(h={}),h[e.set]=d),null!==d&&l.push(c,e)));for(f=0,g=l.length;f0&&x.height>0){var y=V.transformRect(a.getBBox(),p).scale(1/r,1/s);for(e in m)f=m[e],h=this.getAttributeDefinition(e),t=h.offset.call(this,f,y,a,i),t&&(q.offset(g.Point(t).scale(r,s)),w||(w=!0))}}(void 0!==o||v||w)&&(q.round(1),p.e=q.x,p.f=q.y,a.setAttribute("transform",V.matrixToTransformString(p)))},getNodeScale:function(a,b){var c,d;if(b&&b.contains(a)){var e=b.scale();c=1/e.sx,d=1/e.sy}else c=1,d=1;return{sx:c,sy:d}},findNodesAttributes:function(a,b,c,d){var e={};for(var f in a)if(a.hasOwnProperty(f))for(var g=c[f]=this.findBySelector(f,b,d),h=0,i=g.length;h-1?l.splice(t,0,d):l.push(d)}else this.setNodeAttributes(e,i.normal);for(var u=0,v=l.length;u0){this.startBatch("fit-embeds",a),a.deep&&joint.util.invoke(b,"fitEmbeds",a);var c=this.graph.getCellsBBox(b),d=joint.util.normalizeSides(a.padding);c.moveAndExpand({x:-d.left,y:-d.top,width:d.right+d.left,height:d.bottom+d.top}),this.set({position:{x:c.x,y:c.y},size:{width:c.width,height:c.height}},a),this.stopBatch("fit-embeds")}return this},rotate:function(a,b,c,d){if(c){var e=this.getBBox().center(),f=this.get("size"),g=this.get("position");e.rotate(c,this.get("angle")-a);var h=e.x-f.width/2-g.x,i=e.y-f.height/2-g.y;this.startBatch("rotate",{angle:a,absolute:b,origin:c}),this.position(g.x+h,g.y+i,d),this.rotate(a,b,null,d),this.stopBatch("rotate")}else this.set("angle",b?a:(this.get("angle")+a)%360,d);return this},angle:function(){return g.normalizeAngle(this.get("angle")||0)},getBBox:function(a){if(a=a||{},a.deep&&this.graph){var b=this.getEmbeddedCells({deep:!0,breadthFirst:!0});return b.push(this),this.graph.getCellsBBox(b)}var c=this.get("position"),d=this.get("size");return new g.Rect(c.x,c.y,d.width,d.height)}}),joint.dia.ElementView=joint.dia.CellView.extend({_removePorts:function(){},_renderPorts:function(){},className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("element"),a.join(" ")},metrics:null,initialize:function(){joint.dia.CellView.prototype.initialize.apply(this,arguments);var a=this.model;this.listenTo(a,"change:position",this.translate),this.listenTo(a,"change:size",this.resize),this.listenTo(a,"change:angle",this.rotate),this.listenTo(a,"change:markup",this.render),this._initializePorts(),this.metrics={}},_initializePorts:function(){},update:function(a,b){this.metrics={},this._removePorts();var c=this.model,d=c.attr();this.updateDOMSubtreeAttributes(this.el,d,{rootBBox:new g.Rect(c.size()),selectors:this.selectors,scalableNode:this.scalableNode,rotatableNode:this.rotatableNode,roAttributes:b===d?null:b}),this._renderPorts()},rotatableSelector:"rotatable",scalableSelector:"scalable",scalableNode:null,rotatableNode:null,renderMarkup:function(){var a=this.model,b=a.get("markup")||a.markup;if(!b)throw new Error("dia.ElementView: markup required");if(Array.isArray(b))return this.renderJSONMarkup(b);if("string"==typeof b)return this.renderStringMarkup(b);throw new Error("dia.ElementView: invalid markup")},renderJSONMarkup:function(a){var b=joint.util.parseDOMJSON(a),c=this.selectors=b.selectors,d=this.selector;if(c[d])throw new Error("dia.ElementView: ambiguous root selector.");c[d]=this.el,this.rotatableNode=V(c[this.rotatableSelector])||null,this.scalableNode=V(c[this.scalableSelector])||null,this.vel.append(b.fragment)},renderStringMarkup:function(a){var b=this.vel;b.append(V(a)),this.rotatableNode=b.findOne(".rotatable"),this.scalableNode=b.findOne(".scalable");var c=this.selectors={};c[this.selector]=this.el},render:function(){return this.vel.empty(),this.renderMarkup(),this.scalableNode&&this.update(),this.resize(),this.rotatableNode?(this.rotate(),this.translate(),this):(this.updateTransformation(),this)},resize:function(){return this.scalableNode?this.sgResize.apply(this,arguments):(this.model.attributes.angle&&this.rotate(),void this.update())},translate:function(){return this.rotatableNode?this.rgTranslate():void this.updateTransformation()},rotate:function(){return this.rotatableNode?this.rgRotate():void this.updateTransformation()},updateTransformation:function(){var a=this.getTranslateString(),b=this.getRotateString();b&&(a+=" "+b),this.vel.attr("transform",a)},getTranslateString:function(){var a=this.model.attributes.position;return"translate("+a.x+","+a.y+")"},getRotateString:function(){var a=this.model.attributes,b=a.angle;if(!b)return null;var c=a.size;return"rotate("+b+","+c.width/2+","+c.height/2+")"},getBBox:function(a){var b;if(a&&a.useModelGeometry){var c=this.model;b=c.getBBox().bbox(c.angle())}else b=this.getNodeBBox(this.el);return this.paper.localToPaperRect(b)},nodeCache:function(a){var b=V.ensureId(a),c=this.metrics[b];return c||(c=this.metrics[b]={}),c},getNodeData:function(a){var b=this.nodeCache(a);return b.data||(b.data={}),b.data},getNodeBBox:function(a){var b=this.getNodeBoundingRect(a),c=this.getNodeMatrix(a),d=this.getRootTranslateMatrix(),e=this.getRootRotateMatrix();return V.transformRect(b,d.multiply(e).multiply(c))},getNodeBoundingRect:function(a){var b=this.nodeCache(a);return void 0===b.boundingRect&&(b.boundingRect=V(a).getBBox()),new g.Rect(b.boundingRect)},getNodeUnrotatedBBox:function(a){var b=this.getNodeBoundingRect(a),c=this.getNodeMatrix(a),d=this.getRootTranslateMatrix();return V.transformRect(b,d.multiply(c))},getNodeShape:function(a){var b=this.nodeCache(a);return void 0===b.geometryShape&&(b.geometryShape=V(a).toGeometryShape()),b.geometryShape.clone()},getNodeMatrix:function(a){var b=this.nodeCache(a);if(void 0===b.magnetMatrix){var c=this.rotatableNode||this.el;b.magnetMatrix=V(a).getTransformToElement(c)}return V.createSVGMatrix(b.magnetMatrix)},getRootTranslateMatrix:function(){var a=this.model,b=a.position(),c=V.createSVGMatrix().translate(b.x,b.y);return c},getRootRotateMatrix:function(){var a=V.createSVGMatrix(),b=this.model,c=b.angle();if(c){var d=b.getBBox(),e=d.width/2,f=d.height/2;a=a.translate(e,f).rotate(c).translate(-e,-f)}return a},rgRotate:function(){this.rotatableNode.attr("transform",this.getRotateString())},rgTranslate:function(){this.vel.attr("transform",this.getTranslateString())},sgResize:function(a,b,c){var d=this.model,e=d.get("angle")||0,f=d.get("size")||{width:1,height:1},g=this.scalableNode,h=!1;g.node.getElementsByTagName("path").length>0&&(h=!0);var i=g.getBBox({recursive:h}),j=f.width/(i.width||1),k=f.height/(i.height||1);g.attr("transform","scale("+j+","+k+")");var l=this.rotatableNode,m=l&&l.attr("transform");if(m&&null!==m){l.attr("transform",m+" rotate("+-e+","+f.width/2+","+f.height/2+")");var n=g.getBBox({target:this.paper.viewport});d.set("position",{x:n.x,y:n.y},c),this.rotate()}this.update()},prepareEmbedding:function(a){a||(a={});var b=a.model||this.model,c=a.paper||this.paper,d=c.model;b.startBatch("to-front"),b.toFront({deep:!0,ui:!0});var e=d.get("cells").max("z").get("z"),f=d.getConnectedLinks(b,{deep:!0});joint.util.invoke(f,"set","z",e+1,{ui:!0}),b.stopBatch("to-front");var g=b.parent();g&&d.getCell(g).unembed(b,{ui:!0})},processEmbedding:function(a){a||(a={});var b=a.model||this.model,c=a.paper||this.paper,d=c.options,e=[];if(joint.util.isFunction(d.findParentBy)){var f=joint.util.toArray(d.findParentBy.call(c.model,this));e=f.filter(function(a){return a instanceof joint.dia.Cell&&this.model.id!==a.id&&!a.isEmbeddedIn(this.model)}.bind(this))}else e=c.model.findModelsUnderElement(b,{searchBy:d.findParentBy});d.frontParentOnly&&(e=e.slice(-1));for(var g=null,h=a.candidateEmbedView,i=e.length-1;i>=0;i--){var j=e[i];if(h&&h.model.id==j.id){g=h;break}var k=j.findView(c);if(d.validateEmbedding.call(c,this,k)){g=k;break}}g&&g!=h&&(this.clearEmbedding(a),a.candidateEmbedView=g.highlight(null,{embedding:!0})),!g&&h&&this.clearEmbedding(a)},clearEmbedding:function(a){a||(a={});var b=a.candidateEmbedView;b&&(b.unhighlight(null,{embedding:!0}),a.candidateEmbedView=null)},finalizeEmbedding:function(a){a||(a={});var b=a.candidateEmbedView,c=a.model||this.model,d=a.paper||this.paper;b&&(b.model.embed(c,{ui:!0}),b.unhighlight(null,{embedding:!0}),a.candidateEmbedView=null),joint.util.invoke(d.model.getConnectedLinks(c,{deep:!0}),"reparent",{ui:!0})},pointerdblclick:function(a,b,c){joint.dia.CellView.prototype.pointerdblclick.apply(this,arguments),this.notify("element:pointerdblclick",a,b,c)},pointerclick:function(a,b,c){joint.dia.CellView.prototype.pointerclick.apply(this,arguments),this.notify("element:pointerclick",a,b,c)},contextmenu:function(a,b,c){joint.dia.CellView.prototype.contextmenu.apply(this,arguments),this.notify("element:contextmenu",a,b,c)},pointerdown:function(a,b,c){joint.dia.CellView.prototype.pointerdown.apply(this,arguments),this.notify("element:pointerdown",a,b,c),this.dragStart(a,b,c)},pointermove:function(a,b,c){var d=this.eventData(a);switch(d.action){case"move":this.drag(a,b,c);break;case"magnet":this.dragMagnet(a,b,c)}d.stopPropagation||(joint.dia.CellView.prototype.pointermove.apply(this,arguments),this.notify("element:pointermove",a,b,c)),this.eventData(a,d)},pointerup:function(a,b,c){var d=this.eventData(a);switch(d.action){case"move":this.dragEnd(a,b,c);break;case"magnet":return void this.dragMagnetEnd(a,b,c)}d.stopPropagation||(this.notify("element:pointerup",a,b,c),joint.dia.CellView.prototype.pointerup.apply(this,arguments))},mouseover:function(a){joint.dia.CellView.prototype.mouseover.apply(this,arguments),this.notify("element:mouseover",a)},mouseout:function(a){joint.dia.CellView.prototype.mouseout.apply(this,arguments),this.notify("element:mouseout",a)},mouseenter:function(a){joint.dia.CellView.prototype.mouseenter.apply(this,arguments),this.notify("element:mouseenter",a)},mouseleave:function(a){joint.dia.CellView.prototype.mouseleave.apply(this,arguments),this.notify("element:mouseleave",a)},mousewheel:function(a,b,c,d){joint.dia.CellView.prototype.mousewheel.apply(this,arguments),this.notify("element:mousewheel",a,b,c,d)},onmagnet:function(a,b,c){this.dragMagnetStart(a,b,c);var d=this.eventData(a).stopPropagation;d&&a.stopPropagation()},dragStart:function(a,b,c){this.can("elementMove")&&this.eventData(a,{action:"move",x:b,y:c,restrictedArea:this.paper.getRestrictedArea(this)})},dragMagnetStart:function(a,b,c){if(this.can("addLinkFromMagnet")){this.model.startBatch("add-link");var d=this.paper,e=d.model,f=a.target,g=d.getDefaultLink(this,f),h=this.getLinkEnd(f,b,c,g,"source"),i={x:b,y:c};g.set({source:h,target:i}),g.addTo(e,{async:!1,ui:!0});var j=g.findView(d);joint.dia.CellView.prototype.pointerdown.apply(j,arguments),j.notify("link:pointerdown",a,b,c);var k=j.startArrowheadMove("target",{whenNotAllowed:"remove"});j.eventData(a,k),this.eventData(a,{action:"magnet",linkView:j,stopPropagation:!0}),this.paper.delegateDragEvents(this,a.data)}},drag:function(a,b,c){var d=this.paper,e=d.options.gridSize,f=this.model,h=f.position(),i=this.eventData(a),j=g.snapToGrid(h.x,e)-h.x+g.snapToGrid(b-i.x,e),k=g.snapToGrid(h.y,e)-h.y+g.snapToGrid(c-i.y,e);f.translate(j,k,{restrictedArea:i.restrictedArea,ui:!0});var l=!!i.embedding;d.options.embeddingMode&&(l||(this.prepareEmbedding(i),l=!0),this.processEmbedding(i)),this.eventData(a,{x:g.snapToGrid(b,e),y:g.snapToGrid(c,e),embedding:l})},dragMagnet:function(a,b,c){var d=this.eventData(a),e=d.linkView;e&&e.pointermove(a,b,c)},dragEnd:function(a,b,c){var d=this.eventData(a);d.embedding&&this.finalizeEmbedding(d)},dragMagnetEnd:function(a,b,c){var d=this.eventData(a),e=d.linkView;e&&e.pointerup(a,b,c),this.model.stopBatch("add-link")}}),joint.dia.Link=joint.dia.Cell.extend({markup:['','','','','','','',''].join(""),toolMarkup:['','','','',"Remove link.","",'','','',"Link options.","",""].join(""),doubleToolMarkup:void 0,vertexMarkup:['','','','',"Remove vertex.","",""].join(""),arrowheadMarkup:['','',""].join(""),defaultLabel:void 0,labelMarkup:void 0,_builtins:{defaultLabel:{markup:[{tagName:"rect",selector:"rect"},{tagName:"text",selector:"text"}],attrs:{text:{fill:"#000000",fontSize:14,textAnchor:"middle",yAlignment:"middle",pointerEvents:"none"},rect:{ref:"text",fill:"#ffffff",rx:3,ry:3,refWidth:1,refHeight:1,refX:0,refY:0}},position:{distance:.5}}},defaults:{type:"link",source:{},target:{}},isLink:function(){return!0},disconnect:function(a){return this.set({source:{x:0,y:0},target:{x:0,y:0}},a)},source:function(a,b,c){if(void 0===a)return joint.util.clone(this.get("source"));var d,e,f=a instanceof joint.dia.Cell;if(f)return d=joint.util.clone(b)||{},d.id=a.id,e=c,this.set("source",d,e);var h=a instanceof g.Point;return h?(d=joint.util.clone(b)||{},d.x=a.x,d.y=a.y,e=c,this.set("source",d,e)):(d=a,e=b,this.set("source",d,e))},target:function(a,b,c){if(void 0===a)return joint.util.clone(this.get("target"));var d,e,f=a instanceof joint.dia.Cell;if(f)return d=joint.util.clone(b)||{},d.id=a.id,e=c,this.set("target",d,e);var h=a instanceof g.Point;return h?(d=joint.util.clone(b)||{},d.x=a.x,d.y=a.y,e=c,this.set("target",d,e)):(d=a,e=b,this.set("target",d,e))},router:function(a,b,c){if(void 0===a)return router=this.get("router"),router?"object"==typeof router?joint.util.clone(router):router:this.get("manhattan")?{name:"orthogonal"}:null;var d="object"==typeof a||"function"==typeof a,e=d?a:{name:a,args:b},f=d?b:c;return this.set("router",e,f)},connector:function(a,b,c){if(void 0===a)return connector=this.get("connector"),connector?"object"==typeof connector?joint.util.clone(connector):connector:this.get("smooth")?{name:"smooth"}:null;var d="object"==typeof a||"function"==typeof a,e=d?a:{name:a,args:b},f=d?b:c;return this.set("connector",e,f)},label:function(a,b,c){var d=this.labels();return a=isFinite(a)&&null!==a?0|a:0,a<0&&(a=d.length+a),arguments.length<=1?this.prop(["labels",a]):this.prop(["labels",a],b,c)},labels:function(a,b){return 0===arguments.length?(a=this.get("labels"),Array.isArray(a)?a.slice():[]):(Array.isArray(a)||(a=[]),this.set("labels",a,b))},insertLabel:function(a,b,c){if(!b)throw new Error("dia.Link: no label provided");var d=this.labels(),e=d.length;return a=isFinite(a)&&null!==a?0|a:e,a<0&&(a=e+a+1),d.splice(a,0,b),this.labels(d,c)},appendLabel:function(a,b){return this.insertLabel(-1,a,b)},removeLabel:function(a,b){var c=this.labels();return a=isFinite(a)&&null!==a?0|a:-1,c.splice(a,1),this.labels(c,b)},vertex:function(a,b,c){var d=this.vertices();return a=isFinite(a)&&null!==a?0|a:0,a<0&&(a=d.length+a),arguments.length<=1?this.prop(["vertices",a]):this.prop(["vertices",a],b,c)},vertices:function(a,b){return 0===arguments.length?(a=this.get("vertices"),Array.isArray(a)?a.slice():[]):(Array.isArray(a)||(a=[]),this.set("vertices",a,b))},insertVertex:function(a,b,c){if(!b)throw new Error("dia.Link: no vertex provided");var d=this.vertices(),e=d.length;return a=isFinite(a)&&null!==a?0|a:e,a<0&&(a=e+a+1),d.splice(a,0,b),this.vertices(d,c)},removeVertex:function(a,b){var c=this.vertices();return a=isFinite(a)&&null!==a?0|a:-1,c.splice(a,1),this.vertices(c,b)},translate:function(a,b,c){return c=c||{},c.translateBy=c.translateBy||this.id,c.tx=a,c.ty=b,this.applyToPoints(function(c){return{x:(c.x||0)+a,y:(c.y||0)+b}},c)},scale:function(a,b,c,d){return this.applyToPoints(function(d){return g.point(d).scale(a,b,c).toJSON()},d)},applyToPoints:function(a,b){if(!joint.util.isFunction(a))throw new TypeError("dia.Link: applyToPoints expects its first parameter to be a function.");var c={},d=this.source();d.id||(c.source=a(d));var e=this.target();e.id||(c.target=a(e));var f=this.vertices();return f.length>0&&(c.vertices=f.map(a)),this.set(c,b)},reparent:function(a){var b;if(this.graph){var c=this.getSourceElement(),d=this.getTargetElement(),e=this.getParentCell();c&&d&&(b=this.graph.getCommonAncestor(c,d)),!e||b&&b.id===e.id||e.unembed(this,a),b&&b.embed(this,a)}return b},hasLoop:function(a){a=a||{};var b=this.source().id,c=this.target().id;if(!b||!c)return!1;var d=b===c;if(!d&&a.deep&&this.graph){var e=this.getSourceElement(),f=this.getTargetElement();d=e.isEmbeddedIn(f)||f.isEmbeddedIn(e)}return d},getSourceElement:function(){var a=this.source(),b=this.graph;return a&&a.id&&b&&b.getCell(a.id)||null},getTargetElement:function(){var a=this.target(),b=this.graph;return a&&a.id&&b&&b.getCell(a.id)||null},getRelationshipAncestor:function(){var a;if(this.graph){var b=[this,this.getSourceElement(),this.getTargetElement()].filter(function(a){return!!a});a=this.graph.getCommonAncestor.apply(this.graph,b)}return a||null},isRelationshipEmbeddedIn:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a.id,c=this.getRelationshipAncestor();return!!c&&(c.id===b||c.isEmbeddedIn(b))},_getDefaultLabel:function(){var a=this.get("defaultLabel")||this.defaultLabel||{},b={};return b.markup=a.markup||this.get("labelMarkup")||this.labelMarkup,b.position=a.position,b.attrs=a.attrs,b.size=a.size,b}},{endsEqual:function(a,b){var c=a.port===b.port||!a.port&&!b.port;return a.id===b.id&&c}}),joint.dia.LinkView=joint.dia.CellView.extend({className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("link"),a.join(" ")},options:{shortLinkLength:105,doubleLinkTools:!1,longLinkLength:155,linkToolsOffset:40,doubleLinkToolsOffset:65,sampleInterval:50},_labelCache:null,_labelSelectors:null,_markerCache:null,_V:null,_dragData:null,metrics:null,decimalsRounding:2,initialize:function(a){joint.dia.CellView.prototype.initialize.apply(this,arguments),"function"!=typeof this.constructor.prototype.watchSource&&(this.constructor.prototype.watchSource=this.createWatcher("source"),this.constructor.prototype.watchTarget=this.createWatcher("target")),this._labelCache={},this._labelSelectors={},this._markerCache={},this._V={},this.metrics={},this.startListening()},startListening:function(){var a=this.model;this.listenTo(a,"change:markup",this.render),this.listenTo(a,"change:smooth change:manhattan change:router change:connector",this.update),this.listenTo(a,"change:toolMarkup",this.onToolsChange),this.listenTo(a,"change:labels change:labelMarkup",this.onLabelsChange),this.listenTo(a,"change:vertices change:vertexMarkup",this.onVerticesChange),this.listenTo(a,"change:source",this.onSourceChange),this.listenTo(a,"change:target",this.onTargetChange)},onSourceChange:function(a,b,c){this.watchSource(a,b);var d=this.model;c.translateBy&&d.get("target").id&&b.id||this.update(d,null,c)},onTargetChange:function(a,b,c){this.watchTarget(a,b);var d=this.model;(!c.translateBy||d.get("source").id&&!b.id&&joint.util.isEmpty(d.get("vertices")))&&this.update(d,null,c)},onVerticesChange:function(a,b,c){this.renderVertexMarkers(),c.translateBy&&c.translateBy!==this.model.id||this.update(a,null,c)},onToolsChange:function(){this.renderTools().updateToolsPosition()},onLabelsChange:function(a,b,c){var d=!0,e=this.model.previous("labels");if(e&&"propertyPathArray"in c&&"propertyValue"in c){var f=c.propertyPathArray||[],g=f.length;if(g>1){var h=!!e[f[1]];h&&(2===g?d="markup"in Object(c.propertyValue):"markup"!==f[2]&&(d=!1))}}d?this.renderLabels():this.updateLabels(),this.updateLabelPositions()},render:function(){this.vel.empty(),this._V={},this.renderMarkup(),this.renderLabels();var a=this.model;return this.watchSource(a,a.source()).watchTarget(a,a.target()).update(),this},renderMarkup:function(){var a=this.model,b=a.get("markup")||a.markup;if(!b)throw new Error("dia.LinkView: markup required");if(Array.isArray(b))return this.renderJSONMarkup(b);if("string"==typeof b)return this.renderStringMarkup(b);throw new Error("dia.LinkView: invalid markup")},renderJSONMarkup:function(a){var b=joint.util.parseDOMJSON(a),c=this.selectors=b.selectors,d=this.selector;if(c[d])throw new Error("dia.LinkView: ambiguous root selector.");c[d]=this.el,this.vel.append(b.fragment)},renderStringMarkup:function(a){var b=V(a);Array.isArray(b)||(b=[b]);for(var c=this._V,d=0,e=b.length;d1||"G"!==d[0].nodeName.toUpperCase()?(c=V("g"),c.append(b),c.addClass("label")):(c=V(d[0]),c.addClass("label")),{node:c.node,selectors:a.selectors}}},renderLabels:function(){var a=this._V,b=a.labels,c=this._labelCache={},d=this._labelSelectors={};b&&b.empty();var e=this.model,f=e.get("labels")||[],g=f.length;if(0===g)return this;b||(b=a.labels=V("g").addClass("labels").appendTo(this.el));for(var h=0;h=this.options.longLinkLength){var e=this.options.doubleLinkToolsOffset||b;d=this.getPointAtLength(c-e),this._tool2Cache.attr("transform","translate("+d.x+", "+d.y+") "+a),this._tool2Cache.attr("visibility","visible")}else this.options.doubleLinkTools&&this._tool2Cache.attr("visibility","hidden")}return this},updateArrowheadMarkers:function(){if(!this._V.markerArrowheads)return this;if("none"===$.css(this._V.markerArrowheads.node,"display"))return this;var a=this.getConnectionLength()0&&b<=1,d=0,e={x:0,y:0};if(a.offset){var f=a.offset;"number"==typeof f&&(d=f),f.x&&(e.x=f.x),f.y&&(e.y=f.y)}var g,h=0!==e.x||0!==e.y||0===d,i=this.path,j={segmentSubdivisions:this.getConnectionSubdivisions()},k=c?b*this.getConnectionLength():b;if(h)g=i.pointAtLength(k,j),g.offset(e);else{var l=i.tangentAtLength(k,j);l?(l.rotate(l.start,-90),l.setLength(d),g=l.end):g=i.start}return g},getVertexIndex:function(a,b){for(var c=this.model,d=c.vertices(),e=this.getClosestPointLength(new g.Point(a,b)),f=0,h=d.length;f0){for(var j=0,k=i.length;j").addClass(joint.util.addClassNamePrefix("paper-background")),this.options.background&&this.drawBackground(this.options.background),this.$grid=$("
").addClass(joint.util.addClassNamePrefix("paper-grid")),this.options.drawGrid&&this.drawGrid(),this.$el.append(this.$background,this.$grid,this.svg),this},update:function(){return this.options.drawGrid&&this.drawGrid(),this._background&&this.updateBackgroundImage(this._background),this},_viewportMatrix:null,_viewportTransformString:null,matrix:function(a){var b=this.viewport;if(void 0===a){var c=b.getAttribute("transform");return(this._viewportTransformString||null)===c?a=this._viewportMatrix:(a=b.getCTM(),this._viewportMatrix=a,this._viewportTransformString=c),V.createSVGMatrix(a)}return a=V.createSVGMatrix(a),ctmString=V.matrixToTransformString(a),b.setAttribute("transform",ctmString),this.tools.setAttribute("transform",ctmString),this._viewportMatrix=a,this._viewportTransformString=b.getAttribute("transform"),this},clientMatrix:function(){return V.createSVGMatrix(this.viewport.getScreenCTM())},_sortDelayingBatches:["add","to-front","to-back"],_onSort:function(){this.model.hasActiveBatch(this._sortDelayingBatches)||this.sortViews()},_onBatchStop:function(a){var b=a&&a.batchName;this._sortDelayingBatches.includes(b)&&!this.model.hasActiveBatch(this._sortDelayingBatches)&&this.sortViews()},onRemove:function(){this.removeViews()},setDimensions:function(a,b){a=this.options.width=a||this.options.width,b=this.options.height=b||this.options.height,this.$el.css({width:Math.round(a),height:Math.round(b)}),this.trigger("resize",a,b)},setOrigin:function(a,b){return this.translate(a||0,b||0,{absolute:!0})},fitToContent:function(a,b,c,d){joint.util.isObject(a)?(d=a,a=d.gridWidth||1,b=d.gridHeight||1,c=d.padding||0):(d=d||{},a=a||1,b=b||1,c=c||0),c=joint.util.normalizeSides(c);var e=V(this.viewport).getBBox(),f=this.scale(),g=this.translate();e.x*=f.sx,e.y*=f.sy,e.width*=f.sx,e.height*=f.sy;var h=Math.max(Math.ceil((e.width+e.x)/a),1)*a,i=Math.max(Math.ceil((e.height+e.y)/b),1)*b,j=0,k=0;("negative"==d.allowNewOrigin&&e.x<0||"positive"==d.allowNewOrigin&&e.x>=0||"any"==d.allowNewOrigin)&&(j=Math.ceil(-e.x/a)*a,j+=c.left,h+=j),("negative"==d.allowNewOrigin&&e.y<0||"positive"==d.allowNewOrigin&&e.y>=0||"any"==d.allowNewOrigin)&&(k=Math.ceil(-e.y/b)*b,k+=c.top,i+=k),h+=c.right,i+=c.bottom,h=Math.max(h,d.minWidth||0),i=Math.max(i,d.minHeight||0),h=Math.min(h,d.maxWidth||Number.MAX_VALUE),i=Math.min(i,d.maxHeight||Number.MAX_VALUE);var l=h!=this.options.width||i!=this.options.height,m=j!=g.tx||k!=g.ty;m&&this.translate(j,k),l&&this.setDimensions(h,i)},scaleContentToFit:function(a){var b=this.getContentBBox();if(b.width&&b.height){a=a||{},joint.util.defaults(a,{padding:0,preserveAspectRatio:!0,scaleGrid:null,minScale:0,maxScale:Number.MAX_VALUE});var c,d=a.padding,e=a.minScaleX||a.minScale,f=a.maxScaleX||a.maxScale,h=a.minScaleY||a.minScale,i=a.maxScaleY||a.maxScale;if(a.fittingBBox)c=a.fittingBBox;else{var j=this.translate();c={x:j.tx,y:j.ty,width:this.options.width,height:this.options.height}}c=g.rect(c).moveAndExpand({x:d,y:d,width:-2*d,height:-2*d});var k=this.scale(),l=c.width/b.width*k.sx,m=c.height/b.height*k.sy;if(a.preserveAspectRatio&&(l=m=Math.min(l,m)),a.scaleGrid){var n=a.scaleGrid;l=n*Math.floor(l/n),m=n*Math.floor(m/n)}l=Math.min(f,Math.max(e,l)),m=Math.min(i,Math.max(h,m)),this.scale(l,m);var o=this.getContentBBox(),p=c.x-o.x,q=c.y-o.y;this.translate(p,q)}},getContentArea:function(){return V(this.viewport).getBBox()},getContentBBox:function(){var a=this.viewport.getBoundingClientRect(),b=this.clientMatrix(),c=this.translate();return g.rect({x:a.left-b.e+c.tx,y:a.top-b.f+c.ty,width:a.width,height:a.height})},getArea:function(){return this.paperToLocalRect({x:0,y:0,width:this.options.width,height:this.options.height})},getRestrictedArea:function(){var a;return a=joint.util.isFunction(this.options.restrictTranslate)?this.options.restrictTranslate.apply(this,arguments):this.options.restrictTranslate===!0?this.getArea():this.options.restrictTranslate||null},createViewForModel:function(a){var b,c,d=this.options.cellViewNamespace,e=a.get("type")+"View",f=joint.util.getByPath(d,e,".");a.isLink()?(b=this.options.linkView,c=joint.dia.LinkView):(b=this.options.elementView,c=joint.dia.ElementView);var g=b.prototype instanceof Backbone.View?f||b:b.call(this,a)||f||c;return new g({model:a,interactive:this.options.interactive})},onCellAdded:function(a,b,c){if(this.options.async&&c.async!==!1&&joint.util.isNumber(c.position)){if(this._asyncCells=this._asyncCells||[],this._asyncCells.push(a),0==c.position){if(this._frameId)throw new Error("another asynchronous rendering in progress");this.asyncRenderViews(this._asyncCells,c),delete this._asyncCells}}else this.renderView(a)},removeView:function(a){var b=this._views[a.id];return b&&(b.remove(),delete this._views[a.id]),b},renderView:function(a){var b=this._views[a.id]=this.createViewForModel(a);return V(this.viewport).append(b.el),b.paper=this,b.render(),b},onImageDragStart:function(){return!1},beforeRenderViews:function(a){return a.sort(function(a){return a.isLink()?1:-1}),a},afterRenderViews:function(){this.sortViews()},resetViews:function(a,b){this.removeViews();var c=a.models.slice();if(c=this.beforeRenderViews(c,b)||c,this.cancelRenderViews(),this.options.async)this.asyncRenderViews(c,b);else{for(var d=0,e=c.length;d(e.get("z")||0)?1:-1})},scale:function(a,b,c,d){if(void 0===a)return V.matrixToScale(this.matrix());void 0===b&&(b=a),void 0===c&&(c=0,d=0);var e=this.translate();if(c||d||e.tx||e.ty){var f=e.tx-c*(a-1),g=e.ty-d*(b-1);this.translate(f,g)}var h=this.matrix();return h.a=a||0,h.d=b||0,this.matrix(h),this.trigger("scale",a,b,c,d),this},rotate:function(a,b,c){if(void 0===a)return V.matrixToRotate(this.matrix());if(void 0===b){var d=this.viewport.getBBox();b=d.width/2,c=d.height/2}var e=this.matrix().translate(b,c).rotate(a).translate(-b,-c);return this.matrix(e),this},translate:function(a,b){if(void 0===a)return V.matrixToTranslate(this.matrix());var c=this.matrix();c.e=a||0,c.f=b||0,this.matrix(c);var d=this.translate(),e=this.options.origin;return e.x=d.tx,e.y=d.ty,this.trigger("translate",d.tx,d.ty),this.options.drawGrid&&this.drawGrid(),this},findView:function(a){for(var b=joint.util.isString(a)?this.viewport.querySelector(a):a instanceof $?a[0]:a;b&&b!==this.el&&b!==document;){var c=b.getAttribute("model-id");if(c)return this._views[c];b=b.parentNode}},findViewByModel:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a&&a.id;return this._views[b]},findViewsFromPoint:function(a){a=g.point(a);var b=this.model.getElements().map(this.findViewByModel,this);return b.filter(function(b){return b&&b.vel.getBBox({target:this.viewport}).containsPoint(a)},this)},findViewsInArea:function(a,b){b=joint.util.defaults(b||{},{strict:!1}),a=g.rect(a);var c=this.model.getElements().map(this.findViewByModel,this),d=b.strict?"containsRect":"intersect";return c.filter(function(b){return b&&a[d](b.vel.getBBox({target:this.viewport}))},this)},removeTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"remove"),this},hideTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"hide"),this},showTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"show"),this},getModelById:function(a){return this.model.getCell(a)},snapToGrid:function(a,b){return this.clientToLocalPoint(a,b).snapToGrid(this.options.gridSize)},localToPaperPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix());return g.Point(d)},localToPaperRect:function(a,b,c,d){var e=g.Rect(a,b),f=V.transformRect(e,this.matrix());return g.Rect(f)},paperToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix().inverse());return g.Point(d)},paperToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.matrix().inverse());return g.Rect(f)},localToClientPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix());return g.Point(d)},localToClientRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix());return g.Rect(f)},clientToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix().inverse());return g.Point(d)},clientToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix().inverse());return g.Rect(f)},localToPagePoint:function(a,b){return this.localToPaperPoint(a,b).offset(this.pageOffset())},localToPageRect:function(a,b,c,d){return this.localToPaperRect(a,b,c,d).moveAndExpand(this.pageOffset())},pageToLocalPoint:function(a,b){var c=g.Point(a,b),d=c.difference(this.pageOffset());return this.paperToLocalPoint(d)},pageToLocalRect:function(a,b,c,d){var e=this.pageOffset(),f=g.Rect(a,b,c,d);return f.x-=e.x,f.y-=e.y,this.paperToLocalRect(f)},clientOffset:function(){var a=this.svg.getBoundingClientRect();return g.Point(a.left,a.top)},pageOffset:function(){return this.clientOffset().offset(window.scrollX,window.scrollY)},linkAllowed:function(a){if(!(a instanceof joint.dia.LinkView))throw new Error("Must provide a linkView.");var b=a.model,c=this.options,d=this.model,e=d.constructor.validations;return!(!c.multiLinks&&!e.multiLinks.call(this,d,b))&&(!(!c.linkPinning&&!e.linkPinning.call(this,d,b))&&!("function"==typeof c.allowLink&&!c.allowLink.call(this,a,this)))},getDefaultLink:function(a,b){return joint.util.isFunction(this.options.defaultLink)?this.options.defaultLink.call(this,a,b):this.options.defaultLink.clone()},resolveHighlighter:function(a){a=a||{};var b=a.highlighter,c=this.options;if(void 0===b){var d=["embedding","connecting","magnetAvailability","elementAvailability"].find(function(b){return!!a[b]});b=d&&c.highlighting[d]||c.highlighting.default}if(!b)return!1;joint.util.isString(b)&&(b={name:b});var e=b.name,f=c.highlighterNamespace[e];if(!f)throw new Error('Unknown highlighter ("'+e+'")');if("function"!=typeof f.highlight)throw new Error('Highlighter ("'+e+'") is missing required highlight() method');if("function"!=typeof f.unhighlight)throw new Error('Highlighter ("'+e+'") is missing required unhighlight() method');return{highlighter:f,options:b.options||{},name:e}},onCellHighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){b.id||(b.id=V.uniqueId());var d=c.name+b.id+JSON.stringify(c.options);if(!this._highlights[d]){var e=c.highlighter;e.highlight(a,b,joint.util.assign({},c.options)),this._highlights[d]={cellView:a,magnetEl:b,opt:c.options,highlighter:e}}}},onCellUnhighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){var d=c.name+b.id+JSON.stringify(c.options),e=this._highlights[d];e&&(e.highlighter.unhighlight(e.cellView,e.magnetEl,e.opt),this._highlights[d]=null)}},pointerdblclick:function(a){a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerdblclick(a,c.x,c.y):this.trigger("blank:pointerdblclick",a,c.x,c.y)}},pointerclick:function(a){if(this._mousemoved<=this.options.clickThreshold){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(this.guard(a,b))return;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerclick(a,c.x,c.y):this.trigger("blank:pointerclick",a,c.x,c.y)}},contextmenu:function(a){ +this.options.preventContextMenu&&a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.contextmenu(a,c.x,c.y):this.trigger("blank:contextmenu",a,c.x,c.y)}},pointerdown:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?(a.preventDefault(),b.pointerdown(a,c.x,c.y)):(this.options.preventDefaultBlankAction&&a.preventDefault(),this.trigger("blank:pointerdown",a,c.x,c.y)),this.delegateDragEvents(b,a.data)}},pointermove:function(a){a.preventDefault();var b=this.eventData(a);b.mousemoved||(b.mousemoved=0);var c=++b.mousemoved;if(!(c<=this.options.moveThreshold)){a=joint.util.normalizeEvent(a);var d=this.snapToGrid({x:a.clientX,y:a.clientY}),e=b.sourceView;e?e.pointermove(a,d.x,d.y):this.trigger("blank:pointermove",a,d.x,d.y),this.eventData(a,b)}},pointerup:function(a){this.undelegateDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.snapToGrid({x:a.clientX,y:a.clientY}),c=this.eventData(a).sourceView;c?c.pointerup(a,b.x,b.y):this.trigger("blank:pointerup",a,b.x,b.y),this.delegateEvents()},mouseover:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b))if(b)b.mouseover(a);else{if(this.el===a.target)return;this.trigger("blank:mouseover",a)}},mouseout:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b))if(b)b.mouseout(a);else{if(this.el===a.target)return;this.trigger("blank:mouseout",a)}},mouseenter:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.findView(a.relatedTarget);if(b){if(c===b)return;b.mouseenter(a)}else{if(c)return;this.trigger("paper:mouseenter",a)}}},mouseleave:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.findView(a.relatedTarget);if(b){if(c===b)return;b.mouseleave(a)}else{if(c)return;this.trigger("paper:mouseleave",a)}}},mousewheel:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=a.originalEvent,d=this.snapToGrid({x:c.clientX,y:c.clientY}),e=Math.max(-1,Math.min(1,c.wheelDelta||-c.detail));b?b.mousewheel(a,d.x,d.y,e):this.trigger("blank:mousewheel",a,d.x,d.y,e)}},onevent:function(a){var b=a.currentTarget,c=b.getAttribute("event");if(c){var d=this.findView(b);if(d){if(a=joint.util.normalizeEvent(a),this.guard(a,d))return;var e=this.snapToGrid({x:a.clientX,y:a.clientY});d.onevent(a,c,e.x,e.y)}}},onmagnet:function(a){var b=a.currentTarget,c=b.getAttribute("magnet");if(c){var d=this.findView(b);if(d){if(a=joint.util.normalizeEvent(a),this.guard(a,d))return;if(!this.options.validateMagnet(d,b))return;var e=this.snapToGrid(a.clientX,a.clientY);d.onmagnet(a,e.x,e.y)}}},onlabel:function(a){var b=a.currentTarget,c=this.findView(b);if(c){if(a=joint.util.normalizeEvent(a),this.guard(a,c))return;var d=this.snapToGrid(a.clientX,a.clientY);c.onlabel(a,d.x,d.y)}},delegateDragEvents:function(a,b){b||(b={}),this.eventData({data:b},{sourceView:a||null,mousemoved:0}),this.delegateDocumentEvents(null,b),this.undelegateEvents()},guard:function(a,b){return!(!this.options.guard||!this.options.guard(a,b))||(a.data&&void 0!==a.data.guarded?a.data.guarded:!(b&&b.model&&b.model instanceof joint.dia.Cell)&&(this.svg!==a.target&&this.el!==a.target&&!$.contains(this.svg,a.target)))},setGridSize:function(a){return this.options.gridSize=a,this.options.drawGrid&&this.drawGrid(),this},clearGrid:function(){return this.$grid&&this.$grid.css("backgroundImage","none"),this},_getGriRefs:function(){return this._gridCache||(this._gridCache={root:V("svg",{width:"100%",height:"100%"},V("defs")),patterns:{},add:function(a,b){V(this.root.node.childNodes[0]).append(b),this.patterns[a]=b,this.root.append(V("rect",{width:"100%",height:"100%",fill:"url(#"+a+")"}))},get:function(a){return this.patterns[a]},exist:function(a){return void 0!==this.patterns[a]}}),this._gridCache},setGrid:function(a){this.clearGrid(),this._gridCache=null,this._gridSettings=[];var b=Array.isArray(a)?a:[a||{}];return b.forEach(function(a){this._gridSettings.push.apply(this._gridSettings,this._resolveDrawGridOption(a))},this),this},_resolveDrawGridOption:function(a){var b=this.constructor.gridPatterns;if(joint.util.isString(a)&&Array.isArray(b[a]))return b[a].map(function(a){return joint.util.assign({},a)});var c=a||{args:[{}]},d=Array.isArray(c),e=c.name;if(d||e||c.markup||(e="dot"),e&&Array.isArray(b[e])){var f=b[e].map(function(a){return joint.util.assign({},a)}),g=Array.isArray(c.args)?c.args:[c.args||{}];joint.util.defaults(g[0],joint.util.omit(a,"args"));for(var h=0;h'),f=joint.util.toArray(d).map(function(a){return e({offset:a.offset,color:a.color,opacity:Number.isFinite(a.opacity)?a.opacity:1})}),g=["<"+c+">",f.join(""),""].join(""),h=joint.util.assign({id:b},a.attrs);V(g,h).appendTo(this.defs)}return b},defineMarker:function(a){if(!joint.util.isObject(a))throw new TypeError("dia.Paper: defineMarker() requires 1. argument to be an object.");var b=a.id;if(b||(b=this.svg.id+joint.util.hashCode(JSON.stringify(a))),!this.isDefined(b)){var c=joint.util.omit(a,"type","userSpaceOnUse"),d=V("marker",{id:b,orient:"auto",overflow:"visible",markerUnits:a.markerUnits||"userSpaceOnUse"},[V(a.type||"path",c)]);d.appendTo(this.defs)}return b}},{backgroundPatterns:{flipXy:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,-1,b.width,b.height),e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,1,b.width,0),e.drawImage(a,0,0,c,d),e.setTransform(1,0,0,-1,0,b.height),e.drawImage(a,0,0,c,d),b},flipX:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(2*c,0),e.scale(-1,1),e.drawImage(a,0,0,c,d),b},flipY:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(0,2*d),e.scale(1,-1),e.drawImage(a,0,0,c,d),b},watermark:function(a,b){b=b||{};var c=a.width,d=a.height,e=document.createElement("canvas");e.width=3*c,e.height=3*d;for(var f=e.getContext("2d"),h=joint.util.isNumber(b.watermarkAngle)?-b.watermarkAngle:-20,i=g.toRad(h),j=e.width/4,k=e.height/4,l=0;l<4;l++)for(var m=0;m<4;m++)(l+m)%2>0&&(f.setTransform(1,0,0,1,(2*l-1)*j,(2*m-1)*k),f.rotate(i),f.drawImage(a,-c/2,-d/2,c,d));return e}},gridPatterns:{dot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){V(a).attr({width:b.thickness*b.sx,height:b.thickness*b.sy,fill:b.color})}}],fixedDot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){var c=b.sx<=1?b.thickness*b.sx:b.thickness;V(a).attr({width:c,height:c,fill:b.color})}}],mesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}],doubleMesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}},{color:"#000000",thickness:3,scaleFactor:4,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}]}}),function(a,b,c){var d=function(b){var d=c.cloneDeep(b)||{};this.ports=[],this.groups={},this.portLayoutNamespace=a.layout.Port,this.portLabelLayoutNamespace=a.layout.PortLabel,this._init(d)};d.prototype={getPorts:function(){return this.ports},getGroup:function(a){return this.groups[a]||{}},getPortsByGroup:function(a){return this.ports.filter(function(b){return b.group===a})},getGroupPortsMetrics:function(a,b){var d=this.getGroup(a),e=this.getPortsByGroup(a),f=d.position||{},h=f.name,i=this.portLayoutNamespace;i[h]||(h="left");var j=f.args||{},k=e.map(function(a){return a&&a.position&&a.position.args}),l=i[h](k,b,j),m={ports:e,result:[]};return c.toArray(l).reduce(function(a,c,d){var e=a.ports[d];return a.result.push({portId:e.id,portTransformation:c,labelTransformation:this._getPortLabelLayout(e,g.Point(c),b),portAttrs:e.attrs,portSize:e.size,labelSize:e.label.size}),a}.bind(this),m),m.result},_getPortLabelLayout:function(a,b,c){var d=this.portLabelLayoutNamespace,e=a.label.position.name||"left";return d[e]?d[e](b,c,a.label.position.args):null},_init:function(a){if(c.isObject(a.groups))for(var b=Object.keys(a.groups),d=0,e=b.length;d0},hasPort:function(a){return this.getPortIndex(a)!==-1},getPorts:function(){return c.cloneDeep(this.prop("ports/items"))||[]},getPort:function(a){return c.cloneDeep(c.toArray(this.prop("ports/items")).find(function(b){return b.id&&b.id===a}))},getPortsPositions:function(a){var b=this._portSettingsData.getGroupPortsMetrics(a,g.Rect(this.size()));return b.reduce(function(a,b){var c=b.portTransformation;return a[b.portId]={x:c.x,y:c.y,angle:c.angle},a},{})},getPortIndex:function(a){var b=c.isObject(a)?a.id:a;return this._isValidPortId(b)?c.toArray(this.prop("ports/items")).findIndex(function(a){return a.id===b}):-1},addPort:function(a,b){if(!c.isObject(a)||Array.isArray(a))throw new Error("Element: addPort requires an object.");var d=c.assign([],this.prop("ports/items"));return d.push(a),this.prop("ports/items",d,b),this},portProp:function(a,b,d,e){var f=this.getPortIndex(a);if(f===-1)throw new Error("Element: unable to find port with id "+a);var g=Array.prototype.slice.call(arguments,1);return Array.isArray(b)?g[0]=["ports","items",f].concat(b):c.isString(b)?g[0]=["ports/items/",f,"/",b].join(""):(g=["ports/items/"+f],c.isPlainObject(b)&&(g.push(b),g.push(d))),this.prop.apply(this,g)},_validatePorts:function(){var b=this.get("ports")||{},d=[];b=b||{};var e=c.toArray(b.items);return e.forEach(function(a){"object"!=typeof a&&d.push("Element: invalid port ",a),this._isValidPortId(a.id)||(a.id=c.uuid())},this),a.util.uniq(e,"id").length!==e.length&&d.push("Element: found id duplicities in ports."),d},_isValidPortId:function(a){return null!==a&&void 0!==a&&!c.isObject(a)},addPorts:function(a,b){return a.length&&this.prop("ports/items",c.assign([],this.prop("ports/items")).concat(a),b),this},removePort:function(a,b){var d=b||{},e=c.assign([],this.prop("ports/items")),f=this.getPortIndex(a);return f!==-1&&(e.splice(f,1),d.rewrite=!0,this.prop("ports/items",e,d)),this},_createPortData:function(){var a=this._validatePorts();if(a.length>0)throw this.set("ports",this.previous("ports")),new Error(a.join(" "));var b;this._portSettingsData&&(b=this._portSettingsData.getPorts()),this._portSettingsData=new d(this.get("ports"));var c=this._portSettingsData.getPorts();if(b){var e=c.filter(function(a){if(!b.find(function(b){return b.id===a.id}))return a}),f=b.filter(function(a){if(!c.find(function(b){return b.id===a.id}))return a});f.length>0&&this.trigger("ports:remove",this,f),e.length>0&&this.trigger("ports:add",this,e)}}}),c.assign(a.dia.ElementView.prototype,{portContainerMarkup:"g",portMarkup:[{tagName:"circle",selector:"circle",attributes:{r:10,fill:"#FFFFFF",stroke:"#000000"}}],portLabelMarkup:[{tagName:"text",selector:"text",attributes:{fill:"#000000"}}],_portElementsCache:null,_initializePorts:function(){this._portElementsCache={},this.listenTo(this.model,"change:ports",function(){this._refreshPorts()})},_refreshPorts:function(){this._removePorts(),this._portElementsCache={},this._renderPorts()},_renderPorts:function(){for(var a=[],b=this._getContainerElement(),d=0,e=b.node.childNodes.length;d1?V("g").append(h):V(h.firstChild),e=g.selectors}else b=V(f),Array.isArray(b)&&(b=V("g").append(b));if(!b)throw new Error("ElementView: Invalid port markup.");b.attr({port:a.id,"port-group":a.group});var i,j=this._getPortLabelMarkup(a.label);if(Array.isArray(j)){var k=c.parseDOMJSON(j),l=k.fragment;d=l.childNodes.length>1?V("g").append(l):V(l.firstChild),i=k.selectors}else d=V(j),Array.isArray(d)&&(d=V("g").append(d));if(!d)throw new Error("ElementView: Invalid port label markup.");var m;if(e&&i){for(var n in i)if(e[n])throw new Error("ElementView: selectors within port must be unique.");m=c.assign({},e,i)}else m=e||i;var o=V(this.portContainerMarkup).addClass("joint-port").append([b.addClass("joint-port-body"),d.addClass("joint-port-label")]);return this._portElementsCache[a.id]={portElement:o,portLabelElement:d,portSelectors:m,portLabelSelectors:i,portContentElement:b,portContentSelectors:e},o},_updatePortGroup:function(a){for(var b=g.Rect(this.model.size()),c=this.model._portSettingsData.getGroupPortsMetrics(a,b),d=0,e=c.length;d'}),joint.shapes.basic.TextView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"change:attrs",this.resize)}}),joint.shapes.basic.Generic.define("basic.Text",{attrs:{text:{"font-size":18,fill:"#000000"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Circle",{size:{width:60,height:60},attrs:{circle:{fill:"#ffffff",stroke:"#000000",r:30,cx:30,cy:30},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Ellipse",{size:{width:60,height:40},attrs:{ellipse:{fill:"#ffffff",stroke:"#000000",rx:30,ry:20,cx:30,cy:20},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polygon",{size:{width:60,height:40},attrs:{polygon:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polyline",{size:{width:60,height:40},attrs:{polyline:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Image",{attrs:{text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Path",{size:{width:60,height:60},attrs:{path:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle",ref:"path","ref-x":.5,"ref-dy":10,fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Path.define("basic.Rhombus",{attrs:{path:{d:"M 30 0 L 60 30 30 60 0 30 z"},text:{"ref-y":.5,"ref-dy":null,"y-alignment":"middle"}}}),joint.shapes.basic.PortsModelInterface={initialize:function(){this.updatePortsAttrs(),this.on("change:inPorts change:outPorts",this.updatePortsAttrs,this),this.constructor.__super__.constructor.__super__.initialize.apply(this,arguments)},updatePortsAttrs:function(a){if(this._portSelectors){var b=joint.util.omit(this.get("attrs"),this._portSelectors);this.set("attrs",b,{silent:!0})}this._portSelectors=[];var c={};joint.util.toArray(this.get("inPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".inPorts","in");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),joint.util.toArray(this.get("outPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".outPorts","out");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),this.attr(c,{silent:!0}),this.processPorts(),this.trigger("process:ports")},getPortSelector:function(a){var b=".inPorts",c=this.get("inPorts").indexOf(a);if(c<0&&(b=".outPorts",c=this.get("outPorts").indexOf(a),c<0))throw new Error("getPortSelector(): Port doesn't exist.");return b+">g:nth-child("+(c+1)+")>.port-body"}},joint.shapes.basic.PortsViewInterface={initialize:function(){this.listenTo(this.model,"process:ports",this.update),joint.dia.ElementView.prototype.initialize.apply(this,arguments)},update:function(){this.renderPorts(),joint.dia.ElementView.prototype.update.apply(this,arguments)},renderPorts:function(){var a=this.$(".inPorts").empty(),b=this.$(".outPorts").empty(),c=joint.util.template(this.model.portMarkup),d=this.model.ports||[];d.filter(function(a){return"in"===a.type}).forEach(function(b,d){a.append(V(c({id:d,port:b})).node)}),d.filter(function(a){return"out"===a.type}).forEach(function(a,d){b.append(V(c({id:d,port:a})).node)})}},joint.shapes.basic.Generic.define("basic.TextBlock",{attrs:{rect:{fill:"#ffffff",stroke:"#000000",width:80,height:100},text:{fill:"#000000","font-size":14,"font-family":"Arial, helvetica, sans-serif"},".content":{text:"","ref-x":.5,"ref-y":.5,"y-alignment":"middle","x-alignment":"middle"}},content:""},{markup:['','',joint.env.test("svgforeignobject")?'
':'',""].join(""),initialize:function(){this.listenTo(this,"change:size",this.updateSize),this.listenTo(this,"change:content",this.updateContent),this.updateSize(this,this.get("size")),this.updateContent(this,this.get("content")),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateSize:function(a,b){this.attr({".fobj":joint.util.assign({},b),div:{style:joint.util.assign({},b)}})},updateContent:function(a,b){joint.env.test("svgforeignobject")?this.attr({".content":{html:joint.util.sanitizeHTML(b)}}):this.attr({".content":{text:b}})},setForeignObjectSize:function(){this.updateSize.apply(this,arguments)},setDivContent:function(){this.updateContent.apply(this,arguments)}}),joint.shapes.basic.TextBlockView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.noSVGForeignObjectElement=!joint.env.test("svgforeignobject"),joint.env.test("svgforeignobject")||this.listenTo(this.model,"change:content change:size",function(a){this.updateContent(a)})},update:function(a,b){var c=this.model;if(joint.env.test("svgforeignobject"))joint.dia.ElementView.prototype.update.call(this,c,b);else{var d=joint.util.omit(b||c.get("attrs"),".content");joint.dia.ElementView.prototype.update.call(this,c,d),b&&!joint.util.has(b,".content")||this.updateContent(c,b)}},updateContent:function(a,b){var c=joint.util.merge({},(b||a.get("attrs"))[".content"]);c=joint.util.omit(c,"text");var d=joint.util.breakText(a.get("content"),a.get("size"),c,{svgDocument:this.paper.svg}),e=joint.util.setByPath({},".content",c,"/");e[".content"].text=d,joint.dia.ElementView.prototype.update.call(this,a,e)}}),function(a,b,c,d){"use strict";var e=a.Element;e.define("standard.Rectangle",{attrs:{body:{refWidth:"100%",refHeight:"100%",strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Circle",{attrs:{body:{refCx:"50%",refCy:"50%",refR:"50%",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"circle",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Ellipse",{attrs:{body:{refCx:"50%",refCy:"50%",refRx:"50%",refRy:"50%",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"ellipse",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Path",{attrs:{body:{refD:"M 0 0 L 10 0 10 10 0 10 Z",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"path",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Polygon",{attrs:{body:{refPoints:"0 0 10 0 10 10 0 10",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"polygon",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Polyline",{attrs:{body:{refPoints:"0 0 10 0 10 10 0 10 0 0",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"polyline",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Image",{attrs:{image:{refWidth:"100%",refHeight:"100%"},label:{textVerticalAnchor:"top",textAnchor:"middle",refX:"50%",refY:"100%",refY2:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"image",selector:"image"},{tagName:"text",selector:"label"}]}),e.define("standard.BorderedImage",{attrs:{border:{refWidth:"100%",refHeight:"100%",stroke:"#333333",strokeWidth:2},image:{refWidth:-1,refHeight:-1,x:.5,y:.5},label:{textVerticalAnchor:"top",textAnchor:"middle",refX:"50%",refY:"100%",refY2:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"image",selector:"image"},{tagName:"rect",selector:"border",attributes:{fill:"none"}},{tagName:"text",selector:"label"}]}),e.define("standard.EmbeddedImage",{attrs:{body:{refWidth:"100%",refHeight:"100%",stroke:"#333333",fill:"#FFFFFF",strokeWidth:2},image:{refWidth:"30%",refHeight:-20,x:10,y:10,preserveAspectRatio:"xMidYMin"},label:{textVerticalAnchor:"top",textAnchor:"left",refX:"30%",refX2:20,refY:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"image",selector:"image"},{tagName:"text",selector:"label"}]}),e.define("standard.HeaderedRectangle",{attrs:{body:{refWidth:"100%",refHeight:"100%",strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},header:{refWidth:"100%",height:30,strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},headerText:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:15,fontSize:16,fill:"#333333"},bodyText:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",refY2:15,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"rect",selector:"header"},{tagName:"text",selector:"headerText"},{tagName:"text",selector:"bodyText"}]});var f=10;joint.dia.Element.define("standard.Cylinder",{attrs:{body:{lateralArea:f,fill:"#FFFFFF",stroke:"#333333",strokeWidth:2},top:{refCx:"50%",cy:f,refRx:"50%",ry:f,fill:"#FFFFFF",stroke:"#333333",strokeWidth:2},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"100%",refY2:15,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"path",selector:"body"},{tagName:"ellipse",selector:"top"},{tagName:"text",selector:"label"}],topRy:function(a,c){if(void 0===a)return this.attr("body/lateralArea");var d=b.isPercentage(a),e={lateralArea:a},f=d?{refCy:a,refRy:a,cy:null,ry:null}:{refCy:null,refRy:null,cy:a,ry:a};return this.attr({body:e,top:f},c)}},{attributes:{lateralArea:{set:function(a,c){var e=b.isPercentage(a);e&&(a=parseFloat(a)/100);var f=c.x,g=c.y,h=c.width,i=c.height,j=h/2,k=e?i*a:a,l=d.KAPPA,m=l*j,n=l*(e?i*a:a),o=f,p=f+h/2,q=f+h,r=g+k,s=r-k,t=g+i-k,u=g+i,v=["M",o,r,"L",o,t,"C",f,t+n,p-m,u,p,u,"C",p+m,u,q,t+n,q,t,"L",q,r,"C",q,r-n,p+m,s,p,s,"C",p-m,s,o,r-n,o,r,"Z"]; +return{d:v.join(" ")}}}}});var g={tagName:"foreignObject",selector:"foreignObject",attributes:{overflow:"hidden"},children:[{tagName:"div",namespaceURI:"http://www.w3.org/1999/xhtml",selector:"label",style:{width:"100%",height:"100%",position:"static",backgroundColor:"transparent",textAlign:"center",margin:0,padding:"0px 5px",boxSizing:"border-box",display:"flex",alignItems:"center",justifyContent:"center"}}]},h={tagName:"text",selector:"label",attributes:{"text-anchor":"middle"}};e.define("standard.TextBlock",{attrs:{body:{refWidth:"100%",refHeight:"100%",stroke:"#333333",fill:"#ffffff",strokeWidth:2},foreignObject:{refWidth:"100%",refHeight:"100%"},label:{style:{fontSize:14}}}},{markup:[{tagName:"rect",selector:"body"},c.test("svgforeignobject")?g:h]},{attributes:{text:{set:function(c,d,e,f){if(!(e instanceof HTMLElement)){var g=f.style||{},h={text:c,width:-5,height:"100%"},i=b.assign({textVerticalAnchor:"middle"},g);return a.attributes.textWrap.set.call(this,h,d,e,i),{fill:g.color||null}}e.textContent=c},position:function(a,b,c){if(c instanceof SVGElement)return b.center()}}}});var i=a.Link;i.define("standard.Link",{attrs:{line:{connection:!0,stroke:"#333333",strokeWidth:2,strokeLinejoin:"round",targetMarker:{type:"path",d:"M 10 -5 0 0 10 5 z"}},wrapper:{connection:!0,strokeWidth:10,strokeLinejoin:"round"}}},{markup:[{tagName:"path",selector:"wrapper",attributes:{fill:"none",cursor:"pointer",stroke:"transparent"}},{tagName:"path",selector:"line",attributes:{fill:"none","pointer-events":"none"}}]}),i.define("standard.DoubleLink",{attrs:{line:{connection:!0,stroke:"#DDDDDD",strokeWidth:4,strokeLinejoin:"round",targetMarker:{type:"path",stroke:"#000000",d:"M 10 -3 10 -10 -2 0 10 10 10 3"}},outline:{connection:!0,stroke:"#000000",strokeWidth:6,strokeLinejoin:"round"}}},{markup:[{tagName:"path",selector:"outline",attributes:{fill:"none"}},{tagName:"path",selector:"line",attributes:{fill:"none"}}]}),i.define("standard.ShadowLink",{attrs:{line:{connection:!0,stroke:"#FF0000",strokeWidth:20,strokeLinejoin:"round",targetMarker:{type:"path",stroke:"none",d:"M 0 -10 -10 0 0 10 z"},sourceMarker:{type:"path",stroke:"none",d:"M -10 -10 0 0 -10 10 0 10 0 -10 z"}},shadow:{connection:!0,refX:3,refY:6,stroke:"#000000",strokeOpacity:.2,strokeWidth:20,strokeLinejoin:"round",targetMarker:{type:"path",d:"M 0 -10 -10 0 0 10 z",stroke:"none"},sourceMarker:{type:"path",stroke:"none",d:"M -10 -10 0 0 -10 10 0 10 0 -10 z"}}}},{markup:[{tagName:"path",selector:"shadow",attributes:{fill:"none"}},{tagName:"path",selector:"line",attributes:{fill:"none"}}]})}(joint.dia,joint.util,joint.env,V),joint.routers.manhattan=function(a,b,c,d){"use strict";function e(a){this.map={},this.options=a,this.mapGridSize=100}function f(){this.items=[],this.hash={},this.values={},this.OPEN=1,this.CLOSE=2}function g(a,b){return b&&b.paddingBox?a.sourceBBox.clone().moveAndExpand(b.paddingBox):a.sourceBBox.clone()}function h(a,b){return b&&b.paddingBox?a.targetBBox.clone().moveAndExpand(b.paddingBox):a.targetBBox.clone()}function i(a,b){if(a.sourceAnchor)return a.sourceAnchor;var c=g(a,b);return c.center()}function j(a,b){if(a.targetAnchor)return a.targetAnchor;var c=h(a,b);return c.center()}function k(b,c,d,e,f){var g=360/d,h=b.theta(l(b,c,e,f)),i=a.normalizeAngle(h+g/2);return g*Math.floor(i/g)}function l(b,c,d,e){var f=e.step,g=c.x-b.x,h=c.y-b.y,i=g/d.x,j=h/d.y,k=i*f,l=j*f;return new a.Point(b.x+k,b.y+l)}function m(a,b){var c=Math.abs(a-b);return c>180?360-c:c}function n(a,b,c){var e=c.step;d.toArray(c.directions).forEach(function(a){a.gridOffsetX=a.offsetX/e*b.x,a.gridOffsetY=a.offsetY/e*b.y})}function o(a,b,c){return{source:b.clone(),x:p(c.x-b.x,a),y:p(c.y-b.y,a)}}function p(a,b){if(!a)return b;var c=Math.abs(a),d=Math.round(c/b);if(!d)return c;var e=d*b,f=c-e,g=f/d;return b+g}function q(b,c){var d=c.source,e=a.snapToGrid(b.x-d.x,c.x)+d.x,f=a.snapToGrid(b.y-d.y,c.y)+d.y;return new a.Point(e,f)}function r(a,b){return a?a.round(b.precision):a}function s(a){return a.clone().round().toString()}function t(b){return new a.Point(0===b.x?0:Math.abs(b.x)/b.x,0===b.y?0:Math.abs(b.y)/b.y)}function u(a,b,c,d,e,f){for(var g,h=[],i=t(e.difference(c)),j=s(c),k=a[j];k;){g=r(b[j],f);var l=t(g.difference(r(k.clone(),f)));l.equals(i)||(h.unshift(g),i=l),j=s(k),k=a[j]}var m=r(b[j],f),n=t(m.difference(d));return n.equals(i)||h.unshift(m),h}function v(a,b){for(var c=1/0,d=0,e=b.length;dj)&&(j=w,t=q(v,f))}var x=r(t,g);x&&(c.containsPoint(x)&&r(x.offset(l.x*f.x,l.y*f.y),g),d.push(x))}return d},[]);return c.containsPoint(i)||n.push(i),n}function x(b,c,e,g){var h,l;h=b instanceof a.Rect?i(this,g).clone():b.clone(),l=c instanceof a.Rect?j(this,g).clone():c.clone();var p,t,x,y,z=o(g.step,h,l);if(b instanceof a.Rect?(p=r(q(h,z),g),x=w(p,b,g.startDirections,z,g)):(p=r(q(h,z),g),x=[p]),c instanceof a.Rect?(t=r(q(l,z),g),y=w(l,c,g.endDirections,z,g)):(t=r(q(l,z),g),y=[t]),x=x.filter(e.isPointAccessible,e),y=y.filter(e.isPointAccessible,e),x.length>0&&y.length>0){for(var A=new f,B={},C={},D={},E=0,F=x.length;E0;){var Q,R=A.pop(),S=B[R],T=C[R],U=D[R],V=void 0===T,W=S.equals(p);if(Q=V?L?W?null:k(p,S,N,z,g):K:k(T,S,N,z,g),O.indexOf(R)>=0)return g.previousDirectionAngle=Q,u(C,B,S,p,t,g);for(E=0;Eg.maxAllowedDirectionChange)){var Y=S.clone().offset(I.gridOffsetX,I.gridOffsetY),Z=s(Y);if(!A.isClose(Z)&&e.isPointAccessible(Y)){if(O.indexOf(Z)>=0){r(Y,g);var $=Y.equals(t);if(!$){var _=k(Y,t,N,z,g),aa=m(X,_);if(aa>g.maxAllowedDirectionChange)continue}}var ba=I.cost,ca=W?0:g.penalties[J],da=U+ba+ca;(!A.isOpen(Z)||da90){var i=f;f=h,h=i}var j=d%90<45?f:h,k=new g.Line(a,j),l=90*Math.ceil(d/90),m=g.Point.fromPolar(k.squaredLength(),g.toRad(l+135),j),n=new g.Line(b,m),o=k.intersection(n),p=o?o:b,q=o?p:a,r=360/c.directions.length,s=q.theta(b),t=g.normalizeAngle(s+r/2),u=r*Math.floor(t/r);return c.previousDirectionAngle=u,p&&e.push(p.round()),e.push(b),e}};return function(c,d,e){if(!a.isFunction(joint.routers.manhattan))throw new Error("Metro requires the manhattan router.");return joint.routers.manhattan(c,a.assign({},b,d),e)}}(joint.util),joint.routers.normal=function(a,b,c){return a},joint.routers.oneSide=function(a,b,c){var d,e,f,g=b.side||"bottom",h=b.padding||40,i=c.sourceBBox,j=c.targetBBox,k=i.center(),l=j.center();switch(g){case"bottom":f=1,d="y",e="height";break;case"top":f=-1,d="y",e="height";break;case"left":f=-1,d="x",e="width";break;case"right":f=1,d="x",e="width";break;default:throw new Error("Router: invalid side")}return k[d]+=f*(i[e]/2+h),l[d]+=f*(j[e]/2+h),f*(k[d]-l[d])>0?l[d]=k[d]:k[d]=l[d],[k].concat(a,l)},joint.routers.orthogonal=function(a){function b(a,b,c){var d=new g.Point(a.x,b.y);return c.containsPoint(d)&&(d=new g.Point(b.x,a.y)),d}function c(a,b){return a["W"===b||"E"===b?"width":"height"]}function d(a,b){return a.x===b.x?a.y>b.y?"N":"S":a.y===b.y?a.x>b.x?"W":"E":null}function e(a){return new g.Rect(a.x,a.y,0,0)}function f(a,b){var c=b&&b.elementPadding||20;return a.sourceBBox.clone().inflate(c)}function h(a,b){var c=b&&b.elementPadding||20;return a.targetBBox.clone().inflate(c)}function i(a,b){if(a.sourceAnchor)return a.sourceAnchor;var c=f(a,b);return c.center()}function j(a,b){if(a.targetAnchor)return a.targetAnchor;var c=h(a,b);return c.center()}function k(a,b,c){var e=new g.Point(a.x,b.y),f=new g.Point(b.x,a.y),h=d(a,e),i=d(a,f),j=q[c],k=h===c||h!==j&&(i===j||i!==c)?e:f;return{points:[k],direction:d(k,b)}}function l(a,c,e){var f=b(a,c,e);return{points:[f],direction:d(f,c)}}function m(e,f,h,i){var j,k={},l=[new g.Point(e.x,f.y),new g.Point(f.x,e.y)],m=l.filter(function(a){return!h.containsPoint(a)}),n=m.filter(function(a){return d(a,e)!==i});if(n.length>0)j=n.filter(function(a){return d(e,a)===i}).pop(),j=j||n[0],k.points=[j],k.direction=d(j,f);else{j=a.difference(l,m)[0];var o=new g.Point(f).move(j,-c(h,i)/2),p=b(o,e,h);k.points=[p,o],k.direction=d(o,f)}return k}function n(a,b,e,f){var h=l(b,a,f),i=h.points[0];if(e.containsPoint(i)){h=l(a,b,e);var j=h.points[0];if(f.containsPoint(j)){var m=new g.Point(a).move(j,-c(e,d(a,j))/2),n=new g.Point(b).move(i,-c(f,d(b,i))/2),o=new g.Line(m,n).midpoint(),p=l(a,o,e),q=k(o,b,p.direction);h.points=[p.points[0],q.points[0]],h.direction=q.direction}}return h}function o(a,c,e,f,h){var i,j,k,l={},m=e.union(f).inflate(1),n=m.center().distance(c)>m.center().distance(a),o=n?c:a,p=n?a:c;return h?(i=g.Point.fromPolar(m.width+m.height,r[h],o),i=m.pointNearestToPoint(i).move(i,-1)):i=m.pointNearestToPoint(o).move(o,1),j=b(i,p,m),i.round().equals(j.round())?(j=g.Point.fromPolar(m.width+m.height,g.toRad(i.theta(o))+Math.PI/2,p),j=m.pointNearestToPoint(j).move(p,1).round(),k=b(i,j,m),l.points=n?[j,k,i]:[i,k,j]):l.points=n?[j,i]:[i,j],l.direction=n?d(i,c):d(j,c),l}function p(b,c,p){var q=c.elementPadding||20,r=f(p,c),s=h(p,c),t=i(p,c),u=j(p,c);r=r.union(e(t)),s=s.union(e(u)),b=a.toArray(b).map(g.Point),b.unshift(t),b.push(u);for(var v,w=[],x=0,y=b.length-1;x=Math.abs(a.y-b.y)){var k=(a.x+b.x)/2;j=g.Path.createSegment("C",k,a.y,k,b.y,b.x,b.y),e.appendSegment(j)}else{var l=(a.y+b.y)/2;j=g.Path.createSegment("C",a.x,l,b.x,l,b.x,b.y),e.appendSegment(j)}}return f?e:e.serialize()},joint.connectors.jumpover=function(a,b,c){function d(a,c,d){var e=[].concat(a,d,c);return e.reduce(function(a,c,d){var f=e[d+1];return null!=f&&(a[d]=b.line(c,f)),a},[])}function e(a){var b=a.paper._jumpOverUpdateList;null==b&&(b=a.paper._jumpOverUpdateList=[],a.paper.on("cell:pointerup",f),a.paper.model.on("reset",function(){b=a.paper._jumpOverUpdateList=[]})),b.indexOf(a)<0&&(b.push(a),a.listenToOnce(a.model,"change:connector remove",function(){b.splice(b.indexOf(a),1)}))}function f(){for(var a=this._jumpOverUpdateList,b=0;bw)||"jumpover"!==d.name)}),z=y.map(function(a){return s.findViewByModel(a)}),A=d(a,b,f),B=z.map(function(a){return null==a?[]:a===this?A:d(a.sourcePoint,a.targetPoint,a.route)},this),C=A.reduce(function(a,b){var c=y.reduce(function(a,c,d){if(c!==v){var e=g(b,B[d]);a.push.apply(a,e)}return a},[]).sort(function(a,c){return h(b.start,a)-h(b.start,c)});return c.length>0?a.push.apply(a,i(b,c,p)):a.push(b),a},[]),D=j(C,p,q);return o?D:D.serialize()}}(_,g,joint.util),function(a,b,c,d){function e(a,b,d){var e=a.toJSON();return e.angle=b||0,c.util.defaults({},d,e)}function f(a,c,d){return a.map(function(a,b,c){var d=this.pointAt((b+.5)/c.length);return(a.dx||a.dy)&&d.offset(a.dx||0,a.dy||0),e(d.round(),0,a)},b.line(c,d))}function g(a,c,d,f){var g=c.center(),h=c.width/c.height,i=c.topMiddle(),j=b.Ellipse.fromRect(c);return a.map(function(a,b,c){var k=d+f(b,c.length),l=i.clone().rotate(g,-k).scale(h,1,g),m=a.compensateRotation?-j.tangentTheta(l):0;return(a.dx||a.dy)&&l.offset(a.dx||0,a.dy||0),a.dr&&l.move(g,a.dr),e(l.round(),m,a)})}function h(a,c){var e=c.x;d.isString(e)&&(e=parseFloat(e)/100*a.width);var f=c.y;return d.isString(f)&&(f=parseFloat(f)/100*a.height),b.point(e||0,f||0)}c.layout.Port={absolute:function(a,b,c){return a.map(h.bind(null,b))},fn:function(a,b,c){return c.fn(a,b,c)},line:function(a,b,c){var d=h(b,c.start||b.origin()),e=h(b,c.end||b.corner());return f(a,d,e)},left:function(a,b,c){return f(a,b.origin(),b.bottomLeft())},right:function(a,b,c){return f(a,b.topRight(),b.corner())},top:function(a,b,c){return f(a,b.origin(),b.topRight())},bottom:function(a,b,c){return f(a,b.bottomLeft(),b.corner())},ellipseSpread:function(a,b,c){var d=c.startAngle||0,e=c.step||360/a.length;return g(a,b,d,function(a){return a*e})},ellipse:function(a,b,c){var d=c.startAngle||0,e=c.step||20;return g(a,b,d,function(a,b){return(a+.5-b/2)*e})}}}(_,g,joint,joint.util),function(a,b,c,d){function e(a,b){return d.defaultsDeep({},a,b,{x:0,y:0,angle:0,attrs:{".":{y:"0","text-anchor":"start"}}})}function f(a,b,c,f){f=d.defaults({},f,{offset:15});var h,i,j,k,l=b.center().theta(a),m=g(b),n=f.offset,o=0;lm[2]?(j=".3em",h=n,i=0,k="start"):lo[2]?(k=".3em",i=-m,j=0,l="end"):h-270&&i<-90?(g="start",j=i-180):g="end";var m=Math.round;return e({x:m(k.x),y:m(k.y),angle:c?j:0,attrs:{".":{y:l,"text-anchor":g}}})}c.layout.PortLabel={manual:function(a,b,c){return e(c,a)},left:function(a,b,c){return e(c,{x:-15,attrs:{".":{y:".3em","text-anchor":"end"}}})},right:function(a,b,c){return e(c,{x:15,attrs:{".":{y:".3em","text-anchor":"start"}}})},top:function(a,b,c){return e(c,{y:-15,attrs:{".":{"text-anchor":"middle"}}})},bottom:function(a,b,c){return e(c,{y:15,attrs:{".":{y:".6em","text-anchor":"middle"}}})},outsideOriented:function(a,b,c){return f(a,b,!0,c)},outside:function(a,b,c){return f(a,b,!1,c)},insideOriented:function(a,b,c){return h(a,b,!0,c)},inside:function(a,b,c){return h(a,b,!1,c)},radial:function(a,b,c){return i(a.difference(b.center()),!1,c)},radialOriented:function(a,b,c){return i(a.difference(b.center()),!0,c)}}}(_,g,joint,joint.util),joint.highlighters.addClass={className:joint.util.addClassNamePrefix("highlighted"),highlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).addClass(e)},unhighlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).removeClass(e)}},joint.highlighters.opacity={highlight:function(a,b){V(b).addClass(joint.util.addClassNamePrefix("highlight-opacity"))},unhighlight:function(a,b){V(b).removeClass(joint.util.addClassNamePrefix("highlight-opacity"))}},joint.highlighters.stroke={defaultOptions:{padding:3,rx:0,ry:0,attrs:{"stroke-width":3,stroke:"#FEB663"}},_views:{},getHighlighterId:function(a,b){return a.id+JSON.stringify(b)},removeHighlighter:function(a){this._views[a]&&(this._views[a].remove(),this._views[a]=null)},highlight:function(a,b,c){var d=this.getHighlighterId(b,c);if(!this._views[d]){var e,f=joint.util.defaults(c||{},this.defaultOptions),g=V(b);try{var h=g.convertToPathData()}catch(a){e=g.bbox(!0),h=V.rectToPath(joint.util.assign({},f,e))}var i=V("path").attr({d:h,"pointer-events":"none","vector-effect":"non-scaling-stroke",fill:"none"}).attr(f.attrs),j=g.getTransformToElement(a.el),k=f.padding;if(k){e||(e=g.bbox(!0));var l=e.x+e.width/2,m=e.y+e.height/2;e=V.transformRect(e,j);var n=Math.max(e.width,1),o=Math.max(e.height,1),p=(n+k)/n,q=(o+k)/o,r=V.createSVGMatrix({a:p,b:0,c:0,d:q,e:l-p*l,f:m-q*m});j=j.multiply(r)}i.transform(j);var s=this._views[d]=new joint.mvc.View({svgElement:!0,className:"highlight-stroke",el:i.node}),t=this.removeHighlighter.bind(this,d),u=a.model;s.listenTo(u,"remove",t),s.listenTo(u.graph,"reset",t),a.vel.append(i)}},unhighlight:function(a,b,c){this.removeHighlighter(this.getHighlighterId(b,c))}},function(a,b){function c(a){return function(c,d,e,f){var g=!!f.rotate,h=g?c.getNodeUnrotatedBBox(d):c.getNodeBBox(d),i=h[a](),j=f.dx;if(j){var k=b.isPercentage(j);j=parseFloat(j),isFinite(j)&&(k&&(j/=100,j*=h.width),i.x+=j)}var l=f.dy;if(l){var m=b.isPercentage(l);l=parseFloat(l),isFinite(l)&&(m&&(l/=100,l*=h.height),i.y+=l)}return g?i.rotate(c.model.getBBox().center(),-c.model.angle()):i}}function d(a){return function(b,c,d,e){if(d instanceof Element){var f=this.paper.findView(d),g=f.getNodeBBox(d).center();return a.call(this,b,c,g,e)}return a.apply(this,arguments)}}function e(a,b,c,d){var e=a.model.angle(),f=a.getNodeBBox(b),h=f.center(),i=f.origin(),j=f.corner(),k=d.padding;if(isFinite(k)||(k=0),i.y+k<=c.y&&c.y<=j.y-k){var l=c.y-h.y;h.x+=0===e||180===e?0:1*l/Math.tan(g.toRad(e)),h.y+=l}else if(i.x+k<=c.x&&c.x<=j.x-k){var m=c.x-h.x;h.y+=90===e||270===e?0:m*Math.tan(g.toRad(e)),h.x+=m}return h}function f(a,b,c,d){var e,f,g,h=!!d.rotate;h?(e=a.getNodeUnrotatedBBox(b),g=a.model.getBBox().center(),f=a.model.angle()):e=a.getNodeBBox(b);var i=d.padding;isFinite(i)&&e.inflate(i),h&&c.rotate(g,f);var j,k=e.sideNearestToPoint(c);switch(k){case"left":j=e.leftMiddle();break;case"right":j=e.rightMiddle();break;case"top":j=e.topMiddle();break;case"bottom":j=e.bottomMiddle()}return h?j.rotate(g,-f):j}function h(a,b){var c=a.model,d=c.getBBox(),e=d.center(),f=c.angle(),h=a.findAttribute("port",b);if(h){portGroup=c.portProp(h,"group");var i=c.getPortsPositions(portGroup),j=new g.Point(i[h]).offset(d.origin());return j.rotate(e,-f),j}return e}a.anchors={center:c("center"),top:c("topMiddle"),bottom:c("bottomMiddle"),left:c("leftMiddle"),right:c("rightMiddle"),topLeft:c("origin"),topRight:c("topRight"),bottomLeft:c("bottomLeft"),bottomRight:c("corner"),perpendicular:d(e),midSide:d(f),modelCenter:h}}(joint,joint.util),function(a,b,c,d){function e(a,c){return 1===a.length?a[0]:b.sortBy(a,function(a){return a.squaredDistance(c)})[0]}function f(a,b,c){if(!isFinite(c))return a;var d=a.distance(b);return 0===c&&d>0?a:a.move(b,-Math.min(c,d-1))}function g(a){var b=a.getAttribute("stroke-width");return null===b?0:parseFloat(b)||0}function h(a,b,c,d){return f(a.end,a.start,d.offset)}function i(a,b,c,d){var h=b.getNodeBBox(c);d.stroke&&h.inflate(g(c)/2);var i=a.intersect(h),j=i?e(i,a.start):a.end;return f(j,a.start,d.offset)}function j(a,b,c,d){var h=b.model.angle();if(0===h)return i(a,b,c,d);var j=b.getNodeUnrotatedBBox(c);d.stroke&&j.inflate(g(c)/2);var k=j.center(),l=a.clone().rotate(k,h),m=l.setLength(1e6).intersect(j),n=m?e(m,l.start).rotate(k,-h):a.end;return f(n,a.start,d.offset)}function k(a,h,i,j){var k,n,o=j.selector,p=a.end;if("string"==typeof o)k=h.findBySelector(o)[0];else if(Array.isArray(o))k=b.getByPath(i,o);else{k=i;do{var q=k.tagName.toUpperCase();if("G"===q)k=k.firstChild;else{if("TITLE"!==q)break;k=k.nextSibling}}while(k)}if(!(k instanceof Element))return p;var r=h.getNodeShape(k),s=h.getNodeMatrix(k),t=h.getRootTranslateMatrix(),u=h.getRootRotateMatrix(),v=t.multiply(u).multiply(s),w=v.inverse(),x=d.transformLine(a,w),y=x.start.clone(),z=h.getNodeData(k);if(j.insideout===!1){z[m]||(z[m]=r.bbox());var A=z[m];if(A.containsPoint(y))return p}var B;if(r instanceof c.Path){var C=j.precision||2;z[l]||(z[l]=r.getSegmentSubdivisions({precision:C})),segmentSubdivisions=z[l],B={precision:C,segmentSubdivisions:z[l]}}j.extrapolate===!0&&x.setLength(1e6),n=x.intersect(r,B),n?d.isArray(n)&&(n=e(n,y)):j.sticky===!0&&(n=r instanceof c.Rect?r.pointNearestToPoint(y):r instanceof c.Ellipse?r.intersectionWithLineFromCenterToPoint(y):r.closestPoint(y,B));var D=n?d.transformPoint(n,v):p,E=j.offset||0;return j.stroke&&(E+=g(k)/2),f(D,a.start,E)}var l="segmentSubdivisons",m="shapeBBox";a.connectionPoints={anchor:h,bbox:i,rectangle:j,boundary:k}}(joint,joint.util,g,V),function(a,b){function c(a,b){return 0===b?"0%":Math.round(a/b*100)+"%"}function d(a){return function(b,d,e,f){var g=d.model.angle(),h=d.getNodeUnrotatedBBox(e),i=d.model.getBBox().center();f.rotate(i,g);var j=f.x-h.x,k=f.y-h.y;return a&&(j=c(j,h.width),k=c(k,h.height)),b.anchor={name:"topLeft",args:{dx:j,dy:k,rotate:!0}},b}}a.connectionStrategies={useDefaults:b.noop,pinAbsolute:d(!1),pinRelative:d(!0)}}(joint,joint.util),function(a,b,c,d){function e(b,c,d){var e=a.connectionStrategies.pinRelative.call(this.paper,{},c,d,b,this.model);return e.anchor}function f(a,b,c,d,e,f){var g=f.options.snapRadius,h="source"===d,i=h?0:-1,j=this.model.vertex(i)||this.getEndAnchor(h?"target":"source");return j&&(Math.abs(j.x-a.x)0?c[a-1]:b.sourceAnchor,f=a0){var d=this.getNeighborPoints(b),e=d.prev,f=d.next;Math.abs(a.x-e.x)'}),joint.shapes.erd.Entity.define("erd.WeakEntity",{attrs:{".inner":{display:"auto"},text:{text:"Weak Entity"}}}),joint.dia.Element.define("erd.Relationship",{size:{width:80,height:80},attrs:{".outer":{fill:"#3498DB",stroke:"#2980B9","stroke-width":2,points:"40,0 80,40 40,80 0,40"},".inner":{fill:"#3498DB",stroke:"#2980B9","stroke-width":2,points:"40,5 75,40 40,75 5,40",display:"none"},text:{text:"Relationship","font-family":"Arial","font-size":12,"ref-x":.5,"ref-y":.5,"y-alignment":"middle","text-anchor":"middle"}}},{markup:''}),joint.shapes.erd.Relationship.define("erd.IdentifyingRelationship",{attrs:{".inner":{display:"auto"},text:{text:"Identifying"}}}),joint.dia.Element.define("erd.Attribute",{size:{width:100,height:50},attrs:{ellipse:{transform:"translate(50, 25)"},".outer":{stroke:"#D35400","stroke-width":2,cx:0,cy:0,rx:50,ry:25,fill:"#E67E22"},".inner":{stroke:"#D35400","stroke-width":2,cx:0,cy:0,rx:45,ry:20,fill:"#E67E22",display:"none"},text:{"font-family":"Arial","font-size":14,"ref-x":.5,"ref-y":.5,"y-alignment":"middle","text-anchor":"middle"}}},{markup:''}),joint.shapes.erd.Attribute.define("erd.Multivalued",{attrs:{".inner":{display:"block"},text:{text:"multivalued"}}}),joint.shapes.erd.Attribute.define("erd.Derived",{attrs:{".outer":{"stroke-dasharray":"3,5"},text:{text:"derived"}}}),joint.shapes.erd.Attribute.define("erd.Key",{attrs:{ellipse:{"stroke-width":4},text:{text:"key","font-weight":"800","text-decoration":"underline"}}}),joint.shapes.erd.Attribute.define("erd.Normal",{attrs:{text:{text:"Normal"}}}),joint.dia.Element.define("erd.ISA",{type:"erd.ISA",size:{width:100,height:50},attrs:{polygon:{points:"0,0 50,50 100,0",fill:"#F1C40F",stroke:"#F39C12","stroke-width":2},text:{text:"ISA","font-size":18,"ref-x":.5,"ref-y":.3,"y-alignment":"middle","text-anchor":"middle"}}},{markup:''}),joint.dia.Link.define("erd.Line",{},{cardinality:function(a){this.set("labels",[{position:-20,attrs:{text:{dy:-8,text:a}}}])}}); joint.shapes.basic.Circle.define("fsa.State",{attrs:{circle:{"stroke-width":3},text:{"font-weight":"800"}}}),joint.dia.Element.define("fsa.StartState",{size:{width:20,height:20},attrs:{circle:{transform:"translate(10, 10)",r:10,fill:"#000000"}}},{markup:''}),joint.dia.Element.define("fsa.EndState",{size:{width:20,height:20},attrs:{".outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#000000"},".inner":{transform:"translate(10, 10)",r:6,fill:"#000000"}}},{markup:''}),joint.dia.Link.define("fsa.Arrow",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z"}},smooth:!0}); joint.dia.Element.define("org.Member",{size:{width:180,height:70},attrs:{rect:{width:170,height:60},".card":{fill:"#FFFFFF",stroke:"#000000","stroke-width":2,"pointer-events":"visiblePainted",rx:10,ry:10},image:{width:48,height:48,ref:".card","ref-x":10,"ref-y":5},".rank":{"text-decoration":"underline",ref:".card","ref-x":.9,"ref-y":.2,"font-family":"Courier New","font-size":14,"text-anchor":"end"},".name":{"font-weight":"800",ref:".card","ref-x":.9,"ref-y":.6,"font-family":"Courier New","font-size":14,"text-anchor":"end"}}},{markup:''}),joint.dia.Link.define("org.Arrow",{source:{selector:".card"},target:{selector:".card"},attrs:{".connection":{stroke:"#585858","stroke-width":3}},z:-1}); joint.shapes.basic.Generic.define("chess.KingWhite",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.KingBlack",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.QueenWhite",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.QueenBlack",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.RookWhite",{size:{width:32,height:34}},{markup:' '}),joint.shapes.basic.Generic.define("chess.RookBlack",{size:{width:32,height:34}},{markup:' '}),joint.shapes.basic.Generic.define("chess.BishopWhite",{size:{width:38,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.BishopBlack",{size:{width:38,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.KnightWhite",{size:{width:38,height:37}},{markup:' '}),joint.shapes.basic.Generic.define("chess.KnightBlack",{size:{width:38,height:37}},{markup:' '}),joint.shapes.basic.Generic.define("chess.PawnWhite",{size:{width:28,height:33}},{markup:''}),joint.shapes.basic.Generic.define("chess.PawnBlack",{size:{width:28,height:33}},{markup:''}); joint.shapes.basic.Generic.define("pn.Place",{size:{width:50,height:50},attrs:{".root":{r:25,fill:"#ffffff",stroke:"#000000",transform:"translate(25, 25)"},".label":{"text-anchor":"middle","ref-x":.5,"ref-y":-20,ref:".root",fill:"#000000","font-size":12},".tokens > circle":{fill:"#000000",r:5},".tokens.one > circle":{transform:"translate(25, 25)"},".tokens.two > circle:nth-child(1)":{transform:"translate(19, 25)"},".tokens.two > circle:nth-child(2)":{transform:"translate(31, 25)"},".tokens.three > circle:nth-child(1)":{transform:"translate(18, 29)"},".tokens.three > circle:nth-child(2)":{transform:"translate(25, 19)"},".tokens.three > circle:nth-child(3)":{transform:"translate(32, 29)"},".tokens.alot > text":{transform:"translate(25, 18)","text-anchor":"middle",fill:"#000000"}}},{markup:''}),joint.shapes.pn.PlaceView=joint.dia.ElementView.extend({},{initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.model.on("change:tokens",function(){this.renderTokens(),this.update()},this)},render:function(){joint.dia.ElementView.prototype.render.apply(this,arguments),this.renderTokens(),this.update()},renderTokens:function(){var a=this.$(".tokens").empty();a[0].className.baseVal="tokens";var b=this.model.get("tokens");if(b)switch(b){case 1:a[0].className.baseVal+=" one",a.append(V("").node);break;case 2:a[0].className.baseVal+=" two",a.append(V("").node,V("").node);break;case 3:a[0].className.baseVal+=" three",a.append(V("").node,V("").node,V("").node);break;default:a[0].className.baseVal+=" alot",a.append(V("").text(b+"").node)}}}),joint.shapes.basic.Generic.define("pn.Transition",{size:{width:12,height:50},attrs:{rect:{width:12,height:50,fill:"#000000",stroke:"#000000"},".label":{"text-anchor":"middle","ref-x":.5,"ref-y":-20,ref:"rect",fill:"#000000","font-size":12}}},{markup:''}),joint.dia.Link.define("pn.Link",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z"}}}); joint.shapes.basic.Generic.define("devs.Model",{inPorts:[],outPorts:[],size:{width:80,height:80},attrs:{".":{magnet:!1},".label":{text:"Model","ref-x":.5,"ref-y":10,"font-size":18,"text-anchor":"middle",fill:"#000"},".body":{"ref-width":"100%","ref-height":"100%",stroke:"#000"}},ports:{groups:{in:{position:{name:"left"},attrs:{".port-label":{fill:"#000"},".port-body":{fill:"#fff",stroke:"#000",r:10,magnet:!0}},label:{position:{name:"left",args:{y:10}}}},out:{position:{name:"right"},attrs:{".port-label":{fill:"#000"},".port-body":{fill:"#fff",stroke:"#000",r:10,magnet:!0}},label:{position:{name:"right",args:{y:10}}}}}}},{markup:'',portMarkup:'',portLabelMarkup:'',initialize:function(){joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments),this.on("change:inPorts change:outPorts",this.updatePortItems,this),this.updatePortItems()},updatePortItems:function(a,b,c){var d=joint.util.uniq(this.get("inPorts")),e=joint.util.difference(joint.util.uniq(this.get("outPorts")),d),f=this.createPortItems("in",d),g=this.createPortItems("out",e);this.prop("ports/items",f.concat(g),joint.util.assign({rewrite:!0},c))},createPortItem:function(a,b){return{id:b,group:a,attrs:{".port-label":{text:b}}}},createPortItems:function(a,b){return joint.util.toArray(b).map(this.createPortItem.bind(this,a))},_addGroupPort:function(a,b,c){var d=this.get(b);return this.set(b,Array.isArray(d)?d.concat(a):[a],c)},addOutPort:function(a,b){return this._addGroupPort(a,"outPorts",b)},addInPort:function(a,b){return this._addGroupPort(a,"inPorts",b)},_removeGroupPort:function(a,b,c){return this.set(b,joint.util.without(this.get(b),a),c)},removeOutPort:function(a,b){return this._removeGroupPort(a,"outPorts",b)},removeInPort:function(a,b){return this._removeGroupPort(a,"inPorts",b)},_changeGroup:function(a,b,c){return this.prop("ports/groups/"+a,joint.util.isObject(b)?b:{},c)},changeInGroup:function(a,b){return this._changeGroup("in",a,b)},changeOutGroup:function(a,b){return this._changeGroup("out",a,b)}}),joint.shapes.devs.Model.define("devs.Atomic",{size:{width:80,height:80},attrs:{".label":{text:"Atomic"}}}),joint.shapes.devs.Model.define("devs.Coupled",{size:{width:200,height:300},attrs:{".label":{text:"Coupled"}}}),joint.dia.Link.define("devs.Link",{attrs:{".connection":{"stroke-width":2}}}); -joint.shapes.basic.Generic.define("uml.Class",{attrs:{rect:{width:200},".uml-class-name-rect":{stroke:"black","stroke-width":2,fill:"#3498db"},".uml-class-attrs-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-methods-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-name-text":{ref:".uml-class-name-rect","ref-y":.5,"ref-x":.5,"text-anchor":"middle","y-alignment":"middle","font-weight":"bold",fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-attrs-text":{ref:".uml-class-attrs-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-methods-text":{ref:".uml-class-methods-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"}},name:[],attributes:[],methods:[]},{markup:['','','',"",'',""].join(""),initialize:function(){this.on("change:name change:attributes change:methods",function(){this.updateRectangles(),this.trigger("uml-update")},this),this.updateRectangles(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},getClassName:function(){return this.get("name")},updateRectangles:function(){var a=this.get("attrs"),b=[{type:"name",text:this.getClassName()},{type:"attrs",text:this.get("attributes")},{type:"methods",text:this.get("methods")}],c=0;b.forEach(function(b){var d=Array.isArray(b.text)?b.text:[b.text],e=20*d.length+20;a[".uml-class-"+b.type+"-text"].text=d.join("\n"),a[".uml-class-"+b.type+"-rect"].height=e,a[".uml-class-"+b.type+"-rect"].transform="translate(0,"+c+")",c+=e})}}),joint.shapes.uml.ClassView=joint.dia.ElementView.extend({},{initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"uml-update",function(){this.update(),this.resize()})}}),joint.shapes.uml.Class.define("uml.Abstract",{attrs:{".uml-class-name-rect":{fill:"#e74c3c"},".uml-class-attrs-rect":{fill:"#c0392b"},".uml-class-methods-rect":{fill:"#c0392b"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.AbstractView=joint.shapes.uml.ClassView,joint.shapes.uml.Class.define("uml.Interface",{attrs:{".uml-class-name-rect":{fill:"#f1c40f"},".uml-class-attrs-rect":{fill:"#f39c12"},".uml-class-methods-rect":{fill:"#f39c12"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.InterfaceView=joint.shapes.uml.ClassView,joint.dia.Link.define("uml.Generalization",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"}}}),joint.dia.Link.define("uml.Implementation",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"},".connection":{"stroke-dasharray":"3,3"}}}),joint.dia.Link.define("uml.Aggregation",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"white"}}}),joint.dia.Link.define("uml.Composition",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"black"}}}),joint.dia.Link.define("uml.Association"),joint.shapes.basic.Generic.define("uml.State",{attrs:{".uml-state-body":{width:200,height:200,rx:10,ry:10,fill:"#ecf0f1",stroke:"#bdc3c7","stroke-width":3},".uml-state-separator":{stroke:"#bdc3c7","stroke-width":2},".uml-state-name":{ref:".uml-state-body","ref-x":.5,"ref-y":5,"text-anchor":"middle",fill:"#000000","font-family":"Courier New","font-size":14},".uml-state-events":{ref:".uml-state-separator","ref-x":5,"ref-y":5,fill:"#000000","font-family":"Courier New","font-size":14}},name:"State",events:[]},{markup:['','','',"",'','','',""].join(""),initialize:function(){this.on({"change:name":this.updateName,"change:events":this.updateEvents,"change:size":this.updatePath},this),this.updateName(),this.updateEvents(),this.updatePath(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateName:function(){this.attr(".uml-state-name/text",this.get("name"))},updateEvents:function(){this.attr(".uml-state-events/text",this.get("events").join("\n"))},updatePath:function(){var a="M 0 20 L "+this.get("size").width+" 20";this.attr(".uml-state-separator/d",a,{silent:!0})}}),joint.shapes.basic.Circle.define("uml.StartState",{type:"uml.StartState",attrs:{circle:{fill:"#34495e",stroke:"#2c3e50","stroke-width":2,rx:1}}}),joint.shapes.basic.Generic.define("uml.EndState",{size:{width:20,height:20},attrs:{"circle.outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#2c3e50"},"circle.inner":{transform:"translate(10, 10)",r:6,fill:"#34495e"}}},{markup:''}),joint.dia.Link.define("uml.Transition",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z",fill:"#34495e",stroke:"#2c3e50"},".connection":{stroke:"#2c3e50"}}}); +joint.shapes.basic.Generic.define("uml.Class",{attrs:{rect:{width:200},".uml-class-name-rect":{stroke:"black","stroke-width":2,fill:"#3498db"},".uml-class-attrs-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-methods-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-name-text":{ref:".uml-class-name-rect","ref-y":.5,"ref-x":.5,"text-anchor":"middle","y-alignment":"middle","font-weight":"bold",fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-attrs-text":{ref:".uml-class-attrs-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-methods-text":{ref:".uml-class-methods-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"}},name:[],attributes:[],methods:[]},{markup:['','','',"",'',""].join(""),initialize:function(){this.on("change:name change:attributes change:methods",function(){this.updateRectangles(),this.trigger("uml-update")},this),this.updateRectangles(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},getClassName:function(){return this.get("name")},updateRectangles:function(){var a=this.get("attrs"),b=[{type:"name",text:this.getClassName()},{type:"attrs",text:this.get("attributes")},{type:"methods",text:this.get("methods")}],c=0;b.forEach(function(b){var d=Array.isArray(b.text)?b.text:[b.text],e=20*d.length+20;a[".uml-class-"+b.type+"-text"].text=d.join("\n"),a[".uml-class-"+b.type+"-rect"].height=e,a[".uml-class-"+b.type+"-rect"].transform="translate(0,"+c+")",c+=e})}}),joint.shapes.uml.ClassView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"uml-update",function(){this.update(),this.resize()})}}),joint.shapes.uml.Class.define("uml.Abstract",{attrs:{".uml-class-name-rect":{fill:"#e74c3c"},".uml-class-attrs-rect":{fill:"#c0392b"},".uml-class-methods-rect":{fill:"#c0392b"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.AbstractView=joint.shapes.uml.ClassView,joint.shapes.uml.Class.define("uml.Interface",{attrs:{".uml-class-name-rect":{fill:"#f1c40f"},".uml-class-attrs-rect":{fill:"#f39c12"},".uml-class-methods-rect":{fill:"#f39c12"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.InterfaceView=joint.shapes.uml.ClassView,joint.dia.Link.define("uml.Generalization",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"}}}),joint.dia.Link.define("uml.Implementation",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"},".connection":{"stroke-dasharray":"3,3"}}}),joint.dia.Link.define("uml.Aggregation",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"white"}}}),joint.dia.Link.define("uml.Composition",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"black"}}}),joint.dia.Link.define("uml.Association"),joint.shapes.basic.Generic.define("uml.State",{attrs:{".uml-state-body":{width:200,height:200,rx:10,ry:10,fill:"#ecf0f1",stroke:"#bdc3c7","stroke-width":3},".uml-state-separator":{stroke:"#bdc3c7","stroke-width":2},".uml-state-name":{ref:".uml-state-body","ref-x":.5,"ref-y":5,"text-anchor":"middle",fill:"#000000","font-family":"Courier New","font-size":14},".uml-state-events":{ref:".uml-state-separator","ref-x":5,"ref-y":5,fill:"#000000","font-family":"Courier New","font-size":14}},name:"State",events:[]},{markup:['','','',"",'','','',""].join(""),initialize:function(){this.on({"change:name":this.updateName,"change:events":this.updateEvents,"change:size":this.updatePath},this),this.updateName(),this.updateEvents(),this.updatePath(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateName:function(){this.attr(".uml-state-name/text",this.get("name"))},updateEvents:function(){this.attr(".uml-state-events/text",this.get("events").join("\n"))},updatePath:function(){var a="M 0 20 L "+this.get("size").width+" 20";this.attr(".uml-state-separator/d",a,{silent:!0})}}),joint.shapes.basic.Circle.define("uml.StartState",{type:"uml.StartState",attrs:{circle:{fill:"#34495e",stroke:"#2c3e50","stroke-width":2,rx:1}}}),joint.shapes.basic.Generic.define("uml.EndState",{size:{width:20,height:20},attrs:{"circle.outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#2c3e50"},"circle.inner":{transform:"translate(10, 10)",r:6,fill:"#34495e"}}},{markup:''}),joint.dia.Link.define("uml.Transition",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z",fill:"#34495e",stroke:"#2c3e50"},".connection":{stroke:"#2c3e50"}}}); joint.shapes.basic.Generic.define("logic.Gate",{size:{width:80,height:40},attrs:{".":{magnet:!1},".body":{width:100,height:50},circle:{r:7,stroke:"black",fill:"transparent","stroke-width":2}}},{operation:function(){return!0}}),joint.shapes.logic.Gate.define("logic.IO",{size:{width:60,height:30},attrs:{".body":{fill:"white",stroke:"black","stroke-width":2},".wire":{ref:".body","ref-y":.5,stroke:"black"},text:{fill:"black",ref:".body","ref-x":.5,"ref-y":.5,"y-alignment":"middle","text-anchor":"middle","font-weight":"bold","font-variant":"small-caps","text-transform":"capitalize","font-size":"14px"}}},{markup:''}),joint.shapes.logic.IO.define("logic.Input",{attrs:{".wire":{"ref-dx":0,d:"M 0 0 L 23 0"},circle:{ref:".body","ref-dx":30,"ref-y":.5,magnet:!0,class:"output",port:"out"},text:{text:"input"}}}),joint.shapes.logic.IO.define("logic.Output",{attrs:{".wire":{"ref-x":0,d:"M 0 0 L -23 0"},circle:{ref:".body","ref-x":-30,"ref-y":.5,magnet:"passive",class:"input",port:"in"},text:{text:"output"}}}),joint.shapes.logic.Gate.define("logic.Gate11",{attrs:{".input":{ref:".body","ref-x":-2,"ref-y":.5,magnet:"passive",port:"in"},".output":{ref:".body","ref-dx":2,"ref-y":.5,magnet:!0,port:"out"}}},{markup:''}),joint.shapes.logic.Gate.define("logic.Gate21",{attrs:{".input1":{ref:".body","ref-x":-2,"ref-y":.3,magnet:"passive",port:"in1"},".input2":{ref:".body","ref-x":-2,"ref-y":.7,magnet:"passive",port:"in2"},".output":{ref:".body","ref-dx":2,"ref-y":.5,magnet:!0,port:"out"}}},{markup:''}),joint.shapes.logic.Gate11.define("logic.Repeater",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PVCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjgiCiAgICAgaW5rc2NhcGU6Y3g9Ijg0LjY4NTM1MiIKICAgICBpbmtzY2FwZTpjeT0iMTUuMjg4NjI4IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzIuMTU2OTEsMjUgTCA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAyOS4wNDM0NzgsMjUgTCA1LjA0MzQ3ODEsMjUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSAyOC45Njg3NSwyLjU5Mzc1IEwgMjguOTY4NzUsNSBMIDI4Ljk2ODc1LDQ1IEwgMjguOTY4NzUsNDcuNDA2MjUgTCAzMS4xMjUsNDYuMzQzNzUgTCA3Mi4xNTYyNSwyNi4zNDM3NSBMIDcyLjE1NjI1LDIzLjY1NjI1IEwgMzEuMTI1LDMuNjU2MjUgTCAyOC45Njg3NSwyLjU5Mzc1IHogTSAzMS45Njg3NSw3LjQwNjI1IEwgNjguMDkzNzUsMjUgTCAzMS45Njg3NSw0Mi41OTM3NSBMIDMxLjk2ODc1LDcuNDA2MjUgeiIKICAgICAgIGlkPSJwYXRoMjYzOCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjY2NjYyIgLz4KICA8L2c+Cjwvc3ZnPgo="}}},{operation:function(a){return a}}),joint.shapes.logic.Gate11.define("logic.Not",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PVCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjgiCiAgICAgaW5rc2NhcGU6Y3g9Ijg0LjY4NTM1MiIKICAgICBpbmtzY2FwZTpjeT0iMTUuMjg4NjI4IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzkuMTU2OTEsMjUgTCA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAyOS4wNDM0NzgsMjUgTCA1LjA0MzQ3ODEsMjUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSAyOC45Njg3NSwyLjU5Mzc1IEwgMjguOTY4NzUsNSBMIDI4Ljk2ODc1LDQ1IEwgMjguOTY4NzUsNDcuNDA2MjUgTCAzMS4xMjUsNDYuMzQzNzUgTCA3Mi4xNTYyNSwyNi4zNDM3NSBMIDcyLjE1NjI1LDIzLjY1NjI1IEwgMzEuMTI1LDMuNjU2MjUgTCAyOC45Njg3NSwyLjU5Mzc1IHogTSAzMS45Njg3NSw3LjQwNjI1IEwgNjguMDkzNzUsMjUgTCAzMS45Njg3NSw0Mi41OTM3NSBMIDMxLjk2ODc1LDcuNDA2MjUgeiIKICAgICAgIGlkPSJwYXRoMjYzOCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjY2NjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDI2NzEiCiAgICAgICBzb2RpcG9kaTpjeD0iNzYiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA4MCwyNSBBIDQsNCAwIDEgMSA3MiwyNSBBIDQsNCAwIDEgMSA4MCwyNSB6IgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEsMCkiIC8+CiAgPC9nPgo8L3N2Zz4K"}}},{operation:function(a){return!a}}),joint.shapes.logic.Gate21.define("logic.Or",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik9SIEFOU0kuc3ZnIgogICBpbmtzY2FwZTpvdXRwdXRfZXh0ZW5zaW9uPSJvcmcuaW5rc2NhcGUub3V0cHV0LnN2Zy5pbmtzY2FwZSI+CiAgPGRlZnMKICAgICBpZD0iZGVmczQiPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjUwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjI1IDogMTAgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjcxNCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iMSA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMC41IDogMC4zMzMzMzMzMyA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyODA2IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyODE5IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjM3Mi4wNDcyNCA6IDM1MC43ODczOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI3NDQuMDk0NDggOiA1MjYuMTgxMDkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzc3IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49Ijc1IDogNDAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iMTUwIDogNjAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDYwIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTMyNzUiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNTAgOiAzMy4zMzMzMzMgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iMTAwIDogNTAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDUwIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTU1MzMiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzIgOiAyMS4zMzMzMzMgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNjQgOiAzMiA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMzIgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjU1NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDE2LjY2NjY2NyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDI1IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAyNSA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iNCIKICAgICBpbmtzY2FwZTpjeD0iMTEzLjAwMDM5IgogICAgIGlua3NjYXBlOmN5PSIxMi44OTM3MzEiCiAgICAgaW5rc2NhcGU6ZG9jdW1lbnQtdW5pdHM9InB4IgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImcyNTYwIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTpncmlkLWJib3g9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1wb2ludHM9InRydWUiCiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAwMDAiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxMzk5IgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9Ijg3NCIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMzciCiAgICAgaW5rc2NhcGU6d2luZG93LXk9Ii00IgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNzAsMjUgYyAyMCwwIDI1LDAgMjUsMCIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMSwxNSA1LDE1IgogICAgICAgaWQ9InBhdGgzMDYxIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzIsMzUgNSwzNSIKICAgICAgIGlkPSJwYXRoMzk0NCIgLz4KICAgIDxnCiAgICAgICBpZD0iZzI1NjAiCiAgICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2LjUsLTM5LjUpIj4KICAgICAgPHBhdGgKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjQwNjI1LDQ0LjUgTCAtMC40MDYyNSw0Ni45Mzc1IEMgLTAuNDA2MjUsNDYuOTM3NSA1LjI1LDUzLjkzNzU0OSA1LjI1LDY0LjUgQyA1LjI1LDc1LjA2MjQ1MSAtMC40MDYyNSw4Mi4wNjI1IC0wLjQwNjI1LDgyLjA2MjUgTCAtMi40MDYyNSw4NC41IEwgMC43NSw4NC41IEwgMTQuNzUsODQuNSBDIDE3LjE1ODA3Niw4NC41MDAwMDEgMjIuNDM5Njk5LDg0LjUyNDUxNCAyOC4zNzUsODIuMDkzNzUgQyAzNC4zMTAzMDEsNzkuNjYyOTg2IDQwLjkxMTUzNiw3NC43NTA0ODQgNDYuMDYyNSw2NS4yMTg3NSBMIDQ0Ljc1LDY0LjUgTCA0Ni4wNjI1LDYzLjc4MTI1IEMgMzUuNzU5Mzg3LDQ0LjcxNTU5IDE5LjUwNjU3NCw0NC41IDE0Ljc1LDQ0LjUgTCAwLjc1LDQ0LjUgTCAtMi40MDYyNSw0NC41IHogTSAzLjQ2ODc1LDQ3LjUgTCAxNC43NSw0Ny41IEMgMTkuNDM0MTczLDQ3LjUgMzMuMDM2ODUsNDcuMzY5NzkzIDQyLjcxODc1LDY0LjUgQyAzNy45NTE5NjQsNzIuOTI5MDc1IDMyLjE5NzQ2OSw3Ny4xODM5MSAyNyw3OS4zMTI1IEMgMjEuNjM5MzM5LDgxLjUwNzkyNCAxNy4xNTgwNzUsODEuNTAwMDAxIDE0Ljc1LDgxLjUgTCAzLjUsODEuNSBDIDUuMzczNTg4NCw3OC4zOTE1NjYgOC4yNSw3Mi40NTA2NSA4LjI1LDY0LjUgQyA4LjI1LDU2LjUyNjY0NiA1LjM0MTQ2ODYsNTAuNTk5ODE1IDMuNDY4NzUsNDcuNSB6IgogICAgICAgICBpZD0icGF0aDQ5NzMiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NzY2NjY3NjY2NjY2NjY2NzY2NzYyIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo="}}},{operation:function(a,b){return a||b}}),joint.shapes.logic.Gate21.define("logic.And",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IkFORCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgPC9kZWZzPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSI4IgogICAgIGlua3NjYXBlOmN4PSI1Ni42OTgzNDgiCiAgICAgaW5rc2NhcGU6Y3k9IjI1LjMyNjg5OSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDcwLDI1IGMgMjAsMCAyNSwwIDI1LDAiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEsMTUgNSwxNSIKICAgICAgIGlkPSJwYXRoMzA2MSIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjk5OTk5OTg4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMyLDM1IDUsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTptZWRpdW07Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDt0ZXh0LWluZGVudDowO3RleHQtYWxpZ246c3RhcnQ7dGV4dC1kZWNvcmF0aW9uOm5vbmU7bGluZS1oZWlnaHQ6bm9ybWFsO2xldHRlci1zcGFjaW5nOm5vcm1hbDt3b3JkLXNwYWNpbmc6bm9ybWFsO3RleHQtdHJhbnNmb3JtOm5vbmU7ZGlyZWN0aW9uOmx0cjtibG9jay1wcm9ncmVzc2lvbjp0Yjt3cml0aW5nLW1vZGU6bHItdGI7dGV4dC1hbmNob3I6c3RhcnQ7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDozO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGU7Zm9udC1mYW1pbHk6Qml0c3RyZWFtIFZlcmEgU2FuczstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOkJpdHN0cmVhbSBWZXJhIFNhbnMiCiAgICAgICBkPSJNIDMwLDUgTCAzMCw2LjQyODU3MTQgTCAzMCw0My41NzE0MjkgTCAzMCw0NSBMIDMxLjQyODU3MSw0NSBMIDUwLjQ3NjE5LDQ1IEMgNjEuNzQ0MDk4LDQ1IDcwLjQ3NjE5LDM1Ljk5OTk1NSA3MC40NzYxOSwyNSBDIDcwLjQ3NjE5LDE0LjAwMDA0NSA2MS43NDQwOTksNS4wMDAwMDAyIDUwLjQ3NjE5LDUgQyA1MC40NzYxOSw1IDUwLjQ3NjE5LDUgMzEuNDI4NTcxLDUgTCAzMCw1IHogTSAzMi44NTcxNDMsNy44NTcxNDI5IEMgNDAuODM0MjY0LDcuODU3MTQyOSA0NS45MTgzNjgsNy44NTcxNDI5IDQ4LjA5NTIzOCw3Ljg1NzE0MjkgQyA0OS4yODU3MTQsNy44NTcxNDI5IDQ5Ljg4MDk1Miw3Ljg1NzE0MjkgNTAuMTc4NTcxLDcuODU3MTQyOSBDIDUwLjMyNzM4MSw3Ljg1NzE0MjkgNTAuNDA5MjI3LDcuODU3MTQyOSA1MC40NDY0MjksNy44NTcxNDI5IEMgNTAuNDY1MDI5LDcuODU3MTQyOSA1MC40NzE1NDMsNy44NTcxNDI5IDUwLjQ3NjE5LDcuODU3MTQyOSBDIDYwLjIzNjg1Myw3Ljg1NzE0MyA2Ny4xNDI4NTcsMTUuNDk3MDk4IDY3LjE0Mjg1NywyNSBDIDY3LjE0Mjg1NywzNC41MDI5MDIgNTkuNzYwNjYyLDQyLjE0Mjg1NyA1MCw0Mi4xNDI4NTcgTCAzMi44NTcxNDMsNDIuMTQyODU3IEwgMzIuODU3MTQzLDcuODU3MTQyOSB6IgogICAgICAgaWQ9InBhdGgyODg0IgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NjY2NzY2NjY3Nzc3NzY2NjIiAvPgogIDwvZz4KPC9zdmc+Cg=="}}},{operation:function(a,b){return a&&b}}),joint.shapes.logic.Gate21.define("logic.Nor",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PUiBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjEiCiAgICAgaW5rc2NhcGU6Y3g9Ijc4LjY3NzY0NCIKICAgICBpbmtzY2FwZTpjeT0iMjIuMTAyMzQ0IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjM3IgogICAgIGlua3NjYXBlOndpbmRvdy15PSItNCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDc5LDI1IEMgOTksMjUgOTUsMjUgOTUsMjUiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEsMTUgNSwxNSIKICAgICAgIGlkPSJwYXRoMzA2MSIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjk5OTk5OTg4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMyLDM1IDUsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8ZwogICAgICAgaWQ9ImcyNTYwIgogICAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNi41LC0zOS41KSI+CiAgICAgIDxwYXRoCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi40MDYyNSw0NC41IEwgLTAuNDA2MjUsNDYuOTM3NSBDIC0wLjQwNjI1LDQ2LjkzNzUgNS4yNSw1My45Mzc1NDkgNS4yNSw2NC41IEMgNS4yNSw3NS4wNjI0NTEgLTAuNDA2MjUsODIuMDYyNSAtMC40MDYyNSw4Mi4wNjI1IEwgLTIuNDA2MjUsODQuNSBMIDAuNzUsODQuNSBMIDE0Ljc1LDg0LjUgQyAxNy4xNTgwNzYsODQuNTAwMDAxIDIyLjQzOTY5OSw4NC41MjQ1MTQgMjguMzc1LDgyLjA5Mzc1IEMgMzQuMzEwMzAxLDc5LjY2Mjk4NiA0MC45MTE1MzYsNzQuNzUwNDg0IDQ2LjA2MjUsNjUuMjE4NzUgTCA0NC43NSw2NC41IEwgNDYuMDYyNSw2My43ODEyNSBDIDM1Ljc1OTM4Nyw0NC43MTU1OSAxOS41MDY1NzQsNDQuNSAxNC43NSw0NC41IEwgMC43NSw0NC41IEwgLTIuNDA2MjUsNDQuNSB6IE0gMy40Njg3NSw0Ny41IEwgMTQuNzUsNDcuNSBDIDE5LjQzNDE3Myw0Ny41IDMzLjAzNjg1LDQ3LjM2OTc5MyA0Mi43MTg3NSw2NC41IEMgMzcuOTUxOTY0LDcyLjkyOTA3NSAzMi4xOTc0NjksNzcuMTgzOTEgMjcsNzkuMzEyNSBDIDIxLjYzOTMzOSw4MS41MDc5MjQgMTcuMTU4MDc1LDgxLjUwMDAwMSAxNC43NSw4MS41IEwgMy41LDgxLjUgQyA1LjM3MzU4ODQsNzguMzkxNTY2IDguMjUsNzIuNDUwNjUgOC4yNSw2NC41IEMgOC4yNSw1Ni41MjY2NDYgNS4zNDE0Njg2LDUwLjU5OTgxNSAzLjQ2ODc1LDQ3LjUgeiIKICAgICAgICAgaWQ9InBhdGg0OTczIgogICAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjc2NjY2NzY2NjY2NjY2Njc2Njc2MiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIHNvZGlwb2RpOnR5cGU9ImFyYyIKICAgICAgICAgc3R5bGU9ImZpbGw6bm9uZTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgICBpZD0icGF0aDI2MDQiCiAgICAgICAgIHNvZGlwb2RpOmN4PSI3NSIKICAgICAgICAgc29kaXBvZGk6Y3k9IjI1IgogICAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgICAgc29kaXBvZGk6cnk9IjQiCiAgICAgICAgIGQ9Ik0gNzksMjUgQSA0LDQgMCAxIDEgNzEsMjUgQSA0LDQgMCAxIDEgNzksMjUgeiIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI2LjUsMzkuNSkiIC8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K" }}},{operation:function(a,b){return!(a||b)}}),joint.shapes.logic.Gate21.define("logic.Nand",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5BTkQgQU5TSS5zdmciCiAgIGlua3NjYXBlOm91dHB1dF9leHRlbnNpb249Im9yZy5pbmtzY2FwZS5vdXRwdXQuc3ZnLmlua3NjYXBlIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCI+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxMCA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzE0IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIwLjUgOiAwLjMzMzMzMzMzIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MDYiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MTkiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzcyLjA0NzI0IDogMzUwLjc4NzM5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9Ijc0NC4wOTQ0OCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTI2LjE4MTA5IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3NzciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNzUgOiA0MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxNTAgOiA2MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNjAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMzI3NSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI1MCA6IDMzLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxMDAgOiA1MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlNTUzMyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzMiA6IDIxLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI2NCA6IDMyIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAzMiA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iMTYiCiAgICAgaW5rc2NhcGU6Y3g9Ijc4LjI4MzMwNyIKICAgICBpbmtzY2FwZTpjeT0iMTYuNDQyODQzIgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzksMjUgQyA5MS44LDI1IDk1LDI1IDk1LDI1IgogICAgICAgaWQ9InBhdGgzMDU5IgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMxLDE1IDUsMTUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk4ODtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMiwzNSA1LDM1IgogICAgICAgaWQ9InBhdGgzOTQ0IiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmb250LXNpemU6bWVkaXVtO2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7dGV4dC1pbmRlbnQ6MDt0ZXh0LWFsaWduOnN0YXJ0O3RleHQtZGVjb3JhdGlvbjpub25lO2xpbmUtaGVpZ2h0Om5vcm1hbDtsZXR0ZXItc3BhY2luZzpub3JtYWw7d29yZC1zcGFjaW5nOm5vcm1hbDt0ZXh0LXRyYW5zZm9ybTpub25lO2RpcmVjdGlvbjpsdHI7YmxvY2stcHJvZ3Jlc3Npb246dGI7d3JpdGluZy1tb2RlOmxyLXRiO3RleHQtYW5jaG9yOnN0YXJ0O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MzttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO2ZvbnQtZmFtaWx5OkJpdHN0cmVhbSBWZXJhIFNhbnM7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpCaXRzdHJlYW0gVmVyYSBTYW5zIgogICAgICAgZD0iTSAzMCw1IEwgMzAsNi40Mjg1NzE0IEwgMzAsNDMuNTcxNDI5IEwgMzAsNDUgTCAzMS40Mjg1NzEsNDUgTCA1MC40NzYxOSw0NSBDIDYxLjc0NDA5OCw0NSA3MC40NzYxOSwzNS45OTk5NTUgNzAuNDc2MTksMjUgQyA3MC40NzYxOSwxNC4wMDAwNDUgNjEuNzQ0MDk5LDUuMDAwMDAwMiA1MC40NzYxOSw1IEMgNTAuNDc2MTksNSA1MC40NzYxOSw1IDMxLjQyODU3MSw1IEwgMzAsNSB6IE0gMzIuODU3MTQzLDcuODU3MTQyOSBDIDQwLjgzNDI2NCw3Ljg1NzE0MjkgNDUuOTE4MzY4LDcuODU3MTQyOSA0OC4wOTUyMzgsNy44NTcxNDI5IEMgNDkuMjg1NzE0LDcuODU3MTQyOSA0OS44ODA5NTIsNy44NTcxNDI5IDUwLjE3ODU3MSw3Ljg1NzE0MjkgQyA1MC4zMjczODEsNy44NTcxNDI5IDUwLjQwOTIyNyw3Ljg1NzE0MjkgNTAuNDQ2NDI5LDcuODU3MTQyOSBDIDUwLjQ2NTAyOSw3Ljg1NzE0MjkgNTAuNDcxNTQzLDcuODU3MTQyOSA1MC40NzYxOSw3Ljg1NzE0MjkgQyA2MC4yMzY4NTMsNy44NTcxNDMgNjcuMTQyODU3LDE1LjQ5NzA5OCA2Ny4xNDI4NTcsMjUgQyA2Ny4xNDI4NTcsMzQuNTAyOTAyIDU5Ljc2MDY2Miw0Mi4xNDI4NTcgNTAsNDIuMTQyODU3IEwgMzIuODU3MTQzLDQyLjE0Mjg1NyBMIDMyLjg1NzE0Myw3Ljg1NzE0MjkgeiIKICAgICAgIGlkPSJwYXRoMjg4NCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2Njc2NjY2Nzc3Nzc2NjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDQwMDgiCiAgICAgICBzb2RpcG9kaTpjeD0iNzUiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA3OSwyNSBBIDQsNCAwIDEgMSA3MSwyNSBBIDQsNCAwIDEgMSA3OSwyNSB6IiAvPgogIDwvZz4KPC9zdmc+Cg=="}}},{operation:function(a,b){return!(a&&b)}}),joint.shapes.logic.Gate21.define("logic.Xor",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IlhPUiBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjUuNjU2ODU0MiIKICAgICBpbmtzY2FwZTpjeD0iMjUuOTM4MTE2IgogICAgIGlua3NjYXBlOmN5PSIxNy4yMzAwNSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDcwLDI1IGMgMjAsMCAyNSwwIDI1LDAiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzAuMzg1NzE3LDE1IEwgNC45OTk5OTk4LDE1IgogICAgICAgaWQ9InBhdGgzMDYxIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5NzY7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEuMzYyMDkxLDM1IEwgNC45OTk5OTk4LDM1IgogICAgICAgaWQ9InBhdGgzOTQ0IiAvPgogICAgPGcKICAgICAgIGlkPSJnMjU2MCIKICAgICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYuNSwtMzkuNSkiPgogICAgICA8cGF0aAogICAgICAgICBpZD0icGF0aDM1MTYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi4yNSw4MS41MDAwMDUgQyAtMy44NDczNzQsODQuMTQ0NDA1IC00LjUsODQuNTAwMDA1IC00LjUsODQuNTAwMDA1IEwgLTguMTU2MjUsODQuNTAwMDA1IEwgLTYuMTU2MjUsODIuMDYyNTA1IEMgLTYuMTU2MjUsODIuMDYyNTA1IC0wLjUsNzUuMDYyNDUxIC0wLjUsNjQuNSBDIC0wLjUsNTMuOTM3NTQ5IC02LjE1NjI1LDQ2LjkzNzUgLTYuMTU2MjUsNDYuOTM3NSBMIC04LjE1NjI1LDQ0LjUgTCAtNC41LDQ0LjUgQyAtMy43MTg3NSw0NS40Mzc1IC0zLjA3ODEyNSw0Ni4xNTYyNSAtMi4yODEyNSw0Ny41IEMgLTAuNDA4NTMxLDUwLjU5OTgxNSAyLjUsNTYuNTI2NjQ2IDIuNSw2NC41IEMgMi41LDcyLjQ1MDY1IC0wLjM5NjY5Nyw3OC4zNzk0MjUgLTIuMjUsODEuNTAwMDA1IHoiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY3NjY2Njc2MiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi40MDYyNSw0NC41IEwgLTAuNDA2MjUsNDYuOTM3NSBDIC0wLjQwNjI1LDQ2LjkzNzUgNS4yNSw1My45Mzc1NDkgNS4yNSw2NC41IEMgNS4yNSw3NS4wNjI0NTEgLTAuNDA2MjUsODIuMDYyNSAtMC40MDYyNSw4Mi4wNjI1IEwgLTIuNDA2MjUsODQuNSBMIDAuNzUsODQuNSBMIDE0Ljc1LDg0LjUgQyAxNy4xNTgwNzYsODQuNTAwMDAxIDIyLjQzOTY5OSw4NC41MjQ1MTQgMjguMzc1LDgyLjA5Mzc1IEMgMzQuMzEwMzAxLDc5LjY2Mjk4NiA0MC45MTE1MzYsNzQuNzUwNDg0IDQ2LjA2MjUsNjUuMjE4NzUgTCA0NC43NSw2NC41IEwgNDYuMDYyNSw2My43ODEyNSBDIDM1Ljc1OTM4Nyw0NC43MTU1OSAxOS41MDY1NzQsNDQuNSAxNC43NSw0NC41IEwgMC43NSw0NC41IEwgLTIuNDA2MjUsNDQuNSB6IE0gMy40Njg3NSw0Ny41IEwgMTQuNzUsNDcuNSBDIDE5LjQzNDE3Myw0Ny41IDMzLjAzNjg1LDQ3LjM2OTc5MyA0Mi43MTg3NSw2NC41IEMgMzcuOTUxOTY0LDcyLjkyOTA3NSAzMi4xOTc0NjksNzcuMTgzOTEgMjcsNzkuMzEyNSBDIDIxLjYzOTMzOSw4MS41MDc5MjQgMTcuMTU4MDc1LDgxLjUwMDAwMSAxNC43NSw4MS41IEwgMy41LDgxLjUgQyA1LjM3MzU4ODQsNzguMzkxNTY2IDguMjUsNzIuNDUwNjUgOC4yNSw2NC41IEMgOC4yNSw1Ni41MjY2NDYgNS4zNDE0Njg2LDUwLjU5OTgxNSAzLjQ2ODc1LDQ3LjUgeiIKICAgICAgICAgaWQ9InBhdGg0OTczIgogICAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjc2NjY2NzY2NjY2NjY2Njc2Njc2MiIC8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K"}}},{operation:function(a,b){return(!a||b)&&(a||!b)}}),joint.shapes.logic.Gate21.define("logic.Xnor",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IlhOT1IgQU5TSS5zdmciCiAgIGlua3NjYXBlOm91dHB1dF9leHRlbnNpb249Im9yZy5pbmtzY2FwZS5vdXRwdXQuc3ZnLmlua3NjYXBlIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCI+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxMCA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzE0IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIwLjUgOiAwLjMzMzMzMzMzIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MDYiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MTkiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzcyLjA0NzI0IDogMzUwLjc4NzM5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9Ijc0NC4wOTQ0OCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTI2LjE4MTA5IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3NzciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNzUgOiA0MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxNTAgOiA2MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNjAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMzI3NSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI1MCA6IDMzLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxMDAgOiA1MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlNTUzMyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzMiA6IDIxLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI2NCA6IDMyIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAzMiA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNTU3IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjI1IDogMTYuNjY2NjY3IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjUwIDogMjUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDI1IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgPC9kZWZzPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSI0IgogICAgIGlua3NjYXBlOmN4PSI5NS43MjM2NiIKICAgICBpbmtzY2FwZTpjeT0iLTI2Ljc3NTAyMyIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjAwMDAwMDI0O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDc4LjMzMzMzMiwyNSBDIDkxLjY2NjY2NiwyNSA5NSwyNSA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk4ODtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMC4zODU3MTcsMTUgTCA0Ljk5OTk5OTgsMTUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk3NjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMS4zNjIwOTEsMzUgTCA0Ljk5OTk5OTgsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8ZwogICAgICAgaWQ9ImcyNTYwIgogICAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNi41LC0zOS41KSI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMzUxNiIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjI1LDgxLjUwMDAwNSBDIC0zLjg0NzM3NCw4NC4xNDQ0MDUgLTQuNSw4NC41MDAwMDUgLTQuNSw4NC41MDAwMDUgTCAtOC4xNTYyNSw4NC41MDAwMDUgTCAtNi4xNTYyNSw4Mi4wNjI1MDUgQyAtNi4xNTYyNSw4Mi4wNjI1MDUgLTAuNSw3NS4wNjI0NTEgLTAuNSw2NC41IEMgLTAuNSw1My45Mzc1NDkgLTYuMTU2MjUsNDYuOTM3NSAtNi4xNTYyNSw0Ni45Mzc1IEwgLTguMTU2MjUsNDQuNSBMIC00LjUsNDQuNSBDIC0zLjcxODc1LDQ1LjQzNzUgLTMuMDc4MTI1LDQ2LjE1NjI1IC0yLjI4MTI1LDQ3LjUgQyAtMC40MDg1MzEsNTAuNTk5ODE1IDIuNSw1Ni41MjY2NDYgMi41LDY0LjUgQyAyLjUsNzIuNDUwNjUgLTAuMzk2Njk3LDc4LjM3OTQyNSAtMi4yNSw4MS41MDAwMDUgeiIKICAgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Njc2NjY2NzYyIgLz4KICAgICAgPHBhdGgKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjQwNjI1LDQ0LjUgTCAtMC40MDYyNSw0Ni45Mzc1IEMgLTAuNDA2MjUsNDYuOTM3NSA1LjI1LDUzLjkzNzU0OSA1LjI1LDY0LjUgQyA1LjI1LDc1LjA2MjQ1MSAtMC40MDYyNSw4Mi4wNjI1IC0wLjQwNjI1LDgyLjA2MjUgTCAtMi40MDYyNSw4NC41IEwgMC43NSw4NC41IEwgMTQuNzUsODQuNSBDIDE3LjE1ODA3Niw4NC41MDAwMDEgMjIuNDM5Njk5LDg0LjUyNDUxNCAyOC4zNzUsODIuMDkzNzUgQyAzNC4zMTAzMDEsNzkuNjYyOTg2IDQwLjkxMTUzNiw3NC43NTA0ODQgNDYuMDYyNSw2NS4yMTg3NSBMIDQ0Ljc1LDY0LjUgTCA0Ni4wNjI1LDYzLjc4MTI1IEMgMzUuNzU5Mzg3LDQ0LjcxNTU5IDE5LjUwNjU3NCw0NC41IDE0Ljc1LDQ0LjUgTCAwLjc1LDQ0LjUgTCAtMi40MDYyNSw0NC41IHogTSAzLjQ2ODc1LDQ3LjUgTCAxNC43NSw0Ny41IEMgMTkuNDM0MTczLDQ3LjUgMzMuMDM2ODUsNDcuMzY5NzkzIDQyLjcxODc1LDY0LjUgQyAzNy45NTE5NjQsNzIuOTI5MDc1IDMyLjE5NzQ2OSw3Ny4xODM5MSAyNyw3OS4zMTI1IEMgMjEuNjM5MzM5LDgxLjUwNzkyNCAxNy4xNTgwNzUsODEuNTAwMDAxIDE0Ljc1LDgxLjUgTCAzLjUsODEuNSBDIDUuMzczNTg4NCw3OC4zOTE1NjYgOC4yNSw3Mi40NTA2NSA4LjI1LDY0LjUgQyA4LjI1LDU2LjUyNjY0NiA1LjM0MTQ2ODYsNTAuNTk5ODE1IDMuNDY4NzUsNDcuNSB6IgogICAgICAgICBpZD0icGF0aDQ5NzMiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NzY2NjY3NjY2NjY2NjY2NzY2NzYyIgLz4KICAgIDwvZz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDM1NTEiCiAgICAgICBzb2RpcG9kaTpjeD0iNzUiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA3OSwyNSBBIDQsNCAwIDEgMSA3MSwyNSBBIDQsNCAwIDEgMSA3OSwyNSB6IiAvPgogIDwvZz4KPC9zdmc+Cg=="}}},{operation:function(a,b){return(!a||!b)&&(a||b)}}),joint.dia.Link.define("logic.Wire",{attrs:{".connection":{"stroke-width":2},".marker-vertex":{r:7}},router:{name:"orthogonal"},connector:{name:"rounded",args:{radius:10}}},{arrowheadMarkup:['','',""].join(""),vertexMarkup:['','','','','',"Remove vertex.","","",""].join("")}); -if("object"==typeof exports)var graphlib=require("graphlib"),dagre=require("dagre");graphlib=graphlib||"undefined"!=typeof window&&window.graphlib,dagre=dagre||"undefined"!=typeof window&&window.dagre,joint.layout.DirectedGraph={exportElement:function(a){return a.size()},exportLink:function(a){var b=a.get("labelSize")||{},c={minLen:a.get("minLen")||1,weight:a.get("weight")||1,labelpos:a.get("labelPosition")||"c",labeloffset:a.get("labelOffset")||0,width:b.width||0,height:b.height||0};return c},importElement:function(a,b,c){var d=this.getCell(b),e=c.node(b);a.setPosition?a.setPosition(d,e):d.set("position",{x:e.x-e.width/2,y:e.y-e.height/2})},importLink:function(a,b,c){var d=this.getCell(b.name),e=c.edge(b),f=e.points||[];if((a.setVertices||a.setLinkVertices)&&(joint.util.isFunction(a.setVertices)?a.setVertices(d,f):d.set("vertices",f.slice(1,f.length-1))),a.setLabels&&"x"in e&&"y"in e){var h={x:e.x,y:e.y};if(joint.util.isFunction(a.setLabels))a.setLabels(d,h,f);else{var i=g.Polyline(f),j=i.closestPointLength(h),k=i.pointAtLength(j),l=j/i.length();d.label(0,{position:{distance:l,offset:g.Point(h).difference(k).toJSON()}})}}},layout:function(a,b){var c;c=a instanceof joint.dia.Graph?a:(new joint.dia.Graph).resetCells(a,{dry:!0}),a=null,b=joint.util.defaults(b||{},{resizeClusters:!0,clusterPadding:10,exportElement:this.exportElement,exportLink:this.exportLink});var d=c.toGraphLib({directed:!0,multigraph:!0,compound:!0,setNodeLabel:b.exportElement,setEdgeLabel:b.exportLink,setEdgeName:function(a){return a.id}}),e={},f=b.marginX||0,h=b.marginY||0;if(b.rankDir&&(e.rankdir=b.rankDir),b.align&&(e.align=b.align),b.nodeSep&&(e.nodesep=b.nodeSep),b.edgeSep&&(e.edgesep=b.edgeSep),b.rankSep&&(e.ranksep=b.rankSep),b.ranker&&(e.ranker=b.ranker),f&&(e.marginx=f),h&&(e.marginy=h),d.setGraph(e),dagre.layout(d,{debugTiming:!!b.debugTiming}),c.startBatch("layout"),c.fromGraphLib(d,{importNode:this.importElement.bind(c,b),importEdge:this.importLink.bind(c,b)}),b.resizeClusters){var i=d.nodes().filter(function(a){return d.children(a).length>0}).map(c.getCell.bind(c)).sort(function(a,b){return b.getAncestors().length-a.getAncestors().length});joint.util.invoke(i,"fitEmbeds",{padding:b.clusterPadding})}c.stopBatch("layout");var j=d.graph();return g.Rect(f,h,Math.abs(j.width-2*f),Math.abs(j.height-2*h))},fromGraphLib:function(a,b){b=b||{};var c=b.importNode||joint.util.noop,d=b.importEdge||joint.util.noop,e=this instanceof joint.dia.Graph?this:new joint.dia.Graph;return a.nodes().forEach(function(d){c.call(e,d,a,e,b)}),a.edges().forEach(function(c){d.call(e,c,a,e,b)}),e},toGraphLib:function(a,b){b=b||{};for(var c=joint.util.pick(b,"directed","compound","multigraph"),d=new graphlib.Graph(c),e=b.setNodeLabel||joint.util.noop,f=b.setEdgeLabel||joint.util.noop,g=b.setEdgeName||joint.util.noop,h=a.get("cells"),i=0,j=h.length;i0}).map(c.getCell.bind(c)).sort(function(a,b){return b.getAncestors().length-a.getAncestors().length});joint.util.invoke(i,"fitEmbeds",{padding:b.clusterPadding})}c.stopBatch("layout");var j=d.graph();return g.Rect(f,h,Math.abs(j.width-2*f),Math.abs(j.height-2*h))},fromGraphLib:function(a,b){b=b||{};var c=b.importNode||joint.util.noop,d=b.importEdge||joint.util.noop,e=this instanceof joint.dia.Graph?this:new joint.dia.Graph;return a.nodes().forEach(function(d){c.call(e,d,a,e,b)}),a.edges().forEach(function(c){d.call(e,c,a,e,b)}),e},toGraphLib:function(a,b){b=b||{};for(var c=joint.util.pick(b,"directed","compound","multigraph"),d=new graphlib.Graph(c),e=b.setNodeLabel||joint.util.noop,f=b.setEdgeLabel||joint.util.noop,g=b.setEdgeName||joint.util.noop,h=a.get("cells"),i=0,j=h.length;i. - // 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 }]; - }; + return [firstControlPoints, secondControlPoints]; }, // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @deprecated // @param rhs Right hand side vector. // @return Solution vector. getFirstControlPoints: function(rhs) { + console.warn('deprecated'); + var n = rhs.length; // `x` is a solution vector. var x = []; @@ -527,870 +527,981 @@ var g = (function() { 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; }, + // Divide a Bezier curve into two at point defined by value 't' <0,1>. + // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 + // @deprecated + // @param control points (start, control start, control end, end) + // @return a function that accepts t and returns 2 curves. + getCurveDivider: function(p0, p1, p2, p3) { + + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + + return function divideCurve(t) { + + var divided = curve.divide(t); + + return [{ + p0: divided[0].start, + p1: divided[0].controlPoint1, + p2: divided[0].controlPoint2, + p3: divided[0].end + }, { + p0: divided[1].start, + p1: divided[1].controlPoint1, + p2: divided[1].controlPoint2, + p3: divided[1].end + }]; + }; + }, + // 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. + // @deprecated // @param control points (start, control start, control end, end) - // @return a function accepts a point and returns t. + // @return a function that 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; - }; - } + console.warn('deprecated'); + + var curve = new Curve(p0, p1, p2, p3); + 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); + + return curve.closestPointT(p); }; } }; - var Ellipse = g.Ellipse = function(c, a, b) { + var Curve = g.Curve = function(p1, p2, p3, p4) { - if (!(this instanceof Ellipse)) { - return new Ellipse(c, a, b); + if (!(this instanceof Curve)) { + return new Curve(p1, p2, p3, p4); } - if (c instanceof Ellipse) { - return new Ellipse(Point(c), c.a, c.b); + if (p1 instanceof Curve) { + return new Curve(p1.start, p1.controlPoint1, p1.controlPoint2, p1.end); } - c = Point(c); - this.x = c.x; - this.y = c.y; - this.a = a; - this.b = b; + this.start = new Point(p1); + this.controlPoint1 = new Point(p2); + this.controlPoint2 = new Point(p3); + this.end = new Point(p4); }; - g.Ellipse.fromRect = function(rect) { + // Curve passing 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} curves. + Curve.throughPoints = (function() { - rect = Rect(rect); - return Ellipse(rect.center(), rect.width / 2, rect.height / 2); - }; + // 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. + function getFirstControlPoints(rhs) { - g.Ellipse.prototype = { + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; - bbox: function() { + x[0] = rhs[0] / b; - return Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.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; + } - clone: function() { + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } - return Ellipse(this); - }, + return x; + } - /** - * @param {g.Point} point - * @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside - */ - normalizedDistance: function(point) { + // 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. + function getCurveControlPoints(knots) { - var x0 = point.x; - var y0 = point.y; - var a = this.a; - var b = this.b; - var x = this.x; - var y = this.y; + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; - return ((x0 - x) * (x0 - x)) / (a * a ) + ((y0 - y) * (y0 - y)) / (b * b); - }, + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = new Point( + (2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3 + ); - // 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; - } + // P2 = 2P1 – P0 + secondControlPoints[0] = new Point( + 2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y + ); - if (dy === undefined) { - dy = dx; + return [firstControlPoints, secondControlPoints]; } - this.a += 2 * dx; - this.b += 2 * dy; - - return this; - }, + // 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; + } - /** - * @param {g.Point} p - * @returns {boolean} - */ - containsPoint: function(p) { + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - return this.normalizedDistance(p) <= 1; - }, + // Get first control points X-values. + var x = getFirstControlPoints(rhs); - /** - * @returns {g.Point} - */ - center: function() { + // Set right hand side Y values. + for (i = 1; i < n - 1; ++i) { + rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; + } - return Point(this.x, this.y); - }, + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - /** 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) { + // Get first control points Y-values. + var y = getFirstControlPoints(rhs); - 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; + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(new Point(x[i], y[i])); - var q1 = x0 > center.x + a / 2; - var q3 = x0 < center.x - a / 2; + // Second control point. + if (i < n - 1) { + secondControlPoints.push(new Point( + 2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1] + )); - 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; + } else { + secondControlPoints.push(new Point( + (knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2 + )); + } } - return g.point(x, y).theta(p); + return [firstControlPoints, secondControlPoints]; + } - }, + return function(points) { - equals: function(ellipse) { + if (!points || (Array.isArray(points) && points.length < 2)) { + throw new Error('At least 2 points are required'); + } - return !!ellipse && - ellipse.x === this.x && - ellipse.y === this.y && - ellipse.a === this.a && - ellipse.b === this.b; - }, + var controlPoints = getCurveControlPoints(points); - // 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) { + var curves = []; + var n = controlPoints[0].length; + for (var i = 0; i < n; i++) { - 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 controlPoint1 = new Point(controlPoints[0][i].x, controlPoints[0][i].y); + var controlPoint2 = new Point(controlPoints[1][i].x, controlPoints[1][i].y); + + curves.push(new Curve(points[i], controlPoint1, controlPoint2, points[i + 1])); } - 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; - }, + return curves; + }; + })(); - toString: function() { + Curve.prototype = { - return Point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; - } - }; + // Returns a bbox that tightly envelops the curve. + bbox: function() { - var Line = g.Line = function(p1, p2) { + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; - if (!(this instanceof Line)) { - return new Line(p1, p2); - } + var x0 = start.x; + var y0 = start.y; + var x1 = controlPoint1.x; + var y1 = controlPoint1.y; + var x2 = controlPoint2.x; + var y2 = controlPoint2.y; + var x3 = end.x; + var y3 = end.y; - if (p1 instanceof Line) { - return Line(p1.start, p1.end); - } + var points = new Array(); // local extremes + var tvalues = new Array(); // t values of local extremes + var bounds = [new Array(), new Array()]; - this.start = Point(p1); - this.end = Point(p2); - }; + var a, b, c, t; + var t1, t2; + var b2ac, sqrtb2ac; - g.Line.prototype = { + for (var i = 0; i < 2; ++i) { - // @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() { + if (i === 0) { + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; - 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)); + } else { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } - var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N']; + if (abs(a) < 1e-12) { // Numerical robustness + if (abs(b) < 1e-12) { // Numerical robustness + continue; + } - var index = brng - 22.5; - if (index < 0) - index += 360; - index = parseInt(index / 45); + t = -c / b; + if ((0 < t) && (t < 1)) tvalues.push(t); - return bearings[index]; - }, + continue; + } - clone: function() { + b2ac = b * b - 4 * c * a; + sqrtb2ac = sqrt(b2ac); - return Line(this.start, this.end); - }, + if (b2ac < 0) continue; - equals: function(l) { + t1 = (-b + sqrtb2ac) / (2 * a); + if ((0 < t1) && (t1 < 1)) tvalues.push(t1); - return !!l && - this.start.x === l.start.x && - this.start.y === l.start.y && - this.end.x === l.end.x && - this.end.y === l.end.y; - }, + t2 = (-b - sqrtb2ac) / (2 * a); + if ((0 < t2) && (t2 < 1)) tvalues.push(t2); + } - // @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) - ); + var j = tvalues.length; + var jlen = j; + var mt; + var x, y; - } else if (l instanceof Rect) { - // Passed in parameter is a rectangle. + while (j--) { + t = tvalues[j]; + mt = 1 - t; - var r = l; - var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; - var points = []; - var dedupeArr = []; - var pt, i; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; - 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()); - } - } + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; - return points.length > 0 ? points : null; + points[j] = { X: x, Y: y }; } - // Passed in parameter is neither a Line nor a Rectangle. - return null; - }, + tvalues[jlen] = 0; + tvalues[jlen + 1] = 1; - // @return {double} length of the line - length: function() { - return sqrt(this.squaredLength()); - }, + points[jlen] = { X: x0, Y: y0 }; + points[jlen + 1] = { X: x3, Y: y3 }; - // @return {point} my midpoint - midpoint: function() { - return Point( - (this.start.x + this.end.x) / 2, - (this.start.y + this.end.y) / 2 - ); - }, + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; - // @return {point} my point at 't' <0,1> - pointAt: function(t) { + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; - 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); - }, + tvalues.length = jlen + 2; + bounds[0].length = jlen + 2; + bounds[1].length = jlen + 2; + points.length = jlen + 2; - // @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) { + var left = min.apply(null, bounds[0]); + var top = min.apply(null, bounds[1]); + var right = max.apply(null, bounds[0]); + var bottom = max.apply(null, bounds[1]); - // 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 new Rect(left, top, (right - left), (bottom - top)); }, - // @return vector {point} of the line - vector: function() { + clone: function() { - return Point(this.end.x - this.start.x, this.end.y - this.start.y); + return new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end); }, - // @return {point} the closest point on the line to point `p` - closestPoint: function(p) { + // Returns the point on the curve closest to point `p` + closestPoint: function(p, opt) { - return this.pointAt(this.closestPointNormalizedLength(p)); + return this.pointAtT(this.closestPointT(p, opt)); }, - // @return {number} the normalized length of the closest point on the line to point `p` - closestPointNormalizedLength: function(p) { + closestPointLength: function(p, opt) { - var product = this.vector().dot(Line(this.start, p).vector()); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - return Math.min(1, Math.max(0, product / this.squaredLength())); + return this.lengthAtT(this.closestPointT(p, localOpt), localOpt); }, - // @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; + closestPointNormalizedLength: function(p, opt) { + + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + var cpLength = this.closestPointLength(p, localOpt); + if (!cpLength) return 0; + + var length = this.length(localOpt); + if (length === 0) return 0; + + return cpLength / length; }, - toString: function() { - return this.start.toString() + ' ' + this.end.toString(); - } - }; + // Returns `t` of the point on the curve closest to point `p` + closestPointT: function(p, opt) { - // For backwards compatibility: - g.Line.prototype.intersection = g.Line.prototype.intersect; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // does not use localOpt + + // identify the subdivision that contains the point: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + var distFromStart; // distance of point from start of baseline + var distFromEnd; // distance of point from end of baseline + var minSumDist; // lowest observed sum of the two distances + var n = subdivisions.length; + var subdivisionSize = (n ? (1 / n) : 0); + for (var i = 0; i < n; i++) { - /* - Point is the most basic object consisting of x/y coordinate. + var currentSubdivision = subdivisions[i]; - Possible instantiations are: - * `Point(10, 20)` - * `new Point(10, 20)` - * `Point('10 20')` - * `Point(Point(10, 20))` - */ - var Point = g.Point = function(x, y) { + var startDist = currentSubdivision.start.distance(p); + var endDist = currentSubdivision.end.distance(p); + var sumDist = startDist + endDist; - if (!(this instanceof Point)) { - return new Point(x, y); - } + // check that the point is closest to current subdivision and not any other + if (!minSumDist || (sumDist < minSumDist)) { + investigatedSubdivision = currentSubdivision; - 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; - } + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - }; + distFromStart = startDist; + distFromEnd = endDist; - // Alternative constructor, from polar coordinates. - // @param {number} Distance. - // @param {number} Angle in radians. - // @param {point} [optional] Origin. - g.Point.fromPolar = function(distance, angle, origin) { + minSumDist = sumDist; + } + } - origin = (origin && Point(origin)) || Point(0, 0); - var x = abs(distance * cos(angle)); - var y = abs(distance * sin(angle)); - var deg = normalizeAngle(toDeg(angle)); + var precisionRatio = pow(10, -precision); - if (deg < 90) { - y = -y; - } else if (deg < 180) { - x = -x; - y = -y; - } else if (deg < 270) { - x = -x; - } + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(origin.x + x, origin.y + y); - }; + // check if we have reached required observed precision + var startPrecisionRatio; + var endPrecisionRatio; - // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. - g.Point.random = function(x1, x2, y1, y2) { + startPrecisionRatio = (distFromStart ? (abs(distFromStart - distFromEnd) / distFromStart) : 0); + endPrecisionRatio = (distFromEnd ? (abs(distFromStart - distFromEnd) / distFromEnd) : 0); + if ((startPrecisionRatio < precisionRatio) || (endPrecisionRatio) < precisionRatio) { + return ((distFromStart <= distFromEnd) ? investigatedSubdivisionStartT : investigatedSubdivisionEndT); + } - return Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); - }; + // otherwise, set up for next iteration + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - g.Point.prototype = { + var startDist1 = divided[0].start.distance(p); + var endDist1 = divided[0].end.distance(p); + var sumDist1 = startDist1 + endDist1; - // 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) { + var startDist2 = divided[1].start.distance(p); + var endDist2 = divided[1].end.distance(p); + var sumDist2 = startDist2 + endDist2; - if (r.containsPoint(this)) { - return this; - } + if (sumDist1 <= sumDist2) { + investigatedSubdivision = divided[0]; - 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; - }, + investigatedSubdivisionEndT -= subdivisionSize; // subdivisionSize was already halved - // Return the bearing between me and the given point. - bearing: function(point) { + distFromStart = startDist1; + distFromEnd = endDist1; - return Line(this, point).bearing(); - }, + } else { + investigatedSubdivision = divided[1]; - // Returns change in angle from my previous position (-dx, -dy) to my new position - // relative to ref point. - changeInAngle: function(dx, dy, ref) { + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved - // Revert the translation and measure the change in angle around x-axis. - return Point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); + distFromStart = startDist2; + distFromEnd = endDist2; + } + } }, - clone: function() { + closestPointTangent: function(p, opt) { - return Point(this); + return this.tangentAtT(this.closestPointT(p, opt)); }, - difference: function(dx, dy) { + // Divides the curve into two at point defined by `t` between 0 and 1. + // Using de Casteljau's algorithm (http://math.stackexchange.com/a/317867). + // Additional resource: https://pomax.github.io/bezierinfo/#decasteljau + divide: function(t) { - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; + var start = this.start; + var controlPoint1 = this.controlPoint1; + var controlPoint2 = this.controlPoint2; + var end = this.end; + + // shortcuts for `t` values that are out of range + if (t <= 0) { + return [ + new Curve(start, start, start, start), + new Curve(start, controlPoint1, controlPoint2, end) + ]; } - return Point(this.x - (dx || 0), this.y - (dy || 0)); - }, + if (t >= 1) { + return [ + new Curve(start, controlPoint1, controlPoint2, end), + new Curve(end, end, end, end) + ]; + } - // Returns distance between me and point `p`. - distance: function(p) { + var dividerPoints = this.getSkeletonPoints(t); + + var startControl1 = dividerPoints.startControlPoint1; + var startControl2 = dividerPoints.startControlPoint2; + var divider = dividerPoints.divider; + var dividerControl1 = dividerPoints.dividerControlPoint1; + var dividerControl2 = dividerPoints.dividerControlPoint2; - return Line(this, p).length(); + // return array with two new curves + return [ + new Curve(start, startControl1, startControl2, divider), + new Curve(divider, dividerControl1, dividerControl2, end) + ]; }, - squaredDistance: function(p) { + // Returns the distance between the curve's start and end points. + endpointDistance: function() { - return Line(this, p).squaredLength(); + return this.start.distance(this.end); }, - equals: function(p) { + // Checks whether two curves are exactly the same. + equals: function(c) { - return !!p && this.x === p.x && this.y === p.y; + return !!c && + this.start.x === c.start.x && + this.start.y === c.start.y && + this.controlPoint1.x === c.controlPoint1.x && + this.controlPoint1.y === c.controlPoint1.y && + this.controlPoint2.x === c.controlPoint2.x && + this.controlPoint2.y === c.controlPoint2.y && + this.end.x === c.end.x && + this.end.y === c.end.y; }, - magnitude: function() { + // Returns five helper points necessary for curve division. + getSkeletonPoints: function(t) { - return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; - }, + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; - // Returns a manhattan (taxi-cab) distance between me and point `p`. - manhattanDistance: function(p) { + // shortcuts for `t` values that are out of range + if (t <= 0) { + return { + startControlPoint1: start.clone(), + startControlPoint2: start.clone(), + divider: start.clone(), + dividerControlPoint1: control1.clone(), + dividerControlPoint2: control2.clone() + }; + } - return abs(p.x - this.x) + abs(p.y - this.y); - }, + if (t >= 1) { + return { + startControlPoint1: control1.clone(), + startControlPoint2: control2.clone(), + divider: end.clone(), + dividerControlPoint1: end.clone(), + dividerControlPoint2: end.clone() + }; + } - // Move point on line starting from ref ending at me by - // distance distance. - move: function(ref, distance) { + var midpoint1 = (new Line(start, control1)).pointAt(t); + var midpoint2 = (new Line(control1, control2)).pointAt(t); + var midpoint3 = (new Line(control2, end)).pointAt(t); - var theta = toRad(Point(ref).theta(this)); - return this.offset(cos(theta) * distance, -sin(theta) * distance); - }, + var subControl1 = (new Line(midpoint1, midpoint2)).pointAt(t); + var subControl2 = (new Line(midpoint2, midpoint3)).pointAt(t); - // 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 divider = (new Line(subControl1, subControl2)).pointAt(t); - var scale = (length || 1) / this.magnitude(); - return this.scale(scale, scale); + var output = { + startControlPoint1: midpoint1, + startControlPoint2: subControl1, + divider: divider, + dividerControlPoint1: subControl2, + dividerControlPoint2: midpoint3 + }; + + return output; }, - // Offset me by the specified amount. - offset: function(dx, dy) { + // Returns a list of curves whose flattened length is better than `opt.precision`. + // That is, observed difference in length between recursions is less than 10^(-3) = 0.001 = 0.1% + // (Observed difference is not real precision, but close enough as long as special cases are covered) + // (That is why skipping iteration 1 is important) + // As a rule of thumb, increasing `precision` by 1 requires two more division operations + // - Precision 0 (endpointDistance) - total of 2^0 - 1 = 0 operations (1 subdivision) + // - Precision 1 (<10% error) - total of 2^2 - 1 = 3 operations (4 subdivisions) + // - Precision 2 (<1% error) - total of 2^4 - 1 = 15 operations requires 4 division operations on all elements (15 operations total) (16 subdivisions) + // - Precision 3 (<0.1% error) - total of 2^6 - 1 = 63 operations - acceptable when drawing (64 subdivisions) + // - Precision 4 (<0.01% error) - total of 2^8 - 1 = 255 operations - high resolution, can be used to interpolate `t` (256 subdivisions) + // (Variation of 1 recursion worse or better is possible depending on the curve, doubling/halving the number of operations accordingly) + getSubdivisions: function(opt) { - if ((Object(dx) === dx)) { - dy = dx.y; - dx = dx.x; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt - this.x += dx || 0; - this.y += dy || 0; - return this; - }, + var subdivisions = [new Curve(this.start, this.controlPoint1, this.controlPoint2, this.end)]; + if (precision === 0) return subdivisions; - // Returns a point that is the reflection of me with - // the center of inversion in ref point. - reflection: function(ref) { + var previousLength = this.endpointDistance(); - return Point(ref).move(this, this.distance(ref)); - }, + var precisionRatio = pow(10, -precision); - // Rotate point by angle around origin. - rotate: function(origin, angle) { + // recursively divide curve at `t = 0.5` + // until the difference between observed length at subsequent iterations is lower than precision + var iteration = 0; + while (true) { + iteration += 1; - 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; - }, + // divide all subdivisions + var newSubdivisions = []; + var numSubdivisions = subdivisions.length; + for (var i = 0; i < numSubdivisions; i++) { - round: function(precision) { + var currentSubdivision = subdivisions[i]; + var divided = currentSubdivision.divide(0.5); // dividing at t = 0.5 (not at middle length!) + newSubdivisions.push(divided[0], divided[1]); + } - var f = pow(10, precision || 0); - this.x = round(this.x * f) / f; - this.y = round(this.y * f) / f; - return this; - }, + // measure new length + var length = 0; + var numNewSubdivisions = newSubdivisions.length; + for (var j = 0; j < numNewSubdivisions; j++) { - // Scale point with origin. - scale: function(sx, sy, origin) { + var currentNewSubdivision = newSubdivisions[j]; + length += currentNewSubdivision.endpointDistance(); + } - 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; + // check if we have reached required observed precision + // sine-like curves may have the same observed length in iteration 0 and 1 - skip iteration 1 + // not a problem for further iterations because cubic curves cannot have more than two local extrema + // (i.e. cubic curves cannot intersect the baseline more than once) + // therefore two subsequent iterations cannot produce sampling with equal length + var observedPrecisionRatio = ((length !== 0) ? ((length - previousLength) / length) : 0); + if (iteration > 1 && observedPrecisionRatio < precisionRatio) { + return newSubdivisions; + } + + // otherwise, set up for next iteration + subdivisions = newSubdivisions; + previousLength = length; + } }, - snapToGrid: function(gx, gy) { + isDifferentiable: function() { - this.x = snapToGrid(this.x, gx); - this.y = snapToGrid(this.y, gy || gx); - return this; + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; + + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); }, - // Compute the angle between me and `p` and the x axis. - // (cartesian-to-polar coordinates conversion) - // Return theta angle in degrees. - theta: function(p) { + // Returns flattened length of the curve with precision better than `opt.precision`; or using `opt.subdivisions` provided. + length: function(opt) { - p = Point(p); - // Invert the y-axis. - var y = -(p.y - this.y); - var x = p.x - this.x; - var rad = atan2(y, x); // defined for all 0 corner cases - - // Correction for III. and IV. quadrant. - if (rad < 0) { - rad = 2 * PI + rad; - } - return 180 * rad / PI; - }, + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt - // Compute the angle between vector from me to p1 and the vector from me to p2. - // ordering of points p1 and p2 is important! - // theta function's angle convention: - // returns angles between 0 and 180 when the angle is counterclockwise - // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones - // returns NaN if any of the points p1, p2 is coincident with this point - angleBetween: function(p1, p2) { - - var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - if (angleBetween < 0) { - angleBetween += 360; // correction to keep angleBetween between 0 and 360 + var length = 0; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + length += currentSubdivision.endpointDistance(); } - return angleBetween; - }, - // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. - // Returns NaN if p is at 0,0. - vectorAngle: function(p) { - - var zero = Point(0,0); - return zero.angleBetween(this, p); + return length; }, - toJSON: function() { + // Returns distance along the curve up to `t` with precision better than requested `opt.precision`. (Not using `opt.subdivisions`.) + lengthAtT: function(t, opt) { - return { x: this.x, y: this.y }; - }, + if (t <= 0) return 0; - // Converts rectangular to polar coordinates. - // An origin can be specified, otherwise it's 0@0. - toPolar: function(o) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.subdivisions + // not using localOpt - 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; + var subCurve = this.divide(t)[0]; + var subCurveLength = subCurve.length({ precision: precision }); + + return subCurveLength; }, - toString: function() { + // Returns point at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. + // Mirrors Line.pointAt() function. + // For a function that tracks `t`, use Curve.pointAtT(). + pointAt: function(ratio, opt) { - return this.x + '@' + this.y; + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); + + var t = this.tAt(ratio, opt); + + return this.pointAtT(t); }, - update: function(x, y) { + // Returns point at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + pointAtLength: function(length, opt) { - this.x = x || 0; - this.y = y || 0; - return this; + var t = this.tAtLength(length, opt); + + return this.pointAtT(t); }, - // Returns the dot product of this point with given other point - dot: function(p) { + // Returns the point at provided `t` between 0 and 1. + // `t` does not track distance along curve as it does in Line objects. + // Non-linear relationship, speeds up and slows down as curve warps! + // For linear length-based solution, use Curve.pointAt(). + pointAtT: function(t) { - return p ? (this.x * p.x + this.y * p.y) : NaN; + if (t <= 0) return this.start.clone(); + if (t >= 1) return this.end.clone(); + + return this.getSkeletonPoints(t).divider; }, - // Returns the cross product of this point relative to two other points - // this point is the common point - // point p1 lies on the first vector, point p2 lies on the second vector - // watch out for the ordering of points p1 and p2! - // positive result indicates a clockwise ("right") turn from first to second vector - // negative result indicates a counterclockwise ("left") turn from first to second vector - // note that the above directions are reversed from the usual answer on the Internet - // that is because we are in a left-handed coord system (because the y-axis points downward) - cross: function(p1, p2) { + // Default precision + PRECISION: 3, - return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; - } - }; + scale: function(sx, sy, origin) { - var Rect = g.Rect = function(x, y, w, h) { + this.start.scale(sx, sy, origin); + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - if (!(this instanceof Rect)) { - return new Rect(x, y, w, h); - } + // Returns a tangent line at requested `ratio` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAt: function(ratio, opt) { - if ((Object(x) === x)) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } + if (!this.isDifferentiable()) return null; - this.x = x === undefined ? 0 : x; - this.y = y === undefined ? 0 : y; - this.width = w === undefined ? 0 : w; - this.height = h === undefined ? 0 : h; - }; + if (ratio < 0) ratio = 0; + else if (ratio > 1) ratio = 1; - g.Rect.fromEllipse = function(e) { + var t = this.tAt(ratio, opt); - e = Ellipse(e); - return Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); - }; + return this.tangentAtT(t); + }, - g.Rect.prototype = { + // Returns a tangent line at requested `length` with precision better than requested `opt.precision`; or using `opt.subdivisions` provided. + tangentAtLength: function(length, opt) { - // 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) { + if (!this.isDifferentiable()) return null; - 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); + var t = this.tAtLength(length, opt); + + return this.tangentAtT(t); }, - bottomLeft: function() { + // Returns a tangent line at requested `t`. + tangentAtT: function(t) { - return Point(this.x, this.y + this.height); - }, + if (!this.isDifferentiable()) return null; - bottomLine: function() { + if (t < 0) t = 0; + else if (t > 1) t = 1; - return Line(this.bottomLeft(), this.corner()); - }, + var skeletonPoints = this.getSkeletonPoints(t); - bottomMiddle: function() { + var p1 = skeletonPoints.startControlPoint2; + var p2 = skeletonPoints.dividerControlPoint1; - return Point(this.x + this.width / 2, this.y + this.height); - }, + var tangentStart = skeletonPoints.divider; - center: function() { + var tangentLine = new Line(p1, p2); + tangentLine.translate(tangentStart.x - p1.x, tangentStart.y - p1.y); // move so that tangent line starts at the point requested - return Point(this.x + this.width / 2, this.y + this.height / 2); + return tangentLine; }, - clone: function() { + // Returns `t` at requested `ratio` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + tAt: function(ratio, opt) { - return Rect(this); - }, + if (ratio <= 0) return 0; + if (ratio >= 1) return 1; - // @return {bool} true if point p is insight me - containsPoint: function(p) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; - p = Point(p); - return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + var curveLength = this.length(localOpt); + var length = curveLength * ratio; + + return this.tAtLength(length, localOpt); }, - // @return {bool} true if rectangle `r` is inside me. - containsRect: function(r) { + // Returns `t` at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. + // Uses `precision` to approximate length within `precision` (always underestimates) + // Then uses a binary search to find the `t` of a subdivision endpoint that is close (within `precision`) to the `length`, if the curve was as long as approximated + // As a rule of thumb, increasing `precision` by 1 causes the algorithm to go 2^(precision - 1) deeper + // - Precision 0 (chooses one of the two endpoints) - 0 levels + // - Precision 1 (chooses one of 5 points, <10% error) - 1 level + // - Precision 2 (<1% error) - 3 levels + // - Precision 3 (<0.1% error) - 7 levels + // - Precision 4 (<0.01% error) - 15 levels + tAtLength: function(length, opt) { - 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; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - if (!w0 || !h0 || !w1 || !h1) { - // At least one of the dimensions is 0 - return false; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + var localOpt = { precision: precision, subdivisions: subdivisions }; + + // identify the subdivision that contains the point at requested `length`: + var investigatedSubdivision; + var investigatedSubdivisionStartT; // assume that subdivisions are evenly spaced + var investigatedSubdivisionEndT; + //var baseline; // straightened version of subdivision to investigate + //var baselinePoint; // point on the baseline that is the requested distance away from start + var baselinePointDistFromStart; // distance of baselinePoint from start of baseline + var baselinePointDistFromEnd; // distance of baselinePoint from end of baseline + var l = 0; // length so far + var n = subdivisions.length; + var subdivisionSize = 1 / n; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { + + var currentSubdivision = subdivisions[i]; + var d = currentSubdivision.endpointDistance(); // length of current subdivision + + if (length <= (l + d)) { + investigatedSubdivision = currentSubdivision; + + investigatedSubdivisionStartT = i * subdivisionSize; + investigatedSubdivisionEndT = (i + 1) * subdivisionSize; + + baselinePointDistFromStart = (fromStart ? (length - l) : ((d + l) - length)); + baselinePointDistFromEnd = (fromStart ? ((d + l) - length) : (length - l)); + + break; + } + + l += d; } - var x0 = r0.x; - var y0 = r0.y; - var x1 = r1.x; - var y1 = r1.y; + if (!investigatedSubdivision) return (fromStart ? 1 : 0); // length requested is out of range - return maximum t + // note that precision affects what length is recorded + // (imprecise measurements underestimate length by up to 10^(-precision) of the precise length) + // e.g. at precision 1, the length may be underestimated by up to 10% and cause this function to return 1 - w1 += x1; - w0 += x0; - h1 += y1; - h0 += y0; + var curveLength = this.length(localOpt); - return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; - }, + var precisionRatio = pow(10, -precision); - corner: function() { + // recursively divide investigated subdivision: + // until distance between baselinePoint and closest path endpoint is within 10^(-precision) + // then return the closest endpoint of that final subdivision + while (true) { - return Point(this.x + this.width, this.y + this.height); - }, + // check if we have reached required observed precision + var observedPrecisionRatio; - // @return {boolean} true if rectangles are equal. - equals: function(r) { + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromStart / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionStartT; + observedPrecisionRatio = ((curveLength !== 0) ? (baselinePointDistFromEnd / curveLength) : 0); + if (observedPrecisionRatio < precisionRatio) return investigatedSubdivisionEndT; - 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; - }, + // otherwise, set up for next iteration + var newBaselinePointDistFromStart; + var newBaselinePointDistFromEnd; - // @return {rect} if rectangles intersect, {null} if not. - intersect: function(r) { + var divided = investigatedSubdivision.divide(0.5); + subdivisionSize /= 2; - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = r.origin(); - var rCorner = r.corner(); + var baseline1Length = divided[0].endpointDistance(); + var baseline2Length = divided[1].endpointDistance(); - // No intersection found - if (rCorner.x <= myOrigin.x || - rCorner.y <= myOrigin.y || - rOrigin.x >= myCorner.x || - rOrigin.y >= myCorner.y) return null; + if (baselinePointDistFromStart <= baseline1Length) { // point at requested length is inside divided[0] + investigatedSubdivision = divided[0]; + + investigatedSubdivisionEndT -= subdivisionSize; // sudivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart; + newBaselinePointDistFromEnd = baseline1Length - newBaselinePointDistFromStart; - var x = Math.max(myOrigin.x, rOrigin.x); - var y = Math.max(myOrigin.y, rOrigin.y); + } else { // point at requested length is inside divided[1] + investigatedSubdivision = divided[1]; - return Rect(x, y, Math.min(myCorner.x, rCorner.x) - x, Math.min(myCorner.y, rCorner.y) - y); + investigatedSubdivisionStartT += subdivisionSize; // subdivisionSize was already halved + + newBaselinePointDistFromStart = baselinePointDistFromStart - baseline1Length; + newBaselinePointDistFromEnd = baseline2Length - newBaselinePointDistFromStart; + } + + baselinePointDistFromStart = newBaselinePointDistFromStart; + baselinePointDistFromEnd = newBaselinePointDistFromEnd; + } }, - // 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) { + translate: function(tx, ty) { - p = Point(p); - var center = Point(this.x + this.width / 2, this.y + this.height / 2); - var result; - if (angle) p.rotate(center, angle); + this.start.translate(tx, ty); + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, - // (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); + // Returns an array of points that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPoints: function(opt) { - for (var i = sides.length - 1; i >= 0; --i) { - var intersection = sides[i].intersection(connector); - if (intersection !== null) { - result = intersection; - break; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSubdivisions() call + var subdivisions = (opt.subdivisions === undefined) ? this.getSubdivisions({ precision: precision }) : opt.subdivisions; + // not using localOpt + + var points = [subdivisions[0].start.clone()]; + var n = subdivisions.length; + for (var i = 0; i < n; i++) { + + var currentSubdivision = subdivisions[i]; + points.push(currentSubdivision.end.clone()); } - if (result && angle) result.rotate(center, -angle); - return result; + + return points; }, - leftLine: function() { + // Returns a polyline that represents the curve when flattened, up to `opt.precision`; or using `opt.subdivisions` provided. + // Flattened length is no more than 10^(-precision) away from real curve length. + toPolyline: function(opt) { - return Line(this.origin(), this.bottomLeft()); + return new Polyline(this.toPoints(opt)); }, - leftMiddle: function() { + toString: function() { + + return this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; + + 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(new Point(c.x, c.y), c.a, c.b); + } + + c = new Point(c); + this.x = c.x; + this.y = c.y; + this.a = a; + this.b = b; + }; + + Ellipse.fromRect = function(rect) { + + rect = new Rect(rect); + return new Ellipse(rect.center(), rect.width / 2, rect.height / 2); + }; - return Point(this.x , this.y + this.height / 2); + Ellipse.prototype = { + + bbox: function() { + + return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); }, - // Move and expand me. - // @param r {rectangle} representing deltas - moveAndExpand: function(r) { + clone: function() { - this.x += r.x || 0; - this.y += r.y || 0; - this.width += r.width || 0; - this.height += r.height || 0; - return this; + return new Ellipse(this); }, - // Offset me by the specified amount. - offset: function(dx, dy) { - return Point.prototype.offset.call(this, dx, dy); + /** + * @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, recompute origin [x, y] + // 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 @@ -1403,9043 +1514,15234 @@ var g = (function() { dy = dx; } - this.x -= dx; - this.y -= dy; - this.width += 2 * dx; - this.height += 2 * dy; + this.a += 2 * dx; + this.b += 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() { + /** + * @param {g.Point} p + * @returns {boolean} + */ + containsPoint: function(p) { - return Point(this.x, this.y); + return this.normalizedDistance(p) <= 1; }, - // @return {point} a point on my boundary nearest to the given point. - // @see Squeak Smalltalk, Rectangle>>pointNearestTo: - pointNearestToPoint: function(point) { + /** + * @returns {g.Point} + */ + center: function() { - 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); + return new Point(this.x, this.y); }, - rightLine: function() { + /** 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) { - return Line(this.topRight(), this.corner()); - }, + 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; - rightMiddle: function() { + var q1 = x0 > center.x + a / 2; + var q3 = x0 < center.x - a / 2; - return Point(this.x + this.width, this.y + this.height / 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; - round: function(precision) { + } 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 (new Point(x, y)).theta(p); - 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) { + equals: function(ellipse) { - origin = this.origin().scale(sx, sy, origin); - this.x = origin.x; - this.y = origin.y; - this.width *= sx; - this.height *= sy; - return this; + return !!ellipse && + ellipse.x === this.x && + ellipse.y === this.y && + ellipse.a === this.a && + ellipse.b === this.b; }, - maxRectScaleToFit: function(rect, origin) { - - rect = g.Rect(rect); - origin || (origin = rect.center()); + intersectionWithLine: function(line) { - var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; - var ox = origin.x; - var oy = origin.y; + var intersections = []; + var a1 = line.start; + var a2 = line.end; + var rx = this.a; + var ry = this.b; + var dir = line.vector(); + var diff = a1.difference(new Point(this)); + var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)); + var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)); - // 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. + var a = dir.dot(mDir); + var b = dir.dot(mDiff); + var c = diff.dot(mDiff) - 1.0; + var d = b * b - a * c; - sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; + if (d < 0) { + return null; + } else if (d > 0) { + var root = sqrt(d); + var ta = (-b - root) / a; + var tb = (-b + root) / a; - // 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 ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) { + // if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside + return null; + } else { + if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta)); + if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb)); + } + } else { + var t = -b / a; + if (0 <= t && t <= 1) { + intersections.push(a1.lerp(a2, t)); + } else { + // outside + return null; + } } - if (p4.y > oy) { - sy4 = (this.y + this.height - oy) / (p4.y - oy); + + return intersections; + }, + + // 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 = new Point(p); + + if (angle) p.rotate(new 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(new Point(this.x, this.y), -angle); + return result; } - return { - sx: Math.min(sx1, sx2, sx3, sx4), - sy: Math.min(sy1, sy2, sy3, sy4) - }; + 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 = new Point(this.x + x, this.y + y); + + if (angle) return result.rotate(new Point(this.x, this.y), -angle); + return result; }, - maxRectUniformScaleToFit: function(rect, origin) { + toString: function() { - var scale = this.maxRectScaleToFit(rect, origin); - return Math.min(scale.sx, scale.sy); + return (new 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); + } + + if (p1 instanceof Line) { + return new Line(p1.start, p1.end); + } + + this.start = new Point(p1); + this.end = new Point(p2); + }; + + Line.prototype = { + + bbox: function() { + + var left = min(this.start.x, this.end.x); + var top = min(this.start.y, this.end.y); + var right = max(this.start.x, this.end.x); + var bottom = max(this.start.y, this.end.y); + + return new Rect(left, top, (right - left), (bottom - top)); }, - // @return {string} (left|right|top|bottom) side which is nearest to point - // @see Squeak Smalltalk, Rectangle>>sideNearestTo: - sideNearestToPoint: function(point) { + // @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() { - 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'; + 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)); - if (distToRight < closest) { - closest = distToRight; - side = 'right'; - } - if (distToTop < closest) { - closest = distToTop; - side = 'top'; - } - if (distToBottom < closest) { - closest = distToBottom; - side = 'bottom'; - } - return side; + 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]; }, - snapToGrid: function(gx, gy) { + clone: function() { - 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; + return new Line(this.start, this.end); }, - topLine: function() { + // @return {point} the closest point on the line to point `p` + closestPoint: function(p) { - return Line(this.origin(), this.topRight()); + return this.pointAt(this.closestPointNormalizedLength(p)); }, - topMiddle: function() { + closestPointLength: function(p) { - return Point(this.x + this.width / 2, this.y); + return this.closestPointNormalizedLength(p) * this.length(); }, - topRight: function() { + // @return {number} the normalized length of the closest point on the line to point `p` + closestPointNormalizedLength: function(p) { + + var product = this.vector().dot((new Line(this.start, p)).vector()); + var cpNormalizedLength = min(1, max(0, product / this.squaredLength())); - return Point(this.x + this.width, this.y); + // cpNormalizedLength returns `NaN` if this line has zero length + // we can work with that - if `NaN`, return 0 + if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN` + // (`NaN` is the only value that is not equal to itself) + + return cpNormalizedLength; }, - toJSON: function() { + closestPointTangent: function(p) { - return { x: this.x, y: this.y, width: this.width, height: this.height }; + return this.tangentAt(this.closestPointNormalizedLength(p)); }, - toString: function() { + equals: function(l) { - return this.origin().toString() + ' ' + this.corner().toString(); + return !!l && + 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 {rect} representing the union of both rectangles. - union: function(rect) { + intersectionWithLine: function(line) { - rect = Rect(rect); - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = rect.origin(); - var rCorner = rect.corner(); + var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y); + var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y); + var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); + var deltaPt = new Point(line.start.x - this.start.x, line.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); - 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); + if (det === 0 || alpha * det < 0 || beta * det < 0) { + // No intersection found. + return null; + } - return Rect(originX, originY, cornerX - originX, cornerY - originY); - } - }; + if (det > 0) { + if (alpha > det || beta > det) { + return null; + } - var Polyline = g.Polyline = function(points) { + } else { + if (alpha < det || beta < det) { + return null; + } + } - if (!(this instanceof Polyline)) { - return new Polyline(points); - } + return [new Point( + this.start.x + (alpha * pt1Dir.x / det), + this.start.y + (alpha * pt1Dir.y / det) + )]; + }, - this.points = (Array.isArray(points)) ? points.map(Point) : []; - }; + // @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(shape, opt) { - Polyline.prototype = { + if (shape instanceof Line || + shape instanceof Rect || + shape instanceof Polyline || + shape instanceof Ellipse || + shape instanceof Path + ) { + var intersection = shape.intersectionWithLine(this, opt); - pointAtLength: function(length) { - var points = this.points; - var l = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var a = points[i]; - var b = points[i+1]; - var d = a.distance(b); - l += d; - if (length <= l) { - return Line(b, a).pointAt(d ? (l - length) / d : 0); + // Backwards compatibility + if (intersection && (shape instanceof Line)) { + intersection = intersection[0]; } + + return intersection; } + return null; }, + isDifferentiable: function() { + + return !this.start.equals(this.end); + }, + + // @return {double} length of the line length: function() { - var points = this.points; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - length += points[i].distance(points[i+1]); - } - return length; + + return sqrt(this.squaredLength()); }, - closestPoint: function(p) { - return this.pointAtLength(this.closestPointLength(p)); + // @return {point} my midpoint + midpoint: function() { + + return new Point( + (this.start.x + this.end.x) / 2, + (this.start.y + this.end.y) / 2 + ); }, - closestPointLength: function(p) { - var points = this.points; - var pointLength; - var minSqrDistance = Infinity; - var length = 0; - for (var i = 0, n = points.length - 1; i < n; i++) { - var line = Line(points[i], points[i+1]); - var lineLength = line.length(); - var cpNormalizedLength = line.closestPointNormalizedLength(p); - var cp = line.pointAt(cpNormalizedLength); - var sqrDistance = cp.squaredDistance(p); - if (sqrDistance < minSqrDistance) { - minSqrDistance = sqrDistance; - pointLength = length + cpNormalizedLength * lineLength; - } - length += lineLength; - } - return pointLength; - }, - - toString: function() { + // @return {point} my point at 't' <0,1> + pointAt: function(t) { - return this.points + ''; - }, + var start = this.start; + var end = this.end; - // Returns a convex-hull polyline from this polyline. - // this function implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan) - // output polyline starts at the first element of the original polyline that is on the hull - // output polyline then continues clockwise from that point - convexHull: function() { + if (t <= 0) return start.clone(); + if (t >= 1) return end.clone(); - var i; - var n; + return start.lerp(end, t); + }, - var points = this.points; + pointAtLength: function(length) { - // step 1: find the starting point - point with the lowest y (if equality, highest x) - var startPoint; - n = points.length; - for (i = 0; i < n; i++) { - if (startPoint === undefined) { - // if this is the first point we see, set it as start point - startPoint = points[i]; - } else if (points[i].y < startPoint.y) { - // start point should have lowest y from all points - startPoint = points[i]; - } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { - // if two points have the lowest y, choose the one that has highest x - // there are no points to the right of startPoint - no ambiguity about theta 0 - // if there are several coincident start point candidates, first one is reported - startPoint = points[i]; - } - } + var start = this.start; + var end = this.end; - // step 2: sort the list of points - // sorting by angle between line from startPoint to point and the x-axis (theta) - - // step 2a: create the point records = [point, originalIndex, angle] - var sortedPointRecords = []; - n = points.length; - for (i = 0; i < n; i++) { - var angle = startPoint.theta(points[i]); - if (angle === 0) { - angle = 360; // give highest angle to start point - // the start point will end up at end of sorted list - // the start point will end up at beginning of hull points list - } - - var entry = [points[i], i, angle]; - sortedPointRecords.push(entry); + fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - // step 2b: sort the list in place - sortedPointRecords.sort(function(record1, record2) { - // returning a negative number here sorts record1 before record2 - // if first angle is smaller than second, first angle should come before second - var sortOutput = record1[2] - record2[2]; // negative if first angle smaller - if (sortOutput === 0) { - // if the two angles are equal, sort by originalIndex - sortOutput = record2[1] - record1[1]; // negative if first index larger - // coincident points will be sorted in reverse-numerical order - // so the coincident points with lower original index will be considered first - } - return sortOutput; - }); + var lineLength = this.length(); + if (length >= lineLength) return (fromStart ? end.clone() : start.clone()); - // step 2c: duplicate start record from the top of the stack to the bottom of the stack - if (sortedPointRecords.length > 2) { - var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; - sortedPointRecords.unshift(startPointRecord); - } + return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength); + }, - // step 3a: go through sorted points in order and find those with right turns - // we want to get our results in clockwise order - var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull - var hullPointRecords = []; // stack of records with right turns - hull point candidates + // @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) { - var currentPointRecord; - var currentPoint; - var lastHullPointRecord; - var lastHullPoint; - var secondLastHullPointRecord; - var secondLastHullPoint; - while (sortedPointRecords.length !== 0) { - currentPointRecord = sortedPointRecords.pop(); - currentPoint = currentPointRecord[0]; + // Find the sign of the determinant of vectors (start,end), where p is the query point. + p = new g.Point(p); + var start = this.start; + var end = this.end; + var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x)); - // check if point has already been discarded - // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' - if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { - // this point had an incorrect turn at some previous iteration of this loop - // this disqualifies it from possibly being on the hull - continue; - } + return determinant / this.length(); + }, - var correctTurnFound = false; - while (!correctTurnFound) { - if (hullPointRecords.length < 2) { - // not enough points for comparison, just add current point - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; - - } else { - lastHullPointRecord = hullPointRecords.pop(); - lastHullPoint = lastHullPointRecord[0]; - secondLastHullPointRecord = hullPointRecords.pop(); - secondLastHullPoint = secondLastHullPointRecord[0]; + rotate: function(origin, angle) { - var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); + this.start.rotate(origin, angle); + this.end.rotate(origin, angle); + return this; + }, - if (crossProduct < 0) { - // found a right turn - hullPointRecords.push(secondLastHullPointRecord); - hullPointRecords.push(lastHullPointRecord); - hullPointRecords.push(currentPointRecord); - correctTurnFound = true; + round: function(precision) { - } else if (crossProduct === 0) { - // the three points are collinear - // three options: - // there may be a 180 or 0 degree angle at lastHullPoint - // or two of the three points are coincident - var THRESHOLD = 1e-10; // we have to take rounding errors into account - var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); - if (Math.abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 - // if the cross product is 0 because the angle is 180 degrees - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { - // if the cross product is 0 because two points are the same - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - - } else if (Math.abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 - // if the cross product is 0 because the angle is 0 degrees - // remove last hull point from hull BUT do not discard it - // reenter second-to-last hull point (will be last at next iter) - hullPointRecords.push(secondLastHullPointRecord); - // put last hull point back into the sorted point records list - sortedPointRecords.push(lastHullPointRecord); - // we are switching the order of the 0deg and 180deg points - // correct turn not found - } + var f = pow(10, precision || 0); + this.start.x = round(this.start.x * f) / f; + this.start.y = round(this.start.y * f) / f; + this.end.x = round(this.end.x * f) / f; + this.end.y = round(this.end.y * f) / f; + return this; + }, - } else { - // found a left turn - // discard last hull point (add to insidePoints) - //insidePoints.unshift(lastHullPoint); - insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; - // reenter second-to-last hull point (will be last at next iter of loop) - hullPointRecords.push(secondLastHullPointRecord); - // do not do anything with current point - // correct turn not found - } - } - } - } - // at this point, hullPointRecords contains the output points in clockwise order - // the points start with lowest-y,highest-x startPoint, and end at the same point + scale: function(sx, sy, origin) { - // step 3b: remove duplicated startPointRecord from the end of the array - if (hullPointRecords.length > 2) { - hullPointRecords.pop(); - } + this.start.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; + }, - // step 4: find the lowest originalIndex record and put it at the beginning of hull - var lowestHullIndex; // the lowest originalIndex on the hull - var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex - n = hullPointRecords.length; - for (i = 0; i < n; i++) { - var currentHullIndex = hullPointRecords[i][1]; + // @return {number} scale the line so that it has the requested length + setLength: function(length) { - if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { - lowestHullIndex = currentHullIndex; - indexOfLowestHullIndexRecord = i; - } - } + var currentLength = this.length(); + if (!currentLength) return this; - var hullPointRecordsReordered = []; - if (indexOfLowestHullIndexRecord > 0) { - var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); - var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); - hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - } else { - hullPointRecordsReordered = hullPointRecords; - } + var scaleFactor = length / currentLength; + return this.scale(scaleFactor, scaleFactor, this.start); + }, - var hullPoints = []; - n = hullPointRecordsReordered.length; - for (i = 0; i < n; i++) { - hullPoints.push(hullPointRecordsReordered[i][0]); - } + // @return {integer} length without sqrt + // @note for applications where the exact length is not necessary (e.g. compare only) + squaredLength: function() { - return Polyline(hullPoints); - } - }; + 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; + }, + tangentAt: function(t) { - g.scale = { + if (!this.isDifferentiable()) return null; - // Return the `value` from the `domain` interval scaled to the `range` interval. - linear: function(domain, range, value) { + var start = this.start; + var end = this.end; - var domainSpan = domain[1] - domain[0]; - var rangeSpan = range[1] - range[0]; - return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; - } - }; + var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1 - var normalizeAngle = g.normalizeAngle = function(angle) { + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - return (angle % 360) + (angle < 0 ? 360 : 0); - }; + return tangentLine; + }, - var snapToGrid = g.snapToGrid = function(value, gridSize) { + tangentAtLength: function(length) { - return gridSize * Math.round(value / gridSize); - }; + if (!this.isDifferentiable()) return null; - var toDeg = g.toDeg = function(rad) { + var start = this.start; + var end = this.end; - return (180 * rad / PI) % 360; - }; + var tangentStart = this.pointAtLength(length); - var toRad = g.toRad = function(deg, over360) { + var tangentLine = new Line(start, end); + tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested - over360 = over360 || false; - deg = over360 ? deg : (deg % 360); - return deg * PI / 180; - }; + return tangentLine; + }, - // For backwards compatibility: - g.ellipse = g.Ellipse; - g.line = g.Line; - g.point = g.Point; - g.rect = g.Rect; + translate: function(tx, ty) { - return g; + this.start.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, -})(); + // @return vector {point} of the line + vector: function() { -// Vectorizer. -// ----------- + return new Point(this.end.x - this.start.x, this.end.y - this.start.y); + }, -// A tiny library for making your life easier when dealing with SVG. -// The only Vectorizer dependency is the Geometry library. + toString: function() { + return this.start.toString() + ' ' + this.end.toString(); + } + }; -var V; -var Vectorizer; + // For backwards compatibility: + Line.prototype.intersection = Line.prototype.intersect; -V = Vectorizer = (function() { + // Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. + // Path created is not guaranteed to be a valid (serializable) path (might not start with an M). + var Path = g.Path = function(arg) { - 'use strict'; + if (!(this instanceof Path)) { + return new Path(arg); + } - var hasSvg = typeof window === 'object' && - !!( - window.SVGAngle || - document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') - ); + if (typeof arg === 'string') { // create from a path data string + return new Path.parse(arg); + } - // SVG support is required. - if (!hasSvg) { + this.segments = []; - // Return a function that throws an error when it is used. - return function() { - throw new Error('SVG is required to use Vectorizer.'); - }; - } + var i; + var n; - // 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' - }; + if (!arg) { + // don't do anything - var SVGversion = '1.1'; + } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array + n = arg.length; + if (arg[0].isSegment) { // create from an array of segments + for (i = 0; i < n; i++) { - var V = function(el, attrs, children) { + var segment = arg[i]; - // This allows using V() without the new keyword. - if (!(this instanceof V)) { - return V.apply(Object.create(V.prototype), arguments); - } + this.appendSegment(segment); + } - if (!el) return; + } else { // create from an array of Curves and/or Lines + var previousObj = null; + for (i = 0; i < n; i++) { - if (V.isV(el)) { - el = el.node; - } + var obj = arg[i]; - attrs = attrs || {}; + if (!((obj instanceof Line) || (obj instanceof Curve))) { + throw new Error('Cannot construct a path segment from the provided object.'); + } - if (V.isString(el)) { + if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); - if (el.toLowerCase() === 'svg') { + // if objects do not link up, moveto segments are inserted to cover the gaps + if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); - // Create a new SVG canvas. - el = V.createSvgDocument(); + if (obj instanceof Line) { + this.appendSegment(Path.createSegment('L', obj.end)); - } else if (el[0] === '<') { + } else if (obj instanceof Curve) { + this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); + } - // Create element from an SVG string. - // Allows constructs of type: `document.appendChild(V('').node)`. + previousObj = obj; + } + } - var svgDoc = V.createSvgDocument(el); + } else if (arg.isSegment) { // create from a single segment + this.appendSegment(arg); - // 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) { + } else if (arg instanceof Line) { // create from a single Line + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('L', arg.end)); - // Map child nodes to `V`s. - var arrayOfVels = []; - var i, len; + } else if (arg instanceof Curve) { // create from a single Curve + this.appendSegment(Path.createSegment('M', arg.start)); + this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); - for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { + } else if (arg instanceof Polyline && arg.points && arg.points.length !== 0) { // create from a Polyline + n = arg.points.length; + for (i = 0; i < n; i++) { - var childNode = svgDoc.childNodes[i]; - arrayOfVels.push(new V(document.importNode(childNode, true))); - } + var point = arg.points[i]; - return arrayOfVels; - } + if (i === 0) this.appendSegment(Path.createSegment('M', point)); + else this.appendSegment(Path.createSegment('L', point)); + } + } + }; - el = document.importNode(svgDoc.firstChild, true); + // More permissive than V.normalizePathData and Path.prototype.serialize. + // Allows path data strings that do not start with a Moveto command (unlike SVG specification). + // Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). + // Allows for command argument chaining. + // Throws an error if wrong number of arguments is provided with a command. + // Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). + Path.parse = function(pathData) { - } else { + if (!pathData) return new Path(); - el = document.createElementNS(ns.xmlns, el); - } + var path = new Path(); - V.ensureId(el); - } + var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; + var commands = pathData.match(commandRe); - this.node = el; + var numCommands = commands.length; + for (var i = 0; i < numCommands; i++) { - this.setAttributes(attrs); + var command = commands[i]; + var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?))|(?:(?:-?\.\d+))/g; + var args = command.match(argRe); - if (children) { - this.append(children); + var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] + path.appendSegment(segment); } - return this; + return path; }; - /** - * @param {SVGGElement} toElem - * @returns {SVGMatrix} - */ - V.prototype.getTransformToElement = function(toElem) { - toElem = V.toNode(toElem); - return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); - }; + // Create a segment or an array of segments. + // Accepts unlimited points/coords arguments after `type`. + Path.createSegment = function(type) { - /** - * @param {SVGMatrix} matrix - * @param {Object=} opt - * @returns {Vectorizer|SVGMatrix} Setter / Getter - */ - V.prototype.transform = function(matrix, opt) { + if (!type) throw new Error('Type must be provided.'); - var node = this.node; - if (V.isUndefined(matrix)) { - return V.transformStringToMatrix(this.attr('transform')); - } + var segmentConstructor = Path.segmentTypes[type]; + if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); - if (opt && opt.absolute) { - return this.attr('transform', V.matrixToTransformString(matrix)); + var args = []; + var n = arguments.length; + for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array + args.push(arguments[i]); } - var svgTransform = V.createSVGTransform(matrix); - node.transform.baseVal.appendItem(svgTransform); - return this; - }; + return applyToNew(segmentConstructor, args); + }, - V.prototype.translate = function(tx, ty, opt) { + Path.prototype = { - opt = opt || {}; - ty = ty || 0; + // Accepts one segment or an array of segments as argument. + // Throws an error if argument is not a segment or an array of segments. + appendSegment: function(arg) { - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; - // Is it a getter? - if (V.isUndefined(tx)) { - return transform.translate; - } + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); + var currentSegment; - var newTx = opt.absolute ? tx : transform.translate.tx + tx; - var newTy = opt.absolute ? ty : transform.translate.ty + ty; - var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; + var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null + var nextSegment = null; - // Note that `translate()` is always the first transformation. This is - // usually the desired case. - this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); - return this; - }; + if (!Array.isArray(arg)) { // arg is a segment + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - V.prototype.rotate = function(angle, cx, cy, opt) { + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.push(currentSegment); - opt = opt || {}; + } else { // arg is an array of segments + if (!arg[0].isSegment) throw new Error('Segments required.'); - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; + var n = arg.length; + for (var i = 0; i < n; i++) { - // Is it a getter? - if (V.isUndefined(angle)) { - return transform.rotate; - } + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.push(currentSegment); + previousSegment = currentSegment; + } + } + }, - transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); + // Returns the bbox of the path. + // If path has no segments, returns null. + // If path has only invisible segments, returns bbox of the end point of last segment. + bbox: function() { - angle %= 360; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - var newAngle = opt.absolute ? angle : transform.rotate.angle + angle; - var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : ''; - var newRotate = 'rotate(' + newAngle + newOrigin + ')'; + var bbox; + for (var i = 0; i < numSegments; i++) { - this.attr('transform', (transformAttr + ' ' + newRotate).trim()); - return this; - }; + var segment = segments[i]; + if (segment.isVisible) { + var segmentBBox = segment.bbox(); + bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; + } + } - // Note that `scale` as the only transformation does not combine with previous values. - V.prototype.scale = function(sx, sy) { + if (bbox) return bbox; - sy = V.isUndefined(sy) ? sx : sy; + // if the path has only invisible elements, return end point of last segment + var lastSegment = segments[numSegments - 1]; + return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); + }, - var transformAttr = this.attr('transform') || ''; - var transform = V.parseTransformString(transformAttr); - transformAttr = transform.value; + // Returns a new path that is a clone of this path. + clone: function() { - // Is it a getter? - if (V.isUndefined(sx)) { - return transform.scale; - } + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim(); + var path = new Path(); + for (var i = 0; i < numSegments; i++) { - var newScale = 'scale(' + sx + ',' + sy + ')'; + var segment = segments[i].clone(); + path.appendSegment(segment); + } - this.attr('transform', (transformAttr + ' ' + newScale).trim()); - return this; - }; + return path; + }, - // 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) { + closestPoint: function(p, opt) { - var box; - var node = this.node; - var ownerSVGElement = node.ownerSVGElement; + var t = this.closestPointT(p, opt); + if (!t) return null; - // 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); - } + return this.pointAtT(t); + }, - try { + closestPointLength: function(p, opt) { - box = node.getBBox(); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - } catch (e) { + var t = this.closestPointT(p, localOpt); + if (!t) return 0; - // Fallback for IE. - box = { - x: node.clientLeft, - y: node.clientTop, - width: node.clientWidth, - height: node.clientHeight - }; - } + return this.lengthAtT(t, localOpt); + }, - if (withoutTransformations) { - return g.Rect(box); - } + closestPointNormalizedLength: function(p, opt) { - var matrix = this.getTransformToElement(target || ownerSVGElement); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - return V.transformRect(box, matrix); - }; - - // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, - // i.e. after transformations are applied. - // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. - // Takes an (Object) `opt` argument (optional) with the following attributes: - // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this - // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); - V.prototype.getBBox = function(opt) { + var cpLength = this.closestPointLength(p, localOpt); + if (cpLength === 0) return 0; // shortcut - var options = {}; + var length = this.length(localOpt); + if (length === 0) return 0; // prevents division by zero - var outputBBox; - var node = this.node; - var ownerSVGElement = node.ownerSVGElement; + return cpLength / length; + }, - // 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); - } + // Private function. + closestPointT: function(p, opt) { - if (opt) { - if (opt.target) { // check if target exists - options.target = V.toNode(opt.target); // works for V objects, jquery objects, and node objects - } - if (opt.recursive) { - options.recursive = opt.recursive; - } - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!options.recursive) { - try { - outputBBox = node.getBBox(); - } catch (e) { - // Fallback for IE. - outputBBox = { - x: node.clientLeft, - y: node.clientTop, - width: node.clientWidth, - height: node.clientHeight - }; - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - if (!options.target) { - // transform like this (that is, not at all) - return g.Rect(outputBBox); - } else { - // transform like target - var matrix = this.getTransformToElement(options.target); - return V.transformRect(outputBBox, matrix); - } - } else { // if we want to calculate the bbox recursively - // browsers report correct bbox around svg elements (one that envelops the path lines tightly) - // but some browsers fail to report the same bbox when the elements are in a group (returning a looser bbox that also includes control points, like node.getClientRect()) - // this happens even if we wrap a single svg element into a group! - // this option setting makes the function recursively enter all the groups from this and deeper, get bboxes of the elements inside, then return a union of those bboxes + var closestPointT; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { - var children = this.children(); - var n = children.length; - - if (n === 0) { - return this.getBBox({ target: options.target, recursive: false }); + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isVisible) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointT = { segmentIndex: i, value: segmentClosestPointT }; + minSquaredDistance = squaredDistance; + } + } } - // recursion's initial pass-through setting: - // recursive passes-through just keep the target as whatever was set up here during the initial pass-through - if (!options.target) { - // transform children/descendants like this (their parent/ancestor) - options.target = this; - } // else transform children/descendants like target + if (closestPointT) return closestPointT; - for (var i = 0; i < n; i++) { - var currentChild = children[i]; + // if no visible segment, return end of last segment + return { segmentIndex: numSegments - 1, value: 1 }; + }, - var childBBox; + closestPointTangent: function(p, opt) { - // if currentChild is not a group element, get its bbox with a nonrecursive call - if (currentChild.children().length === 0) { - childBBox = currentChild.getBBox({ target: options.target, recursive: false }); - } - else { - // if currentChild is a group element (determined by checking the number of children), enter it with a recursive call - childBBox = currentChild.getBBox({ target: options.target, recursive: true }); - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!outputBBox) { - // if this is the first iteration - outputBBox = childBBox; - } else { - // make a new bounding box rectangle that contains this child's bounding box and previous bounding box - outputBBox = outputBBox.union(childBBox); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt + + var closestPointTangent; + var minSquaredDistance = Infinity; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + + if (segment.isDifferentiable()) { + var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); + var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); + var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); + + if (squaredDistance < minSquaredDistance) { + closestPointTangent = segment.tangentAtT(segmentClosestPointT); + minSquaredDistance = squaredDistance; + } } } - return outputBBox; - } - }; + if (closestPointTangent) return closestPointTangent; - V.prototype.text = function(content, opt) { + // if no valid segment, return null + return null; + }, - // 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 eol = opt.eol; - var lines = content.split('\n'); - var tspan; + // Checks whether two paths are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { - // 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'); - } + if (!p) return false; - // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. - this.attr('xml:space', 'preserve'); + var segments = this.segments; + var otherSegments = p.segments; - // Easy way to erase all `` children; - this.node.textContent = ''; + var numSegments = segments.length; + if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal - var textNode = this.node; + for (var i = 0; i < numSegments; i++) { - if (opt.textPath) { + var segment = segments[i]; + var otherSegment = otherSegments[i]; - // 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); + // as soon as an inequality is found in segments, return false + if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; } - // 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); - } + // if no inequality found in segments, return true + return true; + }, - 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); - } + // Accepts negative indices. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + getSegment: function(index) { - 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 segments = this.segments; + var numSegments = segments.length; + if (!numSegments === 0) throw new Error('Path has no segments.'); - var offset = 0; - var x = ((opt.x !== undefined) ? opt.x : this.attr('x')) || 0; + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - // Shift all the but first by one line (`1em`) - var lineHeight = opt.lineHeight || '1em'; - if (opt.lineHeight === 'auto') { - lineHeight = '1.5em'; - } + return segments[index]; + }, - var firstLineHeight = 0; - for (var i = 0; i < lines.length; i++) { + // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. + getSegmentSubdivisions: function(opt) { - var vLineAttributes = { 'class': 'v-line' }; - if (i === 0) { - vLineAttributes.dy = '0em'; - } else { - vLineAttributes.dy = lineHeight; - vLineAttributes.x = x; - } - var vLine = V('tspan', vLineAttributes); + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - var lastI = lines.length - 1; - var line = lines[i]; - if (line) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + // not using opt.segmentSubdivisions + // not using localOpt - // Get the line height based on the biggest font size in the annotations for this line. - var maxFontSize = 0; - if (opt.annotations) { + var segmentSubdivisions = []; + for (var i = 0; i < numSegments; i++) { - // 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 }); + var segment = segments[i]; + var subdivisions = segment.getSubdivisions({ precision: precision }); + segmentSubdivisions.push(subdivisions); + } - var lastJ = lineAnnotations.length - 1; - for (var j = 0; j < lineAnnotations.length; j++) { + return segmentSubdivisions; + }, - var annotation = lineAnnotations[j]; - if (V.isObject(annotation)) { + // Insert `arg` at given `index`. + // `index = 0` means insert at the beginning. + // `index = segments.length` means insert at the end. + // Accepts negative indices, from `-1` to `-(segments.length + 1)`. + // Accepts one segment or an array of segments as argument. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + insertSegment: function(index, arg) { - var fontSize = parseFloat(annotation.attrs['font-size']); - if (fontSize && fontSize > maxFontSize) { - maxFontSize = fontSize; - } + var segments = this.segments; + var numSegments = segments.length; + // works even if path has no segments - 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']); - } + // note that these are incremented comapared to getSegments() + // we can insert after last element (note that this changes the meaning of index -1) + if (index < 0) index = numSegments + index + 1; // convert negative indices to positive + if (index > numSegments || index < 0) throw new Error('Index out of range.'); - if (eol && j === lastJ && i !== lastI) { - annotation.t += eol; - } - tspan.node.textContent = annotation.t; + var currentSegment; - } else { + var previousSegment = null; + var nextSegment = null; - if (eol && j === lastJ && i !== lastI) { - annotation += eol; - } - tspan = document.createTextNode(annotation || ' '); - } - vLine.append(tspan); - } + if (numSegments !== 0) { + if (index >= 1) { + previousSegment = segments[index - 1]; + nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null - if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) { + } else { // if index === 0 + // previousSegment is null + nextSegment = segments[0]; + } + } - vLine.attr('dy', (maxFontSize * 1.2) + 'px'); - } + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - } else { + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 0, currentSegment); - if (eol && i !== lastI) { - line += eol; - } + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); - vLine.node.textContent = line; - } + var n = arg.length; + for (var i = 0; i < n; i++) { - if (i === 0) { - firstLineHeight = maxFontSize; + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; } - } 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 = '-'; + isDifferentiable: function() { + + var segments = this.segments; + var numSegments = segments.length; + + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + // as soon as a differentiable segment is found in segments, return true + if (segment.isDifferentiable()) return true; } - V(textNode).append(vLine); + // if no differentiable segment is found in segments, return false + return false; + }, - offset += line.length + 1; // + 1 = newline character. - } + // Checks whether current path segments are valid. + // Note that d is allowed to be empty - should disable rendering of the path. + isValid: function() { - // `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 === null) { - this.attr('y', firstLineHeight || '0.8em'); - } + var segments = this.segments; + var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto + return isValid; + }, - return this; - }; + // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. + // If path has no segments, returns 0. + length: function(opt) { - /** - * @public - * @param {string} name - * @returns {Vectorizer} - */ - V.prototype.removeAttr = function(name) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array - var qualifiedName = V.qualifyAttr(name); - var el = this.node; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - if (qualifiedName.ns) { - if (el.hasAttributeNS(qualifiedName.ns, qualifiedName.local)) { - el.removeAttributeNS(qualifiedName.ns, qualifiedName.local); + var length = 0; + for (var i = 0; i < numSegments; i++) { + + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + length += segment.length({ subdivisions: subdivisions }); } - } else if (el.hasAttribute(name)) { - el.removeAttribute(name); - } - return this; - }; - V.prototype.attr = function(name, value) { + return length; + }, - if (V.isUndefined(name)) { + // Private function. + lengthAtT: function(t, opt) { - // Return all attributes. - var attributes = this.node.attributes; - var attrs = {}; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return 0; // if segments is an empty array - for (var i = 0; i < attributes.length; i++) { - attrs[attributes[i].name] = attributes[i].value; - } + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return 0; // regardless of t.value - return attrs; - } + var tValue = t.value; + if (segmentIndex >= numSegments) { + segmentIndex = numSegments - 1; + tValue = 1; + } + else if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - if (V.isString(name) && V.isUndefined(value)) { - return this.node.getAttribute(name); - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - if (typeof name === 'object') { + var subdivisions; + var length = 0; + for (var i = 0; i < segmentIndex; i++) { - for (var attrName in name) { - if (name.hasOwnProperty(attrName)) { - this.setAttribute(attrName, name[attrName]); - } + var segment = segments[i]; + subdivisions = segmentSubdivisions[i]; + length += segment.length({ precisison: precision, subdivisions: subdivisions }); } - } else { + segment = segments[segmentIndex]; + subdivisions = segmentSubdivisions[segmentIndex]; + length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); - this.setAttribute(name, value); - } + return length; + }, - return this; - }; + // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + pointAt: function(ratio, opt) { - V.prototype.remove = function() { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (this.node.parentNode) { - this.node.parentNode.removeChild(this.node); - } + if (ratio <= 0) return this.start.clone(); + if (ratio >= 1) return this.end.clone(); - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - V.prototype.empty = function() { + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - while (this.node.firstChild) { - this.node.removeChild(this.node.firstChild); - } + return this.pointAtLength(length, localOpt); + }, - return this; - }; + // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + pointAtLength: function(length, opt) { - /** - * @private - * @param {object} attrs - * @returns {Vectorizer} - */ - V.prototype.setAttributes = function(attrs) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - for (var key in attrs) { - if (attrs.hasOwnProperty(key)) { - this.setAttribute(key, attrs[key]); + if (length === 0) return this.start.clone(); + + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value } - } - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - V.prototype.append = function(els) { + var lastVisibleSegment; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - if (!V.isArray(els)) { - els = [els]; - } + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - for (var i = 0, len = els.length; i < len; i++) { - this.node.appendChild(V.toNode(els[i])); - } + if (segment.isVisible) { + if (length <= (l + d)) { + return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } - return this; - }; + lastVisibleSegment = segment; + } - V.prototype.prepend = function(els) { + l += d; + } - var child = this.node.firstChild; - return child ? V(child).before(els) : this.append(els); - }; + // if length requested is higher than the length of the path, return last visible segment endpoint + if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); - V.prototype.before = function(els) { + // if no visible segment, return last segment end point (no matter if fromStart or no) + var lastSegment = segments[numSegments - 1]; + return lastSegment.end.clone(); + }, - var node = this.node; - var parent = node.parentNode; + // Private function. + pointAtT: function(t) { - if (parent) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!V.isArray(els)) { - els = [els]; - } + var segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].pointAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); - for (var i = 0, len = els.length; i < len; i++) { - parent.insertBefore(V.toNode(els[i]), node); - } - } + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - return this; - }; + return segments[segmentIndex].pointAtT(tValue); + }, - V.prototype.appendTo = function(node) { - V.toNode(node).appendChild(this.node); - return this; - }, + // Helper method for adding segments. + prepareSegment: function(segment, previousSegment, nextSegment) { - V.prototype.svg = function() { + // insert after previous segment and before previous segment's next segment + segment.previousSegment = previousSegment; + segment.nextSegment = nextSegment; + if (previousSegment) previousSegment.nextSegment = segment; + if (nextSegment) nextSegment.previousSegment = segment; - return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); - }; + var updateSubpathStart = segment; + if (segment.isSubpathStart) { + segment.subpathStartSegment = segment; // assign self as subpath start segment + updateSubpathStart = nextSegment; // start updating from next segment + } - V.prototype.defs = function() { + // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments + if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); - var defs = this.svg().node.getElementsByTagName('defs'); + return segment; + }, - return (defs && defs.length) ? V(defs[0]) : undefined; - }; + // Default precision + PRECISION: 3, - V.prototype.clone = function() { + // Remove the segment at `index`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + removeSegment: function(index) { - 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; - }; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); - V.prototype.findOne = function(selector) { + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - var found = this.node.querySelector(selector); - return found ? V(found) : undefined; - }; + var removedSegment = segments.splice(index, 1)[0]; + var previousSegment = removedSegment.previousSegment; + var nextSegment = removedSegment.nextSegment; - V.prototype.find = function(selector) { + // link the previous and next segments together (if present) + if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null + if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null - var vels = []; - var nodes = this.node.querySelectorAll(selector); + // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached + if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, - if (nodes) { + // Replace the segment at `index` with `arg`. + // Accepts negative indices, from `-1` to `-segments.length`. + // Accepts one segment or an array of segments as argument. + // Throws an error if path has no segments. + // Throws an error if index is out of range. + // Throws an error if argument is not a segment or an array of segments. + replaceSegment: function(index, arg) { - // Map DOM elements to `V`s. - for (var i = 0; i < nodes.length; i++) { - vels.push(V(nodes[i])); - } - } + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) throw new Error('Path has no segments.'); - return vels; - }; + if (index < 0) index = numSegments + index; // convert negative indices to positive + if (index >= numSegments || index < 0) throw new Error('Index out of range.'); - // Returns an array of V elements made from children of this.node. - V.prototype.children = function() { + var currentSegment; - var children = this.node.childNodes; - - var outputArray = []; - for (var i = 0; i < children.length; i++) { - var currentChild = children[i]; - if (currentChild.nodeType === 1) { - outputArray.push(V(children[i])); - } - } - return outputArray; - }; + var replacedSegment = segments[index]; + var previousSegment = replacedSegment.previousSegment; + var nextSegment = replacedSegment.nextSegment; - // Find an index of an element inside its container. - V.prototype.index = function() { + var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? - var index = 0; - var node = this.node.previousSibling; + if (!Array.isArray(arg)) { + if (!arg || !arg.isSegment) throw new Error('Segment required.'); - while (node) { - // nodeType 1 for ELEMENT_NODE - if (node.nodeType === 1) index++; - node = node.previousSibling; - } + currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); + segments.splice(index, 1, currentSegment); // directly replace - return index; - }; + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` - V.prototype.findParentByClass = function(className, terminator) { + } else { + if (!arg[0].isSegment) throw new Error('Segments required.'); - var ownerSVGElement = this.node.ownerSVGElement; - var node = this.node.parentNode; + segments.splice(index, 1); - while (node && node !== terminator && node !== ownerSVGElement) { + var n = arg.length; + for (var i = 0; i < n; i++) { - var vel = V(node); - if (vel.hasClass(className)) { - return vel; + var currentArg = arg[i]; + currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); + segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments + previousSegment = currentSegment; + + if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` + } } - node = node.parentNode; - } + // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached + if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); + }, - return null; - }; + scale: function(sx, sy, origin) { - // https://jsperf.com/get-common-parent - V.prototype.contains = function(el) { + var segments = this.segments; + var numSegments = segments.length; - var a = this.node; - var b = V.toNode(el); - var bup = b && b.parentNode; + for (var i = 0; i < numSegments; i++) { - return (a === bup) || !!(bup && bup.nodeType === 1 && (a.compareDocumentPosition(bup) & 16)); - }; + var segment = segments[i]; + segment.scale(sx, sy, origin); + } - // Convert global point into the coordinate space of this element. - V.prototype.toLocalPoint = function(x, y) { + return this; + }, - var svg = this.svg().node; + segmentAt: function(ratio, opt) { - var p = svg.createSVGPoint(); - p.x = x; - p.y = y; + var index = this.segmentIndexAt(ratio, opt); + if (!index) return null; - try { + return this.getSegment(index); + }, - var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse()); - var globalToLocalMatrix = this.getTransformToElement(svg).inverse(); + // Accepts negative length. + segmentAtLength: function(length, opt) { - } 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; - } + var index = this.segmentIndexAtLength(length, opt); + if (!index) return null; - return globalPoint.matrixTransform(globalToLocalMatrix); - }; + return this.getSegment(index); + }, - V.prototype.translateCenterToPoint = function(p) { + segmentIndexAt: function(ratio, opt) { - var bbox = this.getBBox({ target: this.svg() }); - var center = bbox.center(); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - this.translate(p.x - center.x, p.y - center.y); - return this; - }; + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - // 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) { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - // 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 pathLength = this.length(localOpt); + var length = pathLength * ratio; - var s = this.scale(); - this.attr('transform', ''); - this.scale(s.sx, s.sy); + return this.segmentIndexAtLength(length, localOpt); + }, - var svg = this.svg().node; - var bbox = this.getBBox({ target: target || svg }); + toPoints: function(opt) { - // 1. Translate to origin. - var translateToOrigin = svg.createSVGTransform(); - translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - // 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); + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + + var points = []; + var partialPoints = []; + for (var i = 0; i < numSegments; i++) { + var segment = segments[i]; + if (segment.isVisible) { + var currentSegmentSubdivisions = segmentSubdivisions[i]; + if (currentSegmentSubdivisions.length > 0) { + var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { + return curve.start; + }); + Array.prototype.push.apply(partialPoints, subdivisionPoints); + } else { + partialPoints.push(segment.start); + } + } else if (partialPoints.length > 0) { + partialPoints.push(segments[i - 1].end); + points.push(partialPoints); + partialPoints = []; + } + } - // 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)); + if (partialPoints.length > 0) { + partialPoints.push(this.end); + points.push(partialPoints); + } + return points; + }, - // 4. Apply transformations. - var ctm = this.getTransformToElement(target || svg); - var transform = svg.createSVGTransform(); - transform.setMatrix( - translateFinal.matrix.multiply( - rotateAroundOrigin.matrix.multiply( - translateToOrigin.matrix.multiply( - ctm))) - ); + toPolylines: function(opt) { - // 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 polylines = []; + var points = this.toPoints(opt); + if (!points) return null; + for (var i = 0, n = points.length; i < n; i++) { + polylines.push(new Polyline(points[i])); + } - var decomposition = V.decomposeMatrix(transform.matrix); + return polylines; + }, - 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); + intersectionWithLine: function(line, opt) { - return this; - }; + var intersection = null; + var polylines = this.toPolylines(opt); + if (!polylines) return null; + for (var i = 0, n = polylines.length; i < n; i++) { + var polyline = polylines[i]; + var polylineIntersection = line.intersect(polyline); + if (polylineIntersection) { + intersection || (intersection = []); + if (Array.isArray(polylineIntersection)) { + Array.prototype.push.apply(intersection, polylineIntersection); + } else { + intersection.push(polylineIntersection); + } + } + } - V.prototype.animateAlongPath = function(attrs, path) { + return intersection; + }, - path = V.toNode(path); + // Accepts negative length. + segmentIndexAtLength: function(length, opt) { - var id = V.ensureId(path); - var animateMotion = V('animateMotion', attrs); - var mpath = V('mpath', { 'xlink:href': '#' + id }); + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - animateMotion.append(mpath); + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - 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') { + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) - var animation = animateMotion.node; - animation.animators = []; + var lastVisibleSegmentIndex = null; + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - var animationID = animation.getAttribute('id'); - if (animationID) id2anim[animationID] = animation; + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - 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(); + if (segment.isVisible) { + if (length <= (l + d)) return i; + lastVisibleSegmentIndex = i; } + + l += d; } - } - return this; - }; - V.prototype.hasClass = function(className) { + // if length requested is higher than the length of the path, return last visible segment index + // if no visible segment, return null + return lastVisibleSegmentIndex; + }, - return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); - }; + // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + tangentAt: function(ratio, opt) { - V.prototype.addClass = function(className) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - if (!this.hasClass(className)) { - var prevClasses = this.node.getAttribute('class') || ''; - this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); - } + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - return this; - }; + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; - V.prototype.removeClass = function(className) { + var pathLength = this.length(localOpt); + var length = pathLength * ratio; - 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.tangentAtLength(length, localOpt); + }, - return this; - }; + // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. + // Accepts negative length. + tangentAtLength: function(length, opt) { - V.prototype.toggleClass = function(className, toAdd) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - if (toRemove) { - this.removeClass(className); - } else { - this.addClass(className); - } + opt = opt || {}; + var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; + var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; + // not using localOpt - return this; - }; + var lastValidSegment; // visible AND differentiable (with a tangent) + var l = 0; // length so far + for (var i = (fromStart ? 0 : (numSegments - 1)); (fromStart ? (i < numSegments) : (i >= 0)); (fromStart ? (i++) : (i--))) { - // 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) { + var segment = segments[i]; + var subdivisions = segmentSubdivisions[i]; + var d = segment.length({ precision: precision, subdivisions: subdivisions }); - 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; - }; + if (segment.isDifferentiable()) { + if (length <= (l + d)) { + return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); + } - V.prototype.convertToPath = function() { + lastValidSegment = segment; + } - var path = V('path'); - path.attr(this.attr()); - var d = this.convertToPathData(); - if (d) { - path.attr('d', d); - } - return path; - }; + l += d; + } - V.prototype.convertToPathData = function() { + // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment + if (lastValidSegment) { + var t = (fromStart ? 1 : 0); + return lastValidSegment.tangentAtT(t); + } - var tagName = this.node.tagName.toUpperCase(); + // if no valid segment, return null + return null; + }, - 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); - } + // Private function. + tangentAtT: function(t) { - throw new Error(tagName + ' cannot be converted to PATH.'); - }; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; // if segments is an empty array - // 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 segmentIndex = t.segmentIndex; + if (segmentIndex < 0) return segments[0].tangentAtT(0); + if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); - var svg = this.svg().node; - target = target || svg; - var bbox = this.getBBox({ target: target }); - var center = bbox.center(); + var tValue = t.value; + if (tValue < 0) tValue = 0; + else if (tValue > 1) tValue = 1; - if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; + return segments[segmentIndex].tangentAtT(tValue); + }, - var spot; - var tagName = this.node.localName.toUpperCase(); + translate: function(tx, ty) { - // 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 segments = this.segments; + var numSegments = segments.length; - 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); + for (var i = 0; i < numSegments; i++) { - } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { + var segment = segments[i]; + segment.translate(tx, ty); + } - var pathNode = (tagName === 'PATH') ? this : this.convertToPath(); - var samples = pathNode.sample(); - var minDistance = Infinity; - var closestSamples = []; + return this; + }, - var i, sample, gp, centerDistance, refDistance, distance; + // Helper method for updating subpath start of segments, starting with the one provided. + updateSubpathStartSegment: function(segment) { - for (i = 0; i < samples.length; i++) { + var previousSegment = segment.previousSegment; // may be null + while (segment && !segment.isSubpathStart) { - 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; + // assign previous segment's subpath start segment to this segment + if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null + else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! - if (distance < minDistance) { - minDistance = distance; - closestSamples = [{ sample: sample, refDistance: refDistance }]; - } else if (distance < minDistance + 1) { - closestSamples.push({ sample: sample, refDistance: refDistance }); - } + previousSegment = segment; + segment = segment.nextSegment; // move on to the segment after etc. } + }, - closestSamples.sort(function(a, b) { - return a.refDistance - b.refDistance; - }); + // Returns a string that can be used to reconstruct the path. + // Additional error checking compared to toString (must start with M segment). + serialize: function() { - if (closestSamples[0]) { - spot = closestSamples[0].sample; - } - } + if (!this.isValid()) throw new Error('Invalid path segments.'); - return spot; - }; + return this.toString(); + }, - /** - * @private - * @param {string} name - * @param {string} value - * @returns {Vectorizer} - */ - V.prototype.setAttribute = function(name, value) { + toString: function() { - var el = this.node; + var segments = this.segments; + var numSegments = segments.length; - if (value === null) { - this.removeAttr(name); - return this; - } + var pathData = ''; + for (var i = 0; i < numSegments; i++) { - var qualifiedName = V.qualifyAttr(name); + var segment = segments[i]; + pathData += segment.serialize() + ' '; + } - 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 pathData.trim(); } - - return this; - }; - - // Create an SVG document element. - // If `content` is passed, it will be used as the SVG content of the `` root element. - V.createSvgDocument = function(content) { - - var svg = '' + (content || '') + ''; - var xml = V.parseXML(svg, { async: false }); - return xml.documentElement; }; - V.idCounter = 0; + Object.defineProperty(Path.prototype, 'start', { + // Getter for the first visible endpoint of the path. - // A function returning a unique identifier for this client session with every call. - V.uniqueId = function() { + configurable: true, - return 'v-' + (++V.idCounter); - }; + enumerable: true, - V.toNode = function(el) { - return V.isV(el) ? el.node : (el.nodeName && el || el[0]); - }; + get: function() { - V.ensureId = function(node) { - node = V.toNode(node); - return node.id || (node.id = V.uniqueId()); - }; - // 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. This is used in the text() method but it is - // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests - // when you want to compare the actual DOM text content without having to add the unicode character in - // the place of all spaces. - V.sanitizeText = function(text) { + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; - return (text || '').replace(/ /g, '\u00A0'); - }; + for (var i = 0; i < numSegments; i++) { - V.isUndefined = function(value) { + var segment = segments[i]; + if (segment.isVisible) return segment.start; + } - return typeof value === 'undefined'; - }; + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); - V.isString = function(value) { + Object.defineProperty(Path.prototype, 'end', { + // Getter for the last visible endpoint of the path. - return typeof value === 'string'; - }; + configurable: true, - V.isObject = function(value) { + enumerable: true, - return value && (typeof value === 'object'); - }; + get: function() { - V.isArray = Array.isArray; + var segments = this.segments; + var numSegments = segments.length; + if (numSegments === 0) return null; - V.parseXML = function(data, opt) { + for (var i = numSegments - 1; i >= 0; i--) { - opt = opt || {}; + var segment = segments[i]; + if (segment.isVisible) return segment.end; + } - var xml; + // if no visible segment, return last segment end point + return segments[numSegments - 1].end; + } + }); - try { - var parser = new DOMParser(); + /* + Point is the most basic object consisting of x/y coordinate. - if (!V.isUndefined(opt.async)) { - parser.async = opt.async; - } + Possible instantiations are: + * `Point(10, 20)` + * `new Point(10, 20)` + * `Point('10 20')` + * `Point(Point(10, 20))` + */ + var Point = g.Point = function(x, y) { - xml = parser.parseFromString(data, 'text/xml'); - } catch (error) { - xml = undefined; + if (!(this instanceof Point)) { + return new Point(x, y); } - if (!xml || xml.getElementsByTagName('parsererror').length) { - throw new Error('Invalid XML: ' + data); + if (typeof x === 'string') { + var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@'); + x = parseFloat(xy[0]); + y = parseFloat(xy[1]); + + } else if (Object(x) === x) { + y = x.y; + x = x.x; } - return xml; + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; }; - /** - * @param {string} name - * @returns {{ns: string|null, local: string}} namespace and attribute name - */ - V.qualifyAttr = function(name) { - - if (name.indexOf(':') !== -1) { - var combinedKey = name.split(':'); - return { - ns: ns[combinedKey[0]], - local: combinedKey[1] - }; - } + // Alternative constructor, from polar coordinates. + // @param {number} Distance. + // @param {number} Angle in radians. + // @param {point} [optional] Origin. + Point.fromPolar = function(distance, angle, origin) { - return { - ns: null, - local: name - }; - }; + origin = (origin && new Point(origin)) || new Point(0, 0); + var x = abs(distance * cos(angle)); + var y = abs(distance * sin(angle)); + var deg = normalizeAngle(toDeg(angle)); - V.transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi; - V.transformSeparatorRegex = /[ ,]+/; - V.transformationListRegex = /^(\w+)\((.*)\)/; + if (deg < 90) { + y = -y; - V.transformStringToMatrix = function(transform) { + } else if (deg < 180) { + x = -x; + y = -y; - var transformationMatrix = V.createSVGMatrix(); - var matches = transform && transform.match(V.transformRegex); - if (!matches) { - return transformationMatrix; + } else if (deg < 270) { + x = -x; } - for (var i = 0, n = matches.length; i < n; i++) { - var transformationString = matches[i]; - - var transformationMatch = transformationString.match(V.transformationListRegex); - if (transformationMatch) { - var sx, sy, tx, ty, angle; - var ctm = V.createSVGMatrix(); - var args = transformationMatch[2].split(V.transformSeparatorRegex); - switch (transformationMatch[1].toLowerCase()) { - case 'scale': - sx = parseFloat(args[0]); - sy = (args[1] === undefined) ? sx : parseFloat(args[1]); - ctm = ctm.scaleNonUniform(sx, sy); - break; - case 'translate': - tx = parseFloat(args[0]); - ty = parseFloat(args[1]); - ctm = ctm.translate(tx, ty); - break; - case 'rotate': - angle = parseFloat(args[0]); - tx = parseFloat(args[1]) || 0; - ty = parseFloat(args[2]) || 0; - if (tx !== 0 || ty !== 0) { - ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty); - } else { - ctm = ctm.rotate(angle); - } - break; - case 'skewx': - angle = parseFloat(args[0]); - ctm = ctm.skewX(angle); - break; - case 'skewy': - angle = parseFloat(args[0]); - ctm = ctm.skewY(angle); - break; - case 'matrix': - ctm.a = parseFloat(args[0]); - ctm.b = parseFloat(args[1]); - ctm.c = parseFloat(args[2]); - ctm.d = parseFloat(args[3]); - ctm.e = parseFloat(args[4]); - ctm.f = parseFloat(args[5]); - break; - default: - continue; - } + return new Point(origin.x + x, origin.y + y); + }; - transformationMatrix = transformationMatrix.multiply(ctm); - } + // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. + Point.random = function(x1, x2, y1, y2) { - } - return transformationMatrix; + return new Point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); }; - V.matrixToTransformString = function(matrix) { - matrix || (matrix = true); + Point.prototype = { - return 'matrix(' + - (matrix.a !== undefined ? matrix.a : 1) + ',' + - (matrix.b !== undefined ? matrix.b : 0) + ',' + - (matrix.c !== undefined ? matrix.c : 0) + ',' + - (matrix.d !== undefined ? matrix.d : 1) + ',' + - (matrix.e !== undefined ? matrix.e : 0) + ',' + - (matrix.f !== undefined ? matrix.f : 0) + - ')'; - }; + // 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) { - V.parseTransformString = function(transform) { + if (r.containsPoint(this)) { + return this; + } - var translate, rotate, scale; + this.x = min(max(this.x, r.x), r.x + r.width); + this.y = min(max(this.y, r.y), r.y + r.height); + return this; + }, - if (transform) { + // Return the bearing between me and the given point. + bearing: function(point) { - var separator = V.transformSeparatorRegex; + return (new Line(this, point)).bearing(); + }, - // Allow reading transform string with a single matrix - if (transform.trim().indexOf('matrix') >= 0) { + // Returns change in angle from my previous position (-dx, -dy) to my new position + // relative to ref point. + changeInAngle: function(dx, dy, ref) { - var matrix = V.transformStringToMatrix(transform); - var decomposedMatrix = V.decomposeMatrix(matrix); + // Revert the translation and measure the change in angle around x-axis. + return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref); + }, - translate = [decomposedMatrix.translateX, decomposedMatrix.translateY]; - scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]; - rotate = [decomposedMatrix.rotation]; + clone: function() { - var transformations = []; - if (translate[0] !== 0 || translate[0] !== 0) { - transformations.push('translate(' + translate + ')'); - } - if (scale[0] !== 1 || scale[1] !== 1) { - transformations.push('scale(' + scale + ')'); - } - if (rotate[0] !== 0) { - transformations.push('rotate(' + rotate + ')'); - } - transform = transformations.join(' '); + return new Point(this); + }, - } else { + difference: function(dx, dy) { - var translateMatch = transform.match(/translate\((.*?)\)/); - if (translateMatch) { - translate = translateMatch[1].split(separator); - } - var rotateMatch = transform.match(/rotate\((.*?)\)/); - if (rotateMatch) { - rotate = rotateMatch[1].split(separator); - } - var scaleMatch = transform.match(/scale\((.*?)\)/); - if (scaleMatch) { - scale = scaleMatch[1].split(separator); - } + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; } - } - var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; + return new Point(this.x - (dx || 0), this.y - (dy || 0)); + }, - return { - value: transform, - translate: { - tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, - ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 - }, - rotate: { - angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, - cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, - cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined - }, - scale: { - sx: sx, - sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx - } - }; - }; + // Returns distance between me and point `p`. + distance: function(p) { - V.deltaTransformPoint = function(matrix, point) { + return (new Line(this, p)).length(); + }, - var dx = point.x * matrix.a + point.y * matrix.c + 0; - var dy = point.x * matrix.b + point.y * matrix.d + 0; - return { x: dx, y: dy }; - }; + squaredDistance: function(p) { - V.decomposeMatrix = function(matrix) { + return (new Line(this, p)).squaredLength(); + }, - // @see https://gist.github.com/2052247 + equals: function(p) { - // calculate delta transform point - var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); - var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); + return !!p && + this.x === p.x && + this.y === p.y; + }, - // calculate skew - var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); - var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); + magnitude: function() { - return { + return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; + }, - translateX: matrix.e, - translateY: matrix.f, - scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), - scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), - skewX: skewX, - skewY: skewY, - rotation: skewX // rotation is the same as skew x - }; - }; + // Returns a manhattan (taxi-cab) distance between me and point `p`. + manhattanDistance: function(p) { - // Return the `scale` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToScale = function(matrix) { + return abs(p.x - this.x) + abs(p.y - this.y); + }, - var a,b,c,d; - if (matrix) { - a = V.isUndefined(matrix.a) ? 1 : matrix.a; - d = V.isUndefined(matrix.d) ? 1 : matrix.d; - b = matrix.b; - c = matrix.c; - } else { - a = d = 1; - } - return { - sx: b ? Math.sqrt(a * a + b * b) : a, - sy: c ? Math.sqrt(c * c + d * d) : d - }; - }, + // Move point on line starting from ref ending at me by + // distance distance. + move: function(ref, distance) { - // Return the `rotate` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToRotate = function(matrix) { + var theta = toRad((new Point(ref)).theta(this)); + var offset = this.offset(cos(theta) * distance, -sin(theta) * distance); + return offset; + }, - var p = { x: 0, y: 1 }; - if (matrix) { - p = V.deltaTransformPoint(matrix, p); - } + // 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) { - return { - angle: g.normalizeAngle(g.toDeg(Math.atan2(p.y, p.x)) - 90) - }; - }, + var scale = (length || 1) / this.magnitude(); + return this.scale(scale, scale); + }, - // Return the `translate` transformation from the following equation: - // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` - V.matrixToTranslate = function(matrix) { + // Offset me by the specified amount. + offset: function(dx, dy) { - return { - tx: (matrix && matrix.e) || 0, - ty: (matrix && matrix.f) || 0 - }; - }, + if ((Object(dx) === dx)) { + dy = dx.y; + dx = dx.x; + } - V.isV = function(object) { + this.x += dx || 0; + this.y += dy || 0; + return this; + }, - return object instanceof V; - }; + // Returns a point that is the reflection of me with + // the center of inversion in ref point. + reflection: function(ref) { - // For backwards compatibility: - V.isVElement = V.isV; + return (new Point(ref)).move(this, this.distance(ref)); + }, - var svgDocument = V('svg').node; + // Rotate point by angle around origin. + // Angle is flipped because this is a left-handed coord system (y-axis points downward). + rotate: function(origin, angle) { - V.createSVGMatrix = function(matrix) { + origin = origin || new g.Point(0, 0); - var svgMatrix = svgDocument.createSVGMatrix(); - for (var component in matrix) { - svgMatrix[component] = matrix[component]; - } + angle = toRad(normalizeAngle(-angle)); + var cosAngle = cos(angle); + var sinAngle = sin(angle); - return svgMatrix; - }; + var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x; + var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y; - V.createSVGTransform = function(matrix) { + this.x = x; + this.y = y; + return this; + }, - if (!V.isUndefined(matrix)) { + round: function(precision) { - if (!(matrix instanceof SVGMatrix)) { - matrix = V.createSVGMatrix(matrix); - } + var f = pow(10, precision || 0); + this.x = round(this.x * f) / f; + this.y = round(this.y * f) / f; + return this; + }, - return svgDocument.createSVGTransformFromMatrix(matrix); - } + // Scale point with origin. + scale: function(sx, sy, origin) { - return svgDocument.createSVGTransform(); - }; + origin = (origin && new Point(origin)) || new Point(0, 0); + this.x = origin.x + sx * (this.x - origin.x); + this.y = origin.y + sy * (this.y - origin.y); + return this; + }, - V.createSVGPoint = function(x, y) { + snapToGrid: function(gx, gy) { - var p = svgDocument.createSVGPoint(); - p.x = x; - p.y = y; - return p; - }; + this.x = snapToGrid(this.x, gx); + this.y = snapToGrid(this.y, gy || gx); + return this; + }, - V.transformRect = function(r, matrix) { + // Compute the angle between me and `p` and the x axis. + // (cartesian-to-polar coordinates conversion) + // Return theta angle in degrees. + theta: function(p) { - var p = svgDocument.createSVGPoint(); + p = new Point(p); - p.x = r.x; - p.y = r.y; - var corner1 = p.matrixTransform(matrix); + // Invert the y-axis. + var y = -(p.y - this.y); + var x = p.x - this.x; + var rad = atan2(y, x); // defined for all 0 corner cases - p.x = r.x + r.width; - p.y = r.y; - var corner2 = p.matrixTransform(matrix); + // Correction for III. and IV. quadrant. + if (rad < 0) { + rad = 2 * PI + rad; + } - p.x = r.x + r.width; - p.y = r.y + r.height; - var corner3 = p.matrixTransform(matrix); + return 180 * rad / PI; + }, - p.x = r.x; - p.y = r.y + r.height; - var corner4 = p.matrixTransform(matrix); + // Compute the angle between vector from me to p1 and the vector from me to p2. + // ordering of points p1 and p2 is important! + // theta function's angle convention: + // returns angles between 0 and 180 when the angle is counterclockwise + // returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones + // returns NaN if any of the points p1, p2 is coincident with this point + angleBetween: function(p1, p2) { - var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x); - var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x); - var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y); - var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y); + var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1)); - return g.Rect(minX, minY, maxX - minX, maxY - minY); - }; + if (angleBetween < 0) { + angleBetween += 360; // correction to keep angleBetween between 0 and 360 + } - V.transformPoint = function(p, matrix) { + return angleBetween; + }, - return g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); - }; + // Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p. + // Returns NaN if p is at 0,0. + vectorAngle: function(p) { - // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to - // an object (`{ fill: 'blue', stroke: 'red' }`). - V.styleToObject = function(styleString) { - var ret = {}; - var styles = styleString.split(';'); - for (var i = 0; i < styles.length; i++) { - var style = styles[i]; - var pair = style.split('='); - ret[pair[0].trim()] = pair[1].trim(); - } - return ret; - }; + var zero = new Point(0,0); + return zero.angleBetween(this, p); + }, - // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js - V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { + toJSON: function() { - var svgArcMax = 2 * Math.PI - 1e-6; - var r0 = innerRadius; - var r1 = outerRadius; - var a0 = startAngle; - var a1 = endAngle; - var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); - var df = da < Math.PI ? '0' : '1'; - var c0 = Math.cos(a0); - var s0 = Math.sin(a0); - var c1 = Math.cos(a1); - var s1 = Math.sin(a1); - - return (da >= svgArcMax) - ? (r0 - ? 'M0,' + r1 - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 - + 'M0,' + r0 - + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) - + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 - + 'Z' - : 'M0,' + r1 - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) - + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 - + 'Z') - : (r0 - ? 'M' + r1 * c0 + ',' + r1 * s0 - + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 - + 'L' + r0 * c1 + ',' + r0 * s1 - + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 - + 'Z' - : 'M' + r1 * c0 + ',' + r1 * s0 - + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 - + 'L0,0' - + 'Z'); - }; - - // Merge attributes from object `b` with attributes in object `a`. - // Note that this modifies the object `a`. - // Also important to note that attributes are merged but CSS classes are concatenated. - V.mergeAttrs = function(a, b) { - - for (var attr in b) { - - if (attr === 'class') { - // Concatenate classes. - a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; - } else if (attr === 'style') { - // `style` attribute can be an object. - if (V.isObject(a[attr]) && V.isObject(b[attr])) { - // `style` stored in `a` is an object. - a[attr] = V.mergeAttrs(a[attr], b[attr]); - } else if (V.isObject(a[attr])) { - // `style` in `a` is an object but it's a string in `b`. - // Convert the style represented as a string to an object in `b`. - a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); - } else if (V.isObject(b[attr])) { - // `style` in `a` is a string, in `b` it's an object. - a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); - } else { - // Both styles are strings. - a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); - } - } else { - a[attr] = b[attr]; - } - } + return { x: this.x, y: this.y }; + }, - return a; - }; + // Converts rectangular to polar coordinates. + // An origin can be specified, otherwise it's 0@0. + toPolar: function(o) { - V.annotateString = function(t, annotations, opt) { + o = (o && new Point(o)) || new 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(new Point(x, y))); + return this; + }, - annotations = annotations || []; - opt = opt || {}; + toString: function() { - var offset = opt.offset || 0; - var compacted = []; - var batch; - var ret = []; - var item; - var prev; + return this.x + '@' + this.y; + }, - for (var i = 0; i < t.length; i++) { + update: function(x, y) { - item = ret[i] = t[i]; + this.x = x || 0; + this.y = y || 0; + return this; + }, - for (var j = 0; j < annotations.length; j++) { + // Returns the dot product of this point with given other point + dot: function(p) { - var annotation = annotations[j]; - var start = annotation.start + offset; - var end = annotation.end + offset; + return p ? (this.x * p.x + this.y * p.y) : NaN; + }, - if (i >= start && i < end) { - // Annotation applies. - if (V.isObject(item)) { - // There is more than one annotation to be applied => Merge attributes. - item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); - } else { - item = ret[i] = { t: t[i], attrs: annotation.attrs }; - } - if (opt.includeAnnotationIndices) { - (item.annotations || (item.annotations = [])).push(j); - } - } - } + // Returns the cross product of this point relative to two other points + // this point is the common point + // point p1 lies on the first vector, point p2 lies on the second vector + // watch out for the ordering of points p1 and p2! + // positive result indicates a clockwise ("right") turn from first to second vector + // negative result indicates a counterclockwise ("left") turn from first to second vector + // note that the above directions are reversed from the usual answer on the Internet + // that is because we are in a left-handed coord system (because the y-axis points downward) + cross: function(p1, p2) { - prev = ret[i - 1]; + return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN; + }, - if (!prev) { - batch = item; + // Linear interpolation + lerp: function(p, t) { - } else if (V.isObject(item) && V.isObject(prev)) { - // Both previous item and the current one are annotations. If the attributes - // didn't change, merge the text. - if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { - batch.t += item.t; - } else { - compacted.push(batch); - batch = item; - } + var x = this.x; + var y = this.y; + return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y); + } + }; - } else if (V.isObject(item)) { - // Previous item was a string, current item is an annotation. - compacted.push(batch); - batch = item; + Point.prototype.translate = Point.prototype.offset; - } else if (V.isObject(prev)) { - // Previous item was an annotation, current item is a string. - compacted.push(batch); - batch = item; + var Rect = g.Rect = function(x, y, w, h) { - } else { - // Both previous and current item are strings. - batch = (batch || '') + item; - } + if (!(this instanceof Rect)) { + return new Rect(x, y, w, h); } - if (batch) { - compacted.push(batch); + if ((Object(x) === x)) { + y = x.y; + w = x.width; + h = x.height; + x = x.x; } - return compacted; + this.x = x === undefined ? 0 : x; + this.y = y === undefined ? 0 : y; + this.width = w === undefined ? 0 : w; + this.height = h === undefined ? 0 : h; }; - V.findAnnotationsAtIndex = function(annotations, index) { - - var found = []; - - if (annotations) { - - annotations.forEach(function(annotation) { - - if (annotation.start < index && index <= annotation.end) { - found.push(annotation); - } - }); - } + Rect.fromEllipse = function(e) { - return found; + e = new Ellipse(e); + return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b); }; - V.findAnnotationsBetweenIndexes = function(annotations, start, end) { + Rect.prototype = { - var found = []; + // 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) { - if (annotations) { + if (!angle) return this.clone(); - annotations.forEach(function(annotation) { + 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 new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); + }, - if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { - found.push(annotation); - } - }); - } + bottomLeft: function() { - return found; - }; + return new Point(this.x, this.y + this.height); + }, - // Shift all the text annotations after character `index` by `offset` positions. - V.shiftAnnotations = function(annotations, index, offset) { + bottomLine: function() { - if (annotations) { + return new Line(this.bottomLeft(), this.bottomRight()); + }, - annotations.forEach(function(annotation) { + bottomMiddle: function() { - if (annotation.start < index && annotation.end >= index) { - annotation.end += offset; - } else if (annotation.start >= index) { - annotation.start += offset; - annotation.end += offset; - } - }); - } + return new Point(this.x + this.width / 2, this.y + this.height); + }, - return annotations; - }; + center: function() { - V.convertLineToPathData = function(line) { + return new Point(this.x + this.width / 2, this.y + this.height / 2); + }, - line = V(line); - var d = [ - 'M', line.attr('x1'), line.attr('y1'), - 'L', line.attr('x2'), line.attr('y2') - ].join(' '); - return d; - }; + clone: function() { - V.convertPolygonToPathData = function(polygon) { + return new Rect(this); + }, - var points = V.getPointsFromSvgNode(V(polygon).node); + // @return {bool} true if point p is insight me + containsPoint: function(p) { - if (!(points.length > 0)) return null; + p = new Point(p); + return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height; + }, - return V.svgPointsToPath(points) + ' Z'; - }; + // @return {bool} true if rectangle `r` is inside me. + containsRect: function(r) { - V.convertPolylineToPathData = function(polyline) { + var r0 = new Rect(this).normalize(); + var r1 = new Rect(r).normalize(); + var w0 = r0.width; + var h0 = r0.height; + var w1 = r1.width; + var h1 = r1.height; - var points = V.getPointsFromSvgNode(V(polyline).node); + if (!w0 || !h0 || !w1 || !h1) { + // At least one of the dimensions is 0 + return false; + } - if (!(points.length > 0)) return null; + var x0 = r0.x; + var y0 = r0.y; + var x1 = r1.x; + var y1 = r1.y; - return V.svgPointsToPath(points); - }; - - V.svgPointsToPath = function(points) { + w1 += x1; + w0 += x0; + h1 += y1; + h0 += y0; - var i; + return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0; + }, - for (i = 0; i < points.length; i++) { - points[i] = points[i].x + ' ' + points[i].y; - } + corner: function() { - return 'M ' + points.join(' L'); - }; + return new Point(this.x + this.width, this.y + this.height); + }, - V.getPointsFromSvgNode = function(node) { + // @return {boolean} true if rectangles are equal. + equals: function(r) { - node = V.toNode(node); - var points = []; - var nodePoints = node.points; - if (nodePoints) { - for (var i = 0; i < nodePoints.numberOfItems; i++) { - points.push(nodePoints.getItem(i)); - } - } + var mr = (new Rect(this)).normalize(); + var nr = (new Rect(r)).normalize(); + return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height; + }, - return points; - }; + // @return {rect} if rectangles intersect, {null} if not. + intersect: function(r) { - V.KAPPA = 0.5522847498307935; + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = r.origin(); + var rCorner = r.corner(); - V.convertCircleToPathData = function(circle) { + // No intersection found + if (rCorner.x <= myOrigin.x || + rCorner.y <= myOrigin.y || + rOrigin.x >= myCorner.x || + rOrigin.y >= myCorner.y) return null; - circle = V(circle); - var cx = parseFloat(circle.attr('cx')) || 0; - var cy = parseFloat(circle.attr('cy')) || 0; - var r = parseFloat(circle.attr('r')); - var cd = r * V.KAPPA; // Control distance. + var x = max(myOrigin.x, rOrigin.x); + var y = max(myOrigin.y, rOrigin.y); - var d = [ - 'M', cx, cy - r, // Move to the first point. - 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. - 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. - 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. - 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. - 'Z' - ].join(' '); - return d; - }; + return new Rect(x, y, min(myCorner.x, rCorner.x) - x, min(myCorner.y, rCorner.y) - y); + }, - V.convertEllipseToPathData = function(ellipse) { + intersectionWithLine: function(line) { - ellipse = V(ellipse); - var cx = parseFloat(ellipse.attr('cx')) || 0; - var cy = parseFloat(ellipse.attr('cy')) || 0; - var rx = parseFloat(ellipse.attr('rx')); - var ry = parseFloat(ellipse.attr('ry')) || rx; - var cdx = rx * V.KAPPA; // Control distance x. - var cdy = ry * V.KAPPA; // Control distance y. + var r = this; + var rectLines = [ r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine() ]; + var points = []; + var dedupeArr = []; + var pt, i; - var d = [ - 'M', cx, cy - ry, // Move to the first point. - 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. - 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. - 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. - 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. - 'Z' - ].join(' '); - return d; - }; + var n = rectLines.length; + for (i = 0; i < n; i ++) { - V.convertRectToPathData = function(rect) { + pt = line.intersect(rectLines[i]); + if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) { + points.push(pt); + dedupeArr.push(pt.toString()); + } + } - rect = V(rect); + return points.length > 0 ? points : null; + }, - return V.rectToPath({ - x: parseFloat(rect.attr('x')) || 0, - y: parseFloat(rect.attr('y')) || 0, - width: parseFloat(rect.attr('width')) || 0, - height: parseFloat(rect.attr('height')) || 0, - rx: parseFloat(rect.attr('rx')) || 0, - ry: parseFloat(rect.attr('ry')) || 0 - }); - }; + // 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) { - // Convert a rectangle to SVG path commands. `r` is an object of the form: - // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, - // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for - // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle - // that has only `rx` and `ry` attributes). - V.rectToPath = function(r) { + p = new Point(p); + var center = new Point(this.x + this.width / 2, this.y + this.height / 2); + var result; - var d; - var x = r.x; - var y = r.y; - var width = r.width; - var height = r.height; - var topRx = Math.min(r.rx || r['top-rx'] || 0, width / 2); - var bottomRx = Math.min(r.rx || r['bottom-rx'] || 0, width / 2); - var topRy = Math.min(r.ry || r['top-ry'] || 0, height / 2); - var bottomRy = Math.min(r.ry || r['bottom-ry'] || 0, height / 2); + if (angle) p.rotate(center, angle); - if (topRx || bottomRx || topRy || bottomRy) { - d = [ - 'M', x, y + topRy, - 'v', height - topRy - bottomRy, - 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, - 'h', width - 2 * bottomRx, - 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, - 'v', -(height - bottomRy - topRy), - 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, - 'h', -(width - 2 * topRx), - 'a', topRx, topRy, 0, 0, 0, -topRx, topRy, - 'Z' - ]; - } else { - d = [ - 'M', x, y, - 'H', x + width, - 'V', y + height, - 'H', x, - 'V', y, - 'Z' + // (clockwise, starting from the top side) + var sides = [ + this.topLine(), + this.rightLine(), + this.bottomLine(), + this.leftLine() ]; - } + var connector = new Line(center, p); - return d.join(' '); - }; + 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; + }, - V.namespace = ns; + leftLine: function() { - return V; + return new Line(this.topLeft(), this.bottomLeft()); + }, -})(); + leftMiddle: function() { + return new Point(this.x , this.y + this.height / 2); + }, -// Global namespace. + // Move and expand me. + // @param r {rectangle} representing deltas + moveAndExpand: function(r) { -var joint = { + this.x += r.x || 0; + this.y += r.y || 0; + this.width += r.width || 0; + this.height += r.height || 0; + return this; + }, - version: '2.0.1', + // Offset me by the specified amount. + offset: function(dx, dy) { - config: { - // The class name prefix config is for advanced use only. - // Be aware that if you change the prefix, the JointJS CSS will no longer function properly. - classNamePrefix: 'joint-', - defaultTheme: 'default' - }, + // pretend that this is a point and call offset() + // rewrites x and y according to dx and dy + return Point.prototype.offset.call(this, dx, dy); + }, - // `joint.dia` namespace. - dia: {}, + // 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) { - // `joint.ui` namespace. - ui: {}, + if (dx === undefined) { + dx = 0; + } - // `joint.layout` namespace. - layout: {}, + if (dy === undefined) { + dy = dx; + } - // `joint.shapes` namespace. - shapes: {}, + this.x -= dx; + this.y -= dy; + this.width += 2 * dx; + this.height += 2 * dy; - // `joint.format` namespace. - format: {}, + return this; + }, - // `joint.connectors` namespace. - connectors: {}, + // 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() { - // `joint.highlighters` namespace. - highlighters: {}, + 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; + }, - // `joint.routers` namespace. - routers: {}, + origin: function() { - // `joint.mvc` namespace. - mvc: { - views: {} - }, + return new Point(this.x, this.y); + }, - setTheme: function(theme, opt) { + // @return {point} a point on my boundary nearest to the given point. + // @see Squeak Smalltalk, Rectangle>>pointNearestTo: + pointNearestToPoint: function(point) { - opt = opt || {}; + point = new Point(point); + if (this.containsPoint(point)) { + var side = this.sideNearestToPoint(point); + switch (side){ + case 'right': return new Point(this.x + this.width, point.y); + case 'left': return new Point(this.x, point.y); + case 'bottom': return new Point(point.x, this.y + this.height); + case 'top': return new Point(point.x, this.y); + } + } + return point.adhereToRect(this); + }, - joint.util.invoke(joint.mvc.views, 'setTheme', theme, opt); + rightLine: function() { - // Update the default theme on the view prototype. - joint.mvc.View.prototype.defaultTheme = theme; - }, + return new Line(this.topRight(), this.bottomRight()); + }, - // `joint.env` namespace. - env: { + rightMiddle: function() { - _results: {}, + return new Point(this.x + this.width, this.y + this.height / 2); + }, - _tests: { + round: function(precision) { - svgforeignobject: function() { - return !!document.createElementNS && - /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); - } + 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; }, - addTest: function(name, fn) { + // Scale rectangle with origin. + scale: function(sx, sy, origin) { - return joint.env._tests[name] = fn; + origin = this.origin().scale(sx, sy, origin); + this.x = origin.x; + this.y = origin.y; + this.width *= sx; + this.height *= sy; + return this; }, - test: function(name) { + maxRectScaleToFit: function(rect, origin) { - var fn = joint.env._tests[name]; + rect = new Rect(rect); + origin || (origin = rect.center()); - if (!fn) { - throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); - } + var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4; + var ox = origin.x; + var oy = origin.y; - var result = joint.env._results[name]; + // 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. - if (typeof result !== 'undefined') { - return result; - } + sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity; - try { - result = fn(); - } catch (error) { - result = false; + // Top Left + var p1 = rect.topLeft(); + 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.bottomRight(); + 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); } - // Cache the test result. - joint.env._results[name] = result; + return { + sx: min(sx1, sx2, sx3, sx4), + sy: min(sy1, sy2, sy3, sy4) + }; + }, - return result; - } - }, + maxRectUniformScaleToFit: function(rect, origin) { - util: { + var scale = this.maxRectScaleToFit(rect, origin); + return min(scale.sx, scale.sy); + }, - // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. - hashCode: function(str) { + // @return {string} (left|right|top|bottom) side which is nearest to point + // @see Squeak Smalltalk, Rectangle>>sideNearestTo: + sideNearestToPoint: function(point) { - var hash = 0; - if (str.length == 0) return hash; - for (var i = 0; i < str.length; i++) { - var c = str.charCodeAt(i); - hash = ((hash << 5) - hash) + c; - hash = hash & hash; // Convert to 32bit integer + point = new 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'; } - return hash; + if (distToTop < closest) { + closest = distToTop; + side = 'top'; + } + if (distToBottom < closest) { + closest = distToBottom; + side = 'bottom'; + } + return side; }, - getByPath: function(obj, path, delim) { + snapToGrid: function(gx, gy) { - var keys = Array.isArray(path) ? path.slice() : path.split(delim || '/'); - var key; + 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; + }, - while (keys.length) { - key = keys.shift(); - if (Object(obj) === obj && key in obj) { - obj = obj[key]; - } else { - return undefined; - } - } - return obj; + topLine: function() { + + return new Line(this.topLeft(), this.topRight()); }, - setByPath: function(obj, path, value, delim) { + topMiddle: function() { - var keys = Array.isArray(path) ? path : path.split(delim || '/'); + return new Point(this.x + this.width / 2, this.y); + }, - var diver = obj; - var i = 0; + topRight: function() { - for (var len = keys.length; i < len - 1; i++) { - // diver creates an empty object if there is no nested object under such a key. - // This means that one can populate an empty nested object with setByPath(). - diver = diver[keys[i]] || (diver[keys[i]] = {}); - } - diver[keys[len - 1]] = value; + return new Point(this.x + this.width, this.y); + }, - return obj; + toJSON: function() { + + return { x: this.x, y: this.y, width: this.width, height: this.height }; }, - unsetByPath: function(obj, path, delim) { + toString: function() { - delim = delim || '/'; + return this.origin().toString() + ' ' + this.corner().toString(); + }, - var pathArray = Array.isArray(path) ? path.slice() : path.split(delim); + // @return {rect} representing the union of both rectangles. + union: function(rect) { - var propertyToRemove = pathArray.pop(); - if (pathArray.length > 0) { + rect = new Rect(rect); + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = rect.origin(); + var rCorner = rect.corner(); - // unsetting a nested attribute - var parent = joint.util.getByPath(obj, pathArray, delim); + var originX = min(myOrigin.x, rOrigin.x); + var originY = min(myOrigin.y, rOrigin.y); + var cornerX = max(myCorner.x, rCorner.x); + var cornerY = max(myCorner.y, rCorner.y); - if (parent) { - delete parent[propertyToRemove]; - } + return new Rect(originX, originY, cornerX - originX, cornerY - originY); + } + }; - } else { + Rect.prototype.bottomRight = Rect.prototype.corner; - // unsetting a primitive attribute - delete obj[propertyToRemove]; - } + Rect.prototype.topLeft = Rect.prototype.origin; - return obj; - }, + Rect.prototype.translate = Rect.prototype.offset; - flattenObject: function(obj, delim, stop) { + var Polyline = g.Polyline = function(points) { - delim = delim || '/'; - var ret = {}; + if (!(this instanceof Polyline)) { + return new Polyline(points); + } - for (var key in obj) { + if (typeof points === 'string') { + return new Polyline.parse(points); + } - if (!obj.hasOwnProperty(key)) continue; + this.points = (Array.isArray(points) ? points.map(Point) : []); + }; - var shouldGoDeeper = typeof obj[key] === 'object'; - if (shouldGoDeeper && stop && stop(obj[key])) { - shouldGoDeeper = false; - } + Polyline.parse = function(svgString) { - if (shouldGoDeeper) { + if (svgString === '') return new Polyline(); - var flatObject = this.flattenObject(obj[key], delim, stop); + var points = []; - for (var flatKey in flatObject) { - if (!flatObject.hasOwnProperty(flatKey)) continue; - ret[key + delim + flatKey] = flatObject[flatKey]; - } + var coords = svgString.split(/\s|,/); + var n = coords.length; + for (var i = 0; i < n; i += 2) { + points.push({ x: +coords[i], y: +coords[i + 1] }); + } - } else { + return new Polyline(points); + }; - ret[key] = obj[key]; - } + Polyline.prototype = { + + bbox: function() { + + var x1 = Infinity; + var x2 = -Infinity; + var y1 = Infinity; + var y2 = -Infinity; + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + + var point = points[i]; + var x = point.x; + var y = point.y; + + if (x < x1) x1 = x; + if (x > x2) x2 = x; + if (y < y1) y1 = y; + if (y > y2) y2 = y; } - return ret; + return new Rect(x1, y1, x2 - x1, y2 - y1); }, - uuid: function() { + clone: function() { - // credit: http://stackoverflow.com/posts/2117523/revisions + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random() * 16|0; - var v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }, + var newPoints = []; + for (var i = 0; i < numPoints; i++) { - // Generate global unique id for obj and store it as a property of the object. - guid: function(obj) { + var point = points[i].clone(); + newPoints.push(point); + } - this.guid.id = this.guid.id || 1; - obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); - return obj.id; + return new Polyline(newPoints); }, - toKebabCase: function(string) { + closestPoint: function(p) { - return string.replace(/[A-Z]/g, '-$&').toLowerCase(); + var cpLength = this.closestPointLength(p); + + return this.pointAtLength(cpLength); }, - // Copy all the properties to the first argument from the following arguments. - // All the properties will be overwritten by the properties from the following - // arguments. Inherited properties are ignored. - mixin: _.assign, + closestPointLength: function(p) { - // Copy all properties to the first argument from the following - // arguments only in case if they don't exists in the first argument. - // All the function propererties in the first argument will get - // additional property base pointing to the extenders same named - // property function's call method. - supplement: _.defaults, + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty + if (numPoints === 1) return 0; // if there is only one point - // Same as `mixin()` but deep version. - deepMixin: _.mixin, + var cpLength; + var minSqrDistance = Infinity; + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { - // Same as `supplement()` but deep version. - deepSupplement: _.defaultsDeep, + var line = new Line(points[i], points[i + 1]); + var lineLength = line.length(); - normalizeEvent: function(evt) { + var cpNormalizedLength = line.closestPointNormalizedLength(p); + var cp = line.pointAt(cpNormalizedLength); - var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; - if (touchEvt) { - for (var property in evt) { - // copy all the properties from the input event that are not - // defined on the touch event (functions included). - if (touchEvt[property] === undefined) { - touchEvt[property] = evt[property]; - } + var sqrDistance = cp.squaredDistance(p); + if (sqrDistance < minSqrDistance) { + minSqrDistance = sqrDistance; + cpLength = length + (cpNormalizedLength * lineLength); } - return touchEvt; + + length += lineLength; } - return evt; + return cpLength; }, - nextFrame: (function() { + closestPointNormalizedLength: function(p) { - var raf; + var cpLength = this.closestPointLength(p); + if (cpLength === 0) return 0; // shortcut - if (typeof window !== 'undefined') { + var length = this.length(); + if (length === 0) return 0; // prevents division by zero - raf = window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame; - } + return cpLength / length; + }, - if (!raf) { + closestPointTangent: function(p) { - var lastTime = 0; + var cpLength = this.closestPointLength(p); - raf = function(callback) { + return this.tangentAtLength(cpLength); + }, - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + // Returns a convex-hull polyline from this polyline. + // Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan). + // Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise. + // Minimal polyline is found (only vertices of the hull are reported, no collinear points). + convexHull: function() { - lastTime = currTime + timeToCall; + var i; + var n; - return id; - }; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return new Polyline(); // if points array is empty + + // step 1: find the starting point - point with the lowest y (if equality, highest x) + var startPoint; + for (i = 0; i < numPoints; i++) { + if (startPoint === undefined) { + // if this is the first point we see, set it as start point + startPoint = points[i]; + + } else if (points[i].y < startPoint.y) { + // start point should have lowest y from all points + startPoint = points[i]; + + } else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) { + // if two points have the lowest y, choose the one that has highest x + // there are no points to the right of startPoint - no ambiguity about theta 0 + // if there are several coincident start point candidates, first one is reported + startPoint = points[i]; + } } - return function(callback, context) { - return context - ? raf(callback.bind(context)) - : raf(callback); - }; + // step 2: sort the list of points + // sorting by angle between line from startPoint to point and the x-axis (theta) - })(), + // step 2a: create the point records = [point, originalIndex, angle] + var sortedPointRecords = []; + for (i = 0; i < numPoints; i++) { - cancelFrame: (function() { + var angle = startPoint.theta(points[i]); + if (angle === 0) { + angle = 360; // give highest angle to start point + // the start point will end up at end of sorted list + // the start point will end up at beginning of hull points list + } - var caf; - var client = typeof window != 'undefined'; + var entry = [points[i], i, angle]; + sortedPointRecords.push(entry); + } - if (client) { + // step 2b: sort the list in place + sortedPointRecords.sort(function(record1, record2) { + // returning a negative number here sorts record1 before record2 + // if first angle is smaller than second, first angle should come before second - caf = window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.webkitCancelRequestAnimationFrame || - window.msCancelAnimationFrame || - window.msCancelRequestAnimationFrame || - window.oCancelAnimationFrame || - window.oCancelRequestAnimationFrame || - window.mozCancelAnimationFrame || - window.mozCancelRequestAnimationFrame; + var sortOutput = record1[2] - record2[2]; // negative if first angle smaller + if (sortOutput === 0) { + // if the two angles are equal, sort by originalIndex + sortOutput = record2[1] - record1[1]; // negative if first index larger + // coincident points will be sorted in reverse-numerical order + // so the coincident points with lower original index will be considered first + } + + return sortOutput; + }); + + // step 2c: duplicate start record from the top of the stack to the bottom of the stack + if (sortedPointRecords.length > 2) { + var startPointRecord = sortedPointRecords[sortedPointRecords.length-1]; + sortedPointRecords.unshift(startPointRecord); } - caf = caf || clearTimeout; + // step 3a: go through sorted points in order and find those with right turns + // we want to get our results in clockwise order + var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull + var hullPointRecords = []; // stack of records with right turns - hull point candidates - return client ? caf.bind(window) : caf; + var currentPointRecord; + var currentPoint; + var lastHullPointRecord; + var lastHullPoint; + var secondLastHullPointRecord; + var secondLastHullPoint; + while (sortedPointRecords.length !== 0) { - })(), + currentPointRecord = sortedPointRecords.pop(); + currentPoint = currentPointRecord[0]; - shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { + // check if point has already been discarded + // keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex' + if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) { + // this point had an incorrect turn at some previous iteration of this loop + // this disqualifies it from possibly being on the hull + continue; + } - var bbox; - var spot; + var correctTurnFound = false; + while (!correctTurnFound) { - if (!magnet) { + if (hullPointRecords.length < 2) { + // not enough points for comparison, just add current point + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; - // There is no magnet, try to make the best guess what is the - // wrapping SVG element. This is because we want this "smart" - // connection points to work out of the box without the - // programmer to put magnet marks to any of the subelements. - // For example, we want the functoin to work on basic.Path elements - // without any special treatment of such elements. - // The code below guesses the wrapping element based on - // one simple assumption. The wrapping elemnet is the - // first child of the scalable group if such a group exists - // or the first child of the rotatable group if not. - // This makese sense because usually the wrapping element - // is below any other sub element in the shapes. - var scalable = view.$('.scalable')[0]; - var rotatable = view.$('.rotatable')[0]; + } else { + lastHullPointRecord = hullPointRecords.pop(); + lastHullPoint = lastHullPointRecord[0]; + secondLastHullPointRecord = hullPointRecords.pop(); + secondLastHullPoint = secondLastHullPointRecord[0]; - if (scalable && scalable.firstChild) { + var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint); - magnet = scalable.firstChild; + if (crossProduct < 0) { + // found a right turn + hullPointRecords.push(secondLastHullPointRecord); + hullPointRecords.push(lastHullPointRecord); + hullPointRecords.push(currentPointRecord); + correctTurnFound = true; - } else if (rotatable && rotatable.firstChild) { + } else if (crossProduct === 0) { + // the three points are collinear + // three options: + // there may be a 180 or 0 degree angle at lastHullPoint + // or two of the three points are coincident + var THRESHOLD = 1e-10; // we have to take rounding errors into account + var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint); + if (abs(angleBetween - 180) < THRESHOLD) { // rouding around 180 to 180 + // if the cross product is 0 because the angle is 180 degrees + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found - magnet = rotatable.firstChild; - } - } + } else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) { + // if the cross product is 0 because two points are the same + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found - if (magnet) { + } else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0 + // if the cross product is 0 because the angle is 0 degrees + // remove last hull point from hull BUT do not discard it + // reenter second-to-last hull point (will be last at next iter) + hullPointRecords.push(secondLastHullPointRecord); + // put last hull point back into the sorted point records list + sortedPointRecords.push(lastHullPointRecord); + // we are switching the order of the 0deg and 180deg points + // correct turn not found + } - spot = V(magnet).findIntersection(reference, linkView.paper.viewport); - if (!spot) { - bbox = V(magnet).getBBox({ target: linkView.paper.viewport }); + } else { + // found a left turn + // discard last hull point (add to insidePoints) + //insidePoints.unshift(lastHullPoint); + insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint; + // reenter second-to-last hull point (will be last at next iter of loop) + hullPointRecords.push(secondLastHullPointRecord); + // do not do anything with current point + // correct turn not found + } + } } + } + // at this point, hullPointRecords contains the output points in clockwise order + // the points start with lowest-y,highest-x startPoint, and end at the same point - } else { - - bbox = view.model.getBBox(); - spot = bbox.intersectionWithLineFromCenterToPoint(reference); + // step 3b: remove duplicated startPointRecord from the end of the array + if (hullPointRecords.length > 2) { + hullPointRecords.pop(); } - return spot || bbox.center(); - }, - parseCssNumeric: function(strValue, restrictUnits) { + // step 4: find the lowest originalIndex record and put it at the beginning of hull + var lowestHullIndex; // the lowest originalIndex on the hull + var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex + n = hullPointRecords.length; + for (i = 0; i < n; i++) { - restrictUnits = restrictUnits || []; - var cssNumeric = { value: parseFloat(strValue) }; + var currentHullIndex = hullPointRecords[i][1]; - if (Number.isNaN(cssNumeric.value)) { - return null; + if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) { + lowestHullIndex = currentHullIndex; + indexOfLowestHullIndexRecord = i; + } } - var validUnitsExp = restrictUnits.join('|'); + var hullPointRecordsReordered = []; + if (indexOfLowestHullIndexRecord > 0) { + var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord); + var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord); + hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk); - if (joint.util.isString(strValue)) { - var matches = new RegExp('(\\d+)(' + validUnitsExp + ')$').exec(strValue); - if (!matches) { - return null; - } - if (matches[2]) { - cssNumeric.unit = matches[2]; - } + } else { + hullPointRecordsReordered = hullPointRecords; } - return cssNumeric; - }, - breakText: function(text, size, styles, opt) { + var hullPoints = []; + n = hullPointRecordsReordered.length; + for (i = 0; i < n; i++) { + hullPoints.push(hullPointRecordsReordered[i][0]); + } - opt = opt || {}; + return new Polyline(hullPoints); + }, - var width = size.width; - var height = size.height; + // Checks whether two polylines are exactly the same. + // If `p` is undefined or null, returns false. + equals: function(p) { - var svgDocument = opt.svgDocument || V('svg').node; - var textElement = V('').attr(styles || {}).node; - var textSpan = textElement.firstChild; - var textNode = document.createTextNode(''); + if (!p) return false; - // Prevent flickering - textElement.style.opacity = 0; - // Prevent FF from throwing an uncaught exception when `getBBox()` - // called on element that is not in the render tree (is not measurable). - // .getComputedTextLength() returns always 0 in this case. - // Note that the `textElement` resp. `textSpan` can become hidden - // when it's appended to the DOM and a `display: none` CSS stylesheet - // rule gets applied. - textElement.style.display = 'block'; - textSpan.style.display = 'block'; + var points = this.points; + var otherPoints = p.points; - textSpan.appendChild(textNode); - svgDocument.appendChild(textElement); + var numPoints = points.length; + if (otherPoints.length !== numPoints) return false; // if the two polylines have different number of points, they cannot be equal - if (!opt.svgDocument) { + for (var i = 0; i < numPoints; i++) { - document.body.appendChild(svgDocument); + var point = points[i]; + var otherPoint = p.points[i]; + + // as soon as an inequality is found in points, return false + if (!point.equals(otherPoint)) return false; } - var words = text.split(' '); - var full = []; - var lines = []; - var p; - var lineHeight; + // if no inequality found in points, return true + return true; + }, - for (var i = 0, l = 0, len = words.length; i < len; i++) { + isDifferentiable: function() { - var word = words[i]; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return false; - textNode.data = lines[l] ? lines[l] + ' ' + word : word; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { - if (textSpan.getComputedTextLength() <= width) { + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); - // the current line fits - lines[l] = textNode.data; + // as soon as a differentiable line is found between two points, return true + if (line.isDifferentiable()) return true; + } - if (p) { - // We were partitioning. Put rest of the word onto next line - full[l++] = true; + // if no differentiable line is found between pairs of points, return false + return false; + }, - // cancel partitioning - p = 0; - } + length: function() { - } else { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return 0; // if points array is empty - if (!lines[l] || p) { + var length = 0; + var n = numPoints - 1; + for (var i = 0; i < n; i++) { + length += points[i].distance(points[i + 1]); + } - var partition = !!p; + return length; + }, - p = word.length - 1; + pointAt: function(ratio) { - if (partition || !p) { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point - // word has only one character. - if (!p) { + if (ratio <= 0) return points[0].clone(); + if (ratio >= 1) return points[numPoints - 1].clone(); - if (!lines[l]) { + var polylineLength = this.length(); + var length = polylineLength * ratio; - // we won't fit this text within our rect - lines = []; + return this.pointAtLength(length); + }, - break; - } + pointAtLength: function(length) { - // partitioning didn't help on the non-empty line - // try again, but this time start with a new line + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return points[0].clone(); // if there is only one point - // cancel partitions created - words.splice(i, 2, word + words[i + 1]); + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // adjust word length - len--; + var l = 0; + var n = numPoints - 1; + for (var i = (fromStart ? 0 : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - full[l++] = true; - i--; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); - continue; - } + if (length <= (l + d)) { + return line.pointAtLength((fromStart ? 1 : -1) * (length - l)); + } - // move last letter to the beginning of the next word - words[i] = word.substring(0, p); - words[i + 1] = word.substring(p) + words[i + 1]; + l += d; + } - } else { + // if length requested is higher than the length of the polyline, return last endpoint + var lastPoint = (fromStart ? points[numPoints - 1] : points[0]); + return lastPoint.clone(); + }, - // We initiate partitioning - // split the long word into two words - words.splice(i, 1, word.substring(0, p), word.substring(p)); + scale: function(sx, sy, origin) { - // adjust words length - len++; + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty - if (l && !full[l - 1]) { - // if the previous line is not full, try to fit max part of - // the current word there - l--; - } - } + for (var i = 0; i < numPoints; i++) { + points[i].scale(sx, sy, origin); + } - i--; + return this; + }, - continue; - } + tangentAt: function(ratio) { - l++; - i--; - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point - // if size.height is defined we have to check whether the height of the entire - // text exceeds the rect height - if (height !== undefined) { + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; - if (lineHeight === undefined) { + var polylineLength = this.length(); + var length = polylineLength * ratio; - var heightValue; + return this.tangentAtLength(length); + }, - // use the same defaults as in V.prototype.text - if (styles.lineHeight === 'auto') { - heightValue = { value: 1.5, unit: 'em' }; - } else { - heightValue = joint.util.parseCssNumeric(styles.lineHeight, ['em']) || { value: 1, unit: 'em' }; - } + tangentAtLength: function(length) { - lineHeight = heightValue.value; - if (heightValue.unit === 'em' ) { - lineHeight *= textElement.getBBox().height; - } - } + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty + if (numPoints === 1) return null; // if there is only one point - if (lineHeight * lines.length > height) { + var fromStart = true; + if (length < 0) { + fromStart = false; // negative lengths mean start calculation from end point + length = -length; // absolute value + } - // remove overflowing lines - lines.splice(Math.floor(height / lineHeight)); + var lastValidLine; // differentiable (with a tangent) + var l = 0; // length so far + var n = numPoints - 1; + for (var i = (fromStart ? (0) : (n - 1)); (fromStart ? (i < n) : (i >= 0)); (fromStart ? (i++) : (i--))) { - break; + var a = points[i]; + var b = points[i + 1]; + var line = new Line(a, b); + var d = a.distance(b); + + if (line.isDifferentiable()) { // has a tangent line (line length is not 0) + if (length <= (l + d)) { + return line.tangentAtLength((fromStart ? 1 : -1) * (length - l)); } + + lastValidLine = line; } + + l += d; } - if (opt.svgDocument) { + // if length requested is higher than the length of the polyline, return last valid endpoint + if (lastValidLine) { + var ratio = (fromStart ? 1 : 0); + return lastValidLine.tangentAt(ratio); + } - // svg document was provided, remove the text element only - svgDocument.removeChild(textElement); + // if no valid line, return null + return null; + }, - } else { + intersectionWithLine: function(l) { + var line = new Line(l); + var intersections = []; + var points = this.points; + for (var i = 0, n = points.length - 1; i < n; i++) { + var a = points[i]; + var b = points[i+1]; + var l2 = new Line(a, b); + var int = line.intersectionWithLine(l2); + if (int) intersections.push(int[0]); + } + return (intersections.length > 0) ? intersections : null; + }, - // clean svg document - document.body.removeChild(svgDocument); + translate: function(tx, ty) { + + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return this; // if points array is empty + + for (var i = 0; i < numPoints; i++) { + points[i].translate(tx, ty); } - return lines.join('\n'); + return this; }, - imageToDataUri: function(url, callback) { + // Return svgString that can be used to recreate this line. + serialize: function() { - if (!url || url.substr(0, 'data:'.length) === 'data:') { - // No need to convert to data uri if it is already in data uri. + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return ''; // if points array is empty - // This not only convenient but desired. For example, - // IE throws a security error if data:image/svg+xml is used to render - // an image to the canvas and an attempt is made to read out data uri. - // Now if our image is already in data uri, there is no need to render it to the canvas - // and so we can bypass this error. + var output = ''; + for (var i = 0; i < numPoints; i++) { - // Keep the async nature of the function. - return setTimeout(function() { - callback(null, url); - }, 0); + var point = points[i]; + output += point.x + ',' + point.y + ' '; } - // chrome IE10 IE11 - var modernHandler = function(xhr, callback) { + return output.trim(); + }, - if (xhr.status === 200) { + toString: function() { - var reader = new FileReader(); + return this.points + ''; + } + }; - reader.onload = function(evt) { - var dataUri = evt.target.result; - callback(null, dataUri); - }; + Object.defineProperty(Polyline.prototype, 'start', { + // Getter for the first point of the polyline. - reader.onerror = function() { - callback(new Error('Failed to load image ' + url)); - }; + configurable: true, - reader.readAsDataURL(xhr.response); - } else { - callback(new Error('Failed to load image ' + url)); - } + enumerable: true, - }; + get: function() { - var legacyHandler = function(xhr, callback) { + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - var Uint8ToString = function(u8a) { - var CHUNK_SZ = 0x8000; - var c = []; - for (var i = 0; i < u8a.length; i += CHUNK_SZ) { - c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); - } - return c.join(''); - }; + return this.points[0]; + }, + }); + Object.defineProperty(Polyline.prototype, 'end', { + // Getter for the last point of the polyline. - if (xhr.status === 200) { + configurable: true, - var bytes = new Uint8Array(xhr.response); + enumerable: true, - var suffix = (url.split('.').pop()) || 'png'; - var map = { - 'svg': 'svg+xml' - }; - var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,'; - var b64encoded = meta + btoa(Uint8ToString(bytes)); - callback(null, b64encoded); - } else { - callback(new Error('Failed to load image ' + url)); - } - }; + get: function() { - var xhr = new XMLHttpRequest(); + var points = this.points; + var numPoints = points.length; + if (numPoints === 0) return null; // if points array is empty - xhr.open('GET', url, true); - xhr.addEventListener('error', function() { - callback(new Error('Failed to load image ' + url)); - }); + return this.points[numPoints - 1]; + }, + }); - xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer'; + g.scale = { - xhr.addEventListener('load', function() { - if (window.FileReader) { - modernHandler(xhr, callback); - } else { - legacyHandler(xhr, callback); - } - }); - - xhr.send(); - }, + // Return the `value` from the `domain` interval scaled to the `range` interval. + linear: function(domain, range, value) { - getElementBBox: function(el) { + var domainSpan = domain[1] - domain[0]; + var rangeSpan = range[1] - range[0]; + return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0; + } + }; - var $el = $(el); - if ($el.length === 0) { - throw new Error('Element not found') - } + var normalizeAngle = g.normalizeAngle = function(angle) { - var element = $el[0]; - var doc = element.ownerDocument; - var clientBBox = element.getBoundingClientRect(); + return (angle % 360) + (angle < 0 ? 360 : 0); + }; - var strokeWidthX = 0; - var strokeWidthY = 0; + var snapToGrid = g.snapToGrid = function(value, gridSize) { - // Firefox correction - if (element.ownerSVGElement) { + return gridSize * round(value / gridSize); + }; - var vel = V(element); - var bbox = vel.getBBox({ target: vel.svg() }); + var toDeg = g.toDeg = function(rad) { - // if FF getBoundingClientRect includes stroke-width, getBBox doesn't. - // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value. - strokeWidthX = (clientBBox.width - bbox.width); - strokeWidthY = (clientBBox.height - bbox.height); - } + return (180 * rad / PI) % 360; + }; - return { - x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2, - y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2, - width: clientBBox.width - strokeWidthX, - height: clientBBox.height - strokeWidthY - }; - }, + var toRad = g.toRad = function(deg, over360) { + over360 = over360 || false; + deg = over360 ? deg : (deg % 360); + return deg * PI / 180; + }; - // Highly inspired by the jquery.sortElements plugin by Padolsey. - // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. - sortElements: function(elements, comparator) { + // For backwards compatibility: + g.ellipse = g.Ellipse; + g.line = g.Line; + g.point = g.Point; + g.rect = g.Rect; - var $elements = $(elements); - var placements = $elements.map(function() { + // Local helper function. + // Use an array of arguments to call a constructor (function called with `new`). + // Adapted from https://stackoverflow.com/a/8843181/2263595 + // It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). + // - If that is the case, use `new constructor(arg1, arg2)`, for example. + // It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. + // - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. + function applyToNew(constructor, argsArray) { + // The `new` keyword can only be applied to functions that take a limited number of arguments. + // - We can fake that with .bind(). + // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. + // - So `new (constructor.bind(thisArg, arg1, arg2...))` + // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. + // We need to pass in a variable number of arguments to the bind() call. + // - We can use .apply(). + // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, arg2...]))` + // - `thisArg` can still be anything because `new` overwrites it. + // Finally, to make sure that constructor.bind overwriting is not a problem, we switch to `Function.prototype.bind`. + // - So, the final version is `new (Function.prototype.bind.apply(constructor, [thisArg, arg1, arg2...]))` + + // The function expects `argsArray[0]` to be `thisArg`. + // - This means that whatever is sent as the first element will be ignored. + // - The constructor will only see arguments starting from argsArray[1]. + // - So, a new dummy element is inserted at the start of the array. + argsArray.unshift(null); + + return new (Function.prototype.bind.apply(constructor, argsArray)); + } - var sortElement = this; - var parentNode = sortElement.parentNode; - // Since the element itself will change position, we have - // to have some way of storing it's original position in - // the DOM. The easiest way is to have a 'flag' node: - var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); + // Local helper function. + // Add properties from arguments on top of properties from `obj`. + // This allows for rudimentary inheritance. + // - The `obj` argument acts as parent. + // - This function creates a new object that inherits all `obj` properties and adds/replaces those that are present in arguments. + // - A high-level example: calling `extend(Vehicle, Car)` would be akin to declaring `class Car extends Vehicle`. + function extend(obj) { + // In JavaScript, the combination of a constructor function (e.g. `g.Line = function(...) {...}`) and prototype (e.g. `g.Line.prototype = {...}) is akin to a C++ class. + // - When inheritance is not necessary, we can leave it at that. (This would be akin to calling extend with only `obj`.) + // - But, what if we wanted the `g.Line` quasiclass to inherit from another quasiclass (let's call it `g.GeometryObject`) in JavaScript? + // - First, realize that both of those quasiclasses would still have their own separate constructor function. + // - So what we are actually saying is that we want the `g.Line` prototype to inherit from `g.GeometryObject` prototype. + // - This method provides a way to do exactly that. + // - It copies parent prototype's properties, then adds extra ones from child prototype/overrides parent prototype properties with child prototype properties. + // - Therefore, to continue with the example above: + // - `g.Line.prototype = extend(g.GeometryObject.prototype, linePrototype)` + // - Where `linePrototype` is a properties object that looks just like `g.Line.prototype` does right now. + // - Then, `g.Line` would allow the programmer to access to all methods currently in `g.Line.Prototype`, plus any non-overriden methods from `g.GeometryObject.prototype`. + // - In that aspect, `g.GeometryObject` would then act like the parent of `g.Line`. + // - Multiple inheritance is also possible, if multiple arguments are provided. + // - What if we wanted to add another level of abstraction between `g.GeometryObject` and `g.Line` (let's call it `g.LinearObject`)? + // - `g.Line.prototype = extend(g.GeometryObject.prototype, g.LinearObject.prototype, linePrototype)` + // - The ancestors are applied in order of appearance. + // - That means that `g.Line` would have inherited from `g.LinearObject` that would have inherited from `g.GeometryObject`. + // - Any number of ancestors may be provided. + // - Note that neither `obj` nor any of the arguments need to actually be prototypes of any JavaScript quasiclass, that was just a simplified explanation. + // - We can create a new object composed from the properties of any number of other objects (since they do not have a constructor, we can think of those as interfaces). + // - `extend({ a: 1, b: 2 }, { b: 10, c: 20 }, { c: 100, d: 200 })` gives `{ a: 1, b: 10, c: 100, d: 200 }`. + // - Basically, with this function, we can emulate the `extends` keyword as well as the `implements` keyword. + // - Therefore, both of the following are valid: + // - `Lineto.prototype = extend(Line.prototype, segmentPrototype, linetoPrototype)` + // - `Moveto.prototype = extend(segmentPrototype, movetoPrototype)` - return function() { + var i; + var n; - if (parentNode === this) { - throw new Error('You can\'t sort elements if any one is a descendant of another.'); - } + var args = []; + n = arguments.length; + for (i = 1; i < n; i++) { // skip over obj + args.push(arguments[i]); + } - // Insert before flag: - parentNode.insertBefore(this, nextSibling); - // Remove flag: - parentNode.removeChild(nextSibling); - }; - }); + if (!obj) throw new Error('Missing a parent object.'); + var child = Object.create(obj); - return Array.prototype.sort.call($elements, comparator).each(function(i) { - placements[i].call(this); - }); - }, + n = args.length; + for (i = 0; i < n; i++) { - // Sets attributes on the given element and its descendants based on the selector. - // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} - setAttributesBySelector: function(element, attrs) { + var src = args[i]; - var $element = $(element); + var inheritedProperty; + var key; + for (key in src) { - joint.util.forIn(attrs, function(attrs, selector) { - var $elements = $element.find(selector).addBack().filter(selector); - // Make a special case for setting classes. - // We do not want to overwrite any existing class. - if (joint.util.has(attrs, 'class')) { - $elements.addClass(attrs['class']); - attrs = joint.util.omit(attrs, 'class'); + if (src.hasOwnProperty(key)) { + delete child[key]; // delete property inherited from parent + inheritedProperty = Object.getOwnPropertyDescriptor(src, key); // get new definition of property from src + Object.defineProperty(child, key, inheritedProperty); // re-add property with new definition (includes getter/setter methods) } - $elements.attr(attrs); - }); - }, + } + } - // Return a new object with all for sides (top, bottom, left and right) in it. - // Value of each side is taken from the given argument (either number or object). - // Default value for a side is 0. - // Examples: - // joint.util.normalizeSides(5) --> { top: 5, left: 5, right: 5, bottom: 5 } - // joint.util.normalizeSides({ left: 5 }) --> { top: 0, left: 5, right: 0, bottom: 0 } - normalizeSides: function(box) { + return child; + } - if (Object(box) !== box) { - box = box || 0; - return { top: box, bottom: box, left: box, right: box }; - } + // Path segment interface: + var segmentPrototype = { - return { - top: box.top || 0, - bottom: box.bottom || 0, - left: box.left || 0, - right: box.right || 0 - }; + // Redirect calls to closestPointNormalizedLength() function if closestPointT() is not defined for segment. + closestPointT: function(p) { + + if (this.closestPointNormalizedLength) return this.closestPointNormalizedLength(p); + + throw new Error('Neither closestPointT() nor closestPointNormalizedLength() function is implemented.'); }, - timing: { + isSegment: true, - linear: function(t) { - return t; - }, + isSubpathStart: false, // true for Moveto segments - quad: function(t) { - return t * t; - }, + isVisible: true, // false for Moveto segments - cubic: function(t) { - return t * t * t; - }, + nextSegment: null, // needed for subpath start segment updating - inout: function(t) { - if (t <= 0) return 0; - if (t >= 1) return 1; - var t2 = t * t; - var t3 = t2 * t; - return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); - }, + // Return a fraction of result of length() function if lengthAtT() is not defined for segment. + lengthAtT: function(t) { - exponential: function(t) { - return Math.pow(2, 10 * (t - 1)); - }, + if (t <= 0) return 0; - bounce: function(t) { - for (var a = 0, b = 1; 1; a += b, b /= 2) { - if (t >= (7 - 4 * a) / 11) { - var q = (11 - 6 * a - 11 * t) / 4; - return -q * q + b * b; - } - } - }, + var length = this.length(); - reverse: function(f) { - return function(t) { - return 1 - f(1 - t); - }; - }, + if (t >= 1) return length; - reflect: function(f) { - return function(t) { - return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); - }; - }, + return length * t; + }, - clamp: function(f, n, x) { - n = n || 0; - x = x || 1; - return function(t) { - var r = f(t); - return r < n ? n : r > x ? x : r; - }; - }, + // Redirect calls to pointAt() function if pointAtT() is not defined for segment. + pointAtT: function(t) { - back: function(s) { - if (!s) s = 1.70158; - return function(t) { - return t * t * ((s + 1) * t - s); - }; - }, + if (this.pointAt) return this.pointAt(t); - elastic: function(x) { - if (!x) x = 1.5; - return function(t) { - return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); - }; - } + throw new Error('Neither pointAtT() nor pointAt() function is implemented.'); }, - interpolate: { + previousSegment: null, // needed to get segment start property - number: function(a, b) { - var d = b - a; - return function(t) { return a + d * t; }; - }, + subpathStartSegment: null, // needed to get closepath segment end property - object: function(a, b) { - var s = Object.keys(a); - return function(t) { - var i, p; - var r = {}; - for (i = s.length - 1; i != -1; i--) { - p = s[i]; - r[p] = a[p] + (b[p] - a[p]) * t; - } - return r; - }; - }, + // Redirect calls to tangentAt() function if tangentAtT() is not defined for segment. + tangentAtT: function(t) { - hexColor: function(a, b) { + if (this.tangentAt) return this.tangentAt(t); - var ca = parseInt(a.slice(1), 16); - var cb = parseInt(b.slice(1), 16); - var ra = ca & 0x0000ff; - var rd = (cb & 0x0000ff) - ra; - var ga = ca & 0x00ff00; - var gd = (cb & 0x00ff00) - ga; - var ba = ca & 0xff0000; - var bd = (cb & 0xff0000) - ba; + throw new Error('Neither tangentAtT() nor tangentAt() function is implemented.'); + }, - return function(t) { + // VIRTUAL PROPERTIES (must be overriden by actual Segment implementations): - var r = (ra + rd * t) & 0x000000ff; - var g = (ga + gd * t) & 0x0000ff00; - var b = (ba + bd * t) & 0x00ff0000; + // type - return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); - }; - }, + // start // getter, always throws error for Moveto - unit: function(a, b) { + // end // usually directly assigned, getter for Closepath - var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; - var ma = r.exec(a); - var mb = r.exec(b); - var p = mb[1].indexOf('.'); - var f = p > 0 ? mb[1].length - p - 1 : 0; - a = +ma[1]; - var d = +mb[1] - a; - var u = ma[2]; + bbox: function() { - return function(t) { - return (a + d * t).toFixed(f) + u; - }; - } + throw new Error('Declaration missing for virtual function.'); }, - // SVG filters. - filter: { + clone: function() { - // `color` ... outline color - // `width`... outline width - // `opacity` ... outline opacity - // `margin` ... gap between outline and the element - outline: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var tpl = ''; + closestPoint: function() { - var margin = Number.isFinite(args.margin) ? args.margin : 2; - var width = Number.isFinite(args.width) ? args.width : 1; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template(tpl)({ - color: args.color || 'blue', - opacity: Number.isFinite(args.opacity) ? args.opacity : 1, - outerRadius: margin + width, - innerRadius: margin - }); - }, + closestPointLength: function() { - // `color` ... color - // `width`... width - // `blur` ... blur - // `opacity` ... opacity - highlight: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var tpl = ''; + closestPointNormalizedLength: function() { - return joint.util.template(tpl)({ - color: args.color || 'red', - width: Number.isFinite(args.width) ? args.width : 1, - blur: Number.isFinite(args.blur) ? args.blur : 0, - opacity: Number.isFinite(args.opacity) ? args.opacity : 1 - }); - }, - - // `x` ... horizontal blur - // `y` ... vertical blur (optional) - blur: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var x = Number.isFinite(args.x) ? args.x : 2; + closestPointTangent: function() { - return joint.util.template('')({ - stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `dx` ... horizontal shift - // `dy` ... vertical shift - // `blur` ... blur - // `color` ... color - // `opacity` ... opacity - dropShadow: function(args) { + equals: function() { - var tpl = 'SVGFEDropShadowElement' in window - ? '' - : ''; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template(tpl)({ - dx: args.dx || 0, - dy: args.dy || 0, - opacity: Number.isFinite(args.opacity) ? args.opacity : 1, - color: args.color || 'black', - blur: Number.isFinite(args.blur) ? args.blur : 4 - }); - }, + getSubdivisions: function() { - // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. - grayscale: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + isDifferentiable: function() { - return joint.util.template('')({ - a: 0.2126 + 0.7874 * (1 - amount), - b: 0.7152 - 0.7152 * (1 - amount), - c: 0.0722 - 0.0722 * (1 - amount), - d: 0.2126 - 0.2126 * (1 - amount), - e: 0.7152 + 0.2848 * (1 - amount), - f: 0.0722 - 0.0722 * (1 - amount), - g: 0.2126 - 0.2126 * (1 - amount), - h: 0.0722 + 0.9278 * (1 - amount) - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. - sepia: function(args) { + length: function() { - var amount = Number.isFinite(args.amount) ? args.amount : 1; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template('')({ - a: 0.393 + 0.607 * (1 - amount), - b: 0.769 - 0.769 * (1 - amount), - c: 0.189 - 0.189 * (1 - amount), - d: 0.349 - 0.349 * (1 - amount), - e: 0.686 + 0.314 * (1 - amount), - f: 0.168 - 0.168 * (1 - amount), - g: 0.272 - 0.272 * (1 - amount), - h: 0.534 - 0.534 * (1 - amount), - i: 0.131 + 0.869 * (1 - amount) - }); - }, + pointAt: function() { - // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. - saturate: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + pointAtLength: function() { - return joint.util.template('')({ - amount: 1 - amount - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `angle` ... the number of degrees around the color circle the input samples will be adjusted. - hueRotate: function(args) { + scale: function() { - return joint.util.template('')({ - angle: args.angle || 0 - }); - }, + throw new Error('Declaration missing for virtual function.'); + }, - // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. - invert: function(args) { + tangentAt: function() { - var amount = Number.isFinite(args.amount) ? args.amount : 1; + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template('')({ - amount: amount, - amount2: 1 - amount - }); - }, + tangentAtLength: function() { - // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. - brightness: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - return joint.util.template('')({ - amount: Number.isFinite(args.amount) ? args.amount : 1 - }); - }, + translate: function() { - // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. - contrast: function(args) { + throw new Error('Declaration missing for virtual function.'); + }, - var amount = Number.isFinite(args.amount) ? args.amount : 1; + serialize: function() { - return joint.util.template('')({ - amount: amount, - amount2: .5 - amount / 2 - }); - } + throw new Error('Declaration missing for virtual function.'); }, - format: { + toString: function() { - // Formatting numbers via the Python Format Specification Mini-language. - // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. - // Heavilly inspired by the D3.js library implementation. - number: function(specifier, value, locale) { + throw new Error('Declaration missing for virtual function.'); + } + }; - locale = locale || { + // Path segment implementations: + var Lineto = function() { - currency: ['$', ''], - decimal: '.', - thousands: ',', - grouping: [3] - }; + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. - // [[fill]align][sign][symbol][0][width][,][.precision][type] - var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + if (!(this instanceof Lineto)) { // switching context of `this` to Lineto when called without `new` + return applyToNew(Lineto, args); + } - var match = re.exec(specifier); - var fill = match[1] || ' '; - var align = match[2] || '>'; - var sign = match[3] || ''; - var symbol = match[4] || ''; - var zfill = match[5]; - var width = +match[6]; - var comma = match[7]; - var precision = match[8]; - var type = match[9]; - var scale = 1; - var prefix = ''; - var suffix = ''; - var integer = false; + if (n === 0) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (none provided).'); + } - if (precision) precision = +precision.substring(1); + var outputArray; - if (zfill || fill === '0' && align === '=') { - zfill = fill = '0'; - align = '='; - if (comma) width -= Math.floor((width - 1) / 4); - } + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; - switch (type) { - case 'n': - comma = true; type = 'g'; - break; - case '%': - scale = 100; suffix = '%'; type = 'f'; - break; - case 'p': - scale = 100; suffix = '%'; type = 'r'; - break; - case 'b': - case 'o': - case 'x': - case 'X': - if (symbol === '#') prefix = '0' + type.toLowerCase(); - break; - case 'c': - case 'd': - integer = true; precision = 0; - break; - case 's': - scale = -1; type = 'r'; - break; - } + } else if (n < 2) { + throw new Error('Lineto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); - if (symbol === '$') { - prefix = locale.currency[0]; - suffix = locale.currency[1]; + } else { // this is a poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + outputArray.push(applyToNew(Lineto, segmentCoords)); } + return outputArray; + } - // If no precision is specified for `'r'`, fallback to general notation. - if (type == 'r' && !precision) type = 'g'; + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; - // Ensure that the requested precision is in the supported range. - if (precision != null) { - if (type == 'g') precision = Math.max(1, Math.min(21, precision)); - else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); + } else { // this is a poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { + + segmentPoint = args[i]; + outputArray.push(new Lineto(segmentPoint)); } + return outputArray; + } + } + }; - var zcomma = zfill && comma; + var linetoPrototype = { - // Return the empty string for floats formatted as ints. - if (integer && (value % 1)) return ''; + clone: function() { - // Convert negative to positive, and record the sign prefix. - var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; + return new Lineto(this.end); + }, - var fullSuffix = suffix; + getSubdivisions: function() { - // Apply the scale, computing it from the value's exponent for si format. - // Preserve the existing suffix, if any, such as the currency symbol. - if (scale < 0) { - var unit = this.prefix(value, precision); - value = unit.scale(value); - fullSuffix = unit.symbol + suffix; - } else { - value *= scale; - } + return []; + }, - // Convert to the desired precision. - value = this.convert(type, value, precision); + isDifferentiable: function() { - // Break the value into the integer part (before) and decimal part (after). - var i = value.lastIndexOf('.'); - var before = i < 0 ? value : value.substring(0, i); - var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); + if (!this.previousSegment) return false; - function formatGroup(value) { + return !this.start.equals(this.end); + }, - var i = value.length; - var t = []; - var j = 0; - var g = locale.grouping[0]; - while (i > 0 && g > 0) { - t.push(value.substring(i -= g, i + g)); - g = locale.grouping[j = (j + 1) % locale.grouping.length]; - } - return t.reverse().join(locale.thousands); - } + scale: function(sx, sy, origin) { - // If the fill character is not `'0'`, grouping is applied before padding. - if (!zfill && comma && locale.grouping) { + this.end.scale(sx, sy, origin); + return this; + }, - before = formatGroup(before); - } + translate: function(tx, ty) { - var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); - var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; + this.end.translate(tx, ty); + return this; + }, - // If the fill character is `'0'`, grouping is applied after padding. - if (zcomma) before = formatGroup(padding + before); + type: 'L', - // Apply prefix. - negative += prefix; + serialize: function() { - // Rejoin integer and decimal parts. - value = before + after; + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, - return (align === '<' ? negative + value + padding - : align === '>' ? padding + negative + value - : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) - : negative + (zcomma ? value : padding + value)) + fullSuffix; - }, + toString: function() { - // Formatting string via the Python Format string. - // See https://docs.python.org/2/library/string.html#format-string-syntax) - string: function(formatString, value) { + return this.type + ' ' + this.start + ' ' + this.end; + } + }; - var fieldDelimiterIndex; - var fieldDelimiter = '{'; - var endPlaceholder = false; - var formattedStringArray = []; + Object.defineProperty(linetoPrototype, 'start', { + // get a reference to the end point of previous segment - while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { + configurable: true, - var pieceFormatedString, formatSpec, fieldName; + enumerable: true, - pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); + get: function() { - if (endPlaceholder) { - formatSpec = pieceFormatedString.split(':'); - fieldName = formatSpec.shift().split('.'); - pieceFormatedString = value; + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - for (var i = 0; i < fieldName.length; i++) - pieceFormatedString = pieceFormatedString[fieldName[i]]; + return this.previousSegment.end; + } + }); - if (formatSpec.length) - pieceFormatedString = this.number(formatSpec, pieceFormatedString); - } + Lineto.prototype = extend(segmentPrototype, Line.prototype, linetoPrototype); - formattedStringArray.push(pieceFormatedString); + var Curveto = function() { - formatString = formatString.slice(fieldDelimiterIndex + 1); - fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; - } - formattedStringArray.push(formatString); + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - return formattedStringArray.join(''); - }, + if (!(this instanceof Curveto)) { // switching context of `this` to Curveto when called without `new` + return applyToNew(Curveto, args); + } - convert: function(type, value, precision) { + if (n === 0) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (none provided).'); + } - switch (type) { - case 'b': return value.toString(2); - case 'c': return String.fromCharCode(value); - case 'o': return value.toString(8); - case 'x': return value.toString(16); - case 'X': return value.toString(16).toUpperCase(); - case 'g': return value.toPrecision(precision); - case 'e': return value.toExponential(precision); - case 'f': return value.toFixed(precision); - case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); - default: return value + ''; - } - }, + var outputArray; - round: function(value, precision) { + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 6) { + this.controlPoint1 = new Point(+args[0], +args[1]); + this.controlPoint2 = new Point(+args[2], +args[3]); + this.end = new Point(+args[4], +args[5]); + return this; - return precision - ? Math.round(value * (precision = Math.pow(10, precision))) / precision - : Math.round(value); - }, + } else if (n < 6) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' coordinates provided).'); - precision: function(value, precision) { + } else { // this is a poly-bezier segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 6) { // coords come in groups of six - return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); - }, + segmentCoords = args.slice(i, i + 6); // will send fewer than six coords if args.length not divisible by 6 + outputArray.push(applyToNew(Curveto, segmentCoords)); + } + return outputArray; + } - prefix: function(value, precision) { + } else { // points provided + if (n === 3) { + this.controlPoint1 = new Point(args[0]); + this.controlPoint2 = new Point(args[1]); + this.end = new Point(args[2]); + return this; - var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) { - var k = Math.pow(10, Math.abs(8 - i) * 3); - return { - scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, - symbol: d - }; - }); + } else if (n < 3) { + throw new Error('Curveto constructor expects 3 points or 6 coordinates (' + n + ' points provided).'); - var i = 0; - if (value) { - if (value < 0) value *= -1; - if (precision) value = this.round(value, this.precision(value, precision)); - i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); - i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); + } else { // this is a poly-bezier segment + var segmentPoints; + outputArray = []; + for (i = 0; i < n; i += 3) { // points come in groups of three + + segmentPoints = args.slice(i, i + 3); // will send fewer than three points if args.length is not divisible by 3 + outputArray.push(applyToNew(Curveto, segmentPoints)); } - return prefixes[8 + i / 3]; + return outputArray; } - }, + } + }; - /* - Pre-compile the HTML to be used as a template. - */ - template: function(html) { + var curvetoPrototype = { - /* - Must support the variation in templating syntax found here: - https://lodash.com/docs#template - */ - var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; + clone: function() { - return function(data) { + return new Curveto(this.controlPoint1, this.controlPoint2, this.end); + }, - data = data || {}; + isDifferentiable: function() { - return html.replace(regex, function(match) { + if (!this.previousSegment) return false; - var args = Array.from(arguments); - var attr = args.slice(1, 4).find(function(_attr) { - return !!_attr; - }); + var start = this.start; + var control1 = this.controlPoint1; + var control2 = this.controlPoint2; + var end = this.end; - var attrArray = attr.split('.'); - var value = data[attrArray.shift()]; + return !(start.equals(control1) && control1.equals(control2) && control2.equals(end)); + }, - while (value !== undefined && attrArray.length) { - value = value[attrArray.shift()]; - } + scale: function(sx, sy, origin) { - return value !== undefined ? value : ''; - }); - }; + this.controlPoint1.scale(sx, sy, origin); + this.controlPoint2.scale(sx, sy, origin); + this.end.scale(sx, sy, origin); + return this; }, - /** - * @param {Element=} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default. - */ - toggleFullScreen: function(el) { + translate: function(tx, ty) { - var topDocument = window.top.document; - el = el || topDocument.body; + this.controlPoint1.translate(tx, ty); + this.controlPoint2.translate(tx, ty); + this.end.translate(tx, ty); + return this; + }, - function prefixedResult(el, prop) { + type: 'C', - var prefixes = ['webkit', 'moz', 'ms', 'o', '']; - for (var i = 0; i < prefixes.length; i++) { - var prefix = prefixes[i]; - var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); - if (el[propName] !== undefined) { - return joint.util.isFunction(el[propName]) ? el[propName]() : el[propName]; - } - } - } + serialize: function() { - if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) { - prefixedResult(topDocument, 'ExitFullscreen') || // Spec. - prefixedResult(topDocument, 'CancelFullScreen'); // Firefox - } else { - prefixedResult(el, 'RequestFullscreen') || // Spec. - prefixedResult(el, 'RequestFullScreen'); // Firefox - } + var c1 = this.controlPoint1; + var c2 = this.controlPoint2; + var end = this.end; + return this.type + ' ' + c1.x + ' ' + c1.y + ' ' + c2.x + ' ' + c2.y + ' ' + end.x + ' ' + end.y; }, - addClassNamePrefix: function(className) { + toString: function() { - if (!className) return className; + return this.type + ' ' + this.start + ' ' + this.controlPoint1 + ' ' + this.controlPoint2 + ' ' + this.end; + } + }; - return className.toString().split(' ').map(function(_className) { + Object.defineProperty(curvetoPrototype, 'start', { + // get a reference to the end point of previous segment - if (_className.substr(0, joint.config.classNamePrefix.length) !== joint.config.classNamePrefix) { - _className = joint.config.classNamePrefix + _className; - } + configurable: true, - return _className; + enumerable: true, - }).join(' '); - }, + get: function() { - removeClassNamePrefix: function(className) { + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - if (!className) return className; + return this.previousSegment.end; + } + }); - return className.toString().split(' ').map(function(_className) { + Curveto.prototype = extend(segmentPrototype, Curve.prototype, curvetoPrototype); - if (_className.substr(0, joint.config.classNamePrefix.length) === joint.config.classNamePrefix) { - _className = _className.substr(joint.config.classNamePrefix.length); - } + var Moveto = function() { - return _className; + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - }).join(' '); - }, + if (!(this instanceof Moveto)) { // switching context of `this` to Moveto when called without `new` + return applyToNew(Moveto, args); + } - wrapWith: function(object, methods, wrapper) { + if (n === 0) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (none provided).'); + } - if (joint.util.isString(wrapper)) { + var outputArray; - if (!joint.util.wrappers[wrapper]) { - throw new Error('Unknown wrapper: "' + wrapper + '"'); - } + if (typeof args[0] === 'string' || typeof args[0] === 'number') { // coordinates provided + if (n === 2) { + this.end = new Point(+args[0], +args[1]); + return this; - wrapper = joint.util.wrappers[wrapper]; - } + } else if (n < 2) { + throw new Error('Moveto constructor expects 1 point or 2 coordinates (' + n + ' coordinates provided).'); - if (!joint.util.isFunction(wrapper)) { - throw new Error('Wrapper must be a function.'); + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentCoords; + outputArray = []; + for (i = 0; i < n; i += 2) { // coords come in groups of two + + segmentCoords = args.slice(i, i + 2); // will send one coord if args.length not divisible by 2 + if (i === 0) outputArray.push(applyToNew(Moveto, segmentCoords)); + else outputArray.push(applyToNew(Lineto, segmentCoords)); + } + return outputArray; } - this.toArray(methods).forEach(function(method) { - object[method] = wrapper(object[method]); - }); - }, + } else { // points provided + if (n === 1) { + this.end = new Point(args[0]); + return this; - wrappers: { + } else { // this is a moveto-with-subsequent-poly-line segment + var segmentPoint; + outputArray = []; + for (i = 0; i < n; i += 1) { // points come one by one - /* - Prepares a function with the following usage: + segmentPoint = args[i]; + if (i === 0) outputArray.push(new Moveto(segmentPoint)); + else outputArray.push(new Lineto(segmentPoint)); + } + return outputArray; + } + } + }; - fn([cell, cell, cell], opt); - fn([cell, cell, cell]); - fn(cell, cell, cell, opt); - fn(cell, cell, cell); - fn(cell); - */ - cells: function(fn) { + var movetoPrototype = { - return function() { + bbox: function() { - var args = Array.from(arguments); - var n = args.length; - var cells = n > 0 && args[0] || []; - var opt = n > 1 && args[n - 1] || {}; + return null; + }, - if (!Array.isArray(cells)) { + clone: function() { - if (opt instanceof joint.dia.Cell) { - cells = args; - } else if (cells instanceof joint.dia.Cell) { - if (args.length > 1) { - args.pop(); - } - cells = args; - } - } + return new Moveto(this.end); + }, - if (opt instanceof joint.dia.Cell) { - opt = {}; - } + closestPoint: function() { - return fn.call(this, cells, opt); - }; - } + return this.end.clone(); }, - // lodash 3 vs 4 incompatible - sortedIndex: _.sortedIndexBy || _.sortedIndex, - uniq: _.uniqBy || _.uniq, - uniqueId: _.uniqueId, - sortBy: _.sortBy, - isFunction: _.isFunction, - result: _.result, - union: _.union, - invoke: _.invokeMap || _.invoke, - difference: _.difference, - intersection: _.intersection, - omit: _.omit, - pick: _.pick, - has: _.has, - bindAll: _.bindAll, - assign: _.assign, - defaults: _.defaults, - defaultsDeep: _.defaultsDeep, - isPlainObject: _.isPlainObject, - isEmpty: _.isEmpty, - isEqual: _.isEqual, - noop: function() {}, - cloneDeep: _.cloneDeep, - toArray: _.toArray, - flattenDeep: _.flattenDeep, - camelCase: _.camelCase, - groupBy: _.groupBy, - forIn: _.forIn, - without: _.without, - debounce: _.debounce, - clone: _.clone, - isBoolean: function(value) { - var toString = Object.prototype.toString; - return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]'); - }, + closestPointNormalizedLength: function() { - isObject: function(value) { - return !!value && (typeof value === 'object' || typeof value === 'function'); + return 0; }, - isNumber: function(value) { - var toString = Object.prototype.toString; - return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]'); + closestPointLength: function() { + + return 0; }, - isString: function(value) { - var toString = Object.prototype.toString; - return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]'); + closestPointT: function() { + + return 1; }, - merge: function() { - if (_.mergeWith) { - var args = Array.from(arguments); - var last = args[args.length - 1]; + closestPointTangent: function() { - var customizer = this.isFunction(last) ? last : this.noop; - args.push(function(a,b) { - var customResult = customizer(a, b); - if (customResult !== undefined) { - return customResult; - } + return null; + }, - if (Array.isArray(a) && !Array.isArray(b)) { - return b; - } - }); + equals: function(m) { - return _.mergeWith.apply(this, args) - } - return _.merge.apply(this, arguments); - } - } -}; + return this.end.equals(m.end); + }, + getSubdivisions: function() { -joint.mvc.View = Backbone.View.extend({ + return []; + }, - options: {}, - theme: null, - themeClassNamePrefix: joint.util.addClassNamePrefix('theme-'), - requireSetThemeOverride: false, - defaultTheme: joint.config.defaultTheme, + isDifferentiable: function() { - constructor: function(options) { + return false; + }, - this.requireSetThemeOverride = options && !!options.theme; - this.options = joint.util.assign({}, this.options, options); + isSubpathStart: true, - Backbone.View.call(this, options); - }, + isVisible: false, - initialize: function(options) { + length: function() { - joint.util.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); + return 0; + }, - joint.mvc.views[this.cid] = this; + lengthAtT: function() { - this.setTheme(this.options.theme || this.defaultTheme); - this.init(); - }, + return 0; + }, - // Override the Backbone `_ensureElement()` method in order to create an - // svg element (e.g., ``) node that wraps all the nodes of the Cell view. - // Expose class name setter as a separate method. - _ensureElement: function() { - if (!this.el) { - var tagName = joint.util.result(this, 'tagName'); - var attrs = joint.util.assign({}, joint.util.result(this, 'attributes')); - if (this.id) attrs.id = joint.util.result(this, 'id'); - this.setElement(this._createElement(tagName)); - this._setAttributes(attrs); - } else { - this.setElement(joint.util.result(this, 'el')); - } - this._ensureElClassName(); - }, + pointAt: function() { - _setAttributes: function(attrs) { - if (this.svgElement) { - this.vel.attr(attrs); - } else { - this.$el.attr(attrs); - } - }, + return this.end.clone(); + }, - _createElement: function(tagName) { - if (this.svgElement) { - return document.createElementNS(V.namespace.xmlns, tagName); - } else { - return document.createElement(tagName); - } - }, + pointAtLength: function() { - // Utilize an alternative DOM manipulation API by - // adding an element reference wrapped in Vectorizer. - _setElement: function(el) { - this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); - this.el = this.$el[0]; - if (this.svgElement) this.vel = V(this.el); - }, + return this.end.clone(); + }, - _ensureElClassName: function() { - var className = joint.util.result(this, 'className'); - var prefixedClassName = joint.util.addClassNamePrefix(className); - // Note: className removal here kept for backwards compatibility only - if (this.svgElement) { - this.vel.removeClass(className).addClass(prefixedClassName); - } else { - this.$el.removeClass(className).addClass(prefixedClassName); - } - }, + pointAtT: function() { - init: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + return this.end.clone(); + }, - onRender: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + scale: function(sx, sy, origin) { - setTheme: function(theme, opt) { + this.end.scale(sx, sy, origin); + return this; + }, - opt = opt || {}; + tangentAt: function() { - // Theme is already set, override is required, and override has not been set. - // Don't set the theme. - if (this.theme && this.requireSetThemeOverride && !opt.override) { - return this; - } + return null; + }, - this.removeThemeClassName(); - this.addThemeClassName(theme); - this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); - this.theme = theme; + tangentAtLength: function() { - return this; - }, + return null; + }, - addThemeClassName: function(theme) { + tangentAtT: function() { - theme = theme || this.theme; + return null; + }, - var className = this.themeClassNamePrefix + theme; + translate: function(tx, ty) { + + this.end.translate(tx, ty); + return this; + }, - this.$el.addClass(className); + type: 'M', - return this; - }, + serialize: function() { - removeThemeClassName: function(theme) { + var end = this.end; + return this.type + ' ' + end.x + ' ' + end.y; + }, - theme = theme || this.theme; + toString: function() { - var className = this.themeClassNamePrefix + theme; + return this.type + ' ' + this.end; + } + }; - this.$el.removeClass(className); + Object.defineProperty(movetoPrototype, 'start', { - return this; - }, + configurable: true, - onSetTheme: function(oldTheme, newTheme) { - // Intentionally empty. - // This method is meant to be overriden. - }, + enumerable: true, - remove: function() { + get: function() { - this.onRemove(); + throw new Error('Illegal access. Moveto segments should not need a start property.'); + } + }) - joint.mvc.views[this.cid] = null; + Moveto.prototype = extend(segmentPrototype, movetoPrototype); // does not inherit from any other geometry object - Backbone.View.prototype.remove.apply(this, arguments); + var Closepath = function() { - return this; - }, + var args = []; + var n = arguments.length; + for (var i = 0; i < n; i++) { + args.push(arguments[i]); + } - onRemove: function() { - // Intentionally empty. - // This method is meant to be overriden. - }, + if (!(this instanceof Closepath)) { // switching context of `this` to Closepath when called without `new` + return applyToNew(Closepath, args); + } - getEventNamespace: function() { - // Returns a per-session unique namespace - return '.joint-event-ns-' + this.cid; - } + if (n > 0) { + throw new Error('Closepath constructor expects no arguments.'); + } -}, { + return this; + }; - extend: function() { + var closepathPrototype = { - var args = Array.from(arguments); + clone: function() { - // Deep clone the prototype and static properties objects. - // This prevents unexpected behavior where some properties are overwritten outside of this function. - var protoProps = args[0] && joint.util.assign({}, args[0]) || {}; - var staticProps = args[1] && joint.util.assign({}, args[1]) || {}; + return new Closepath(); + }, - // Need the real render method so that we can wrap it and call it later. - var renderFn = protoProps.render || (this.prototype && this.prototype.render) || null; + getSubdivisions: function() { - /* - Wrap the real render method so that: - .. `onRender` is always called. - .. `this` is always returned. - */ - protoProps.render = function() { + return []; + }, - if (renderFn) { - // Call the original render method. - renderFn.apply(this, arguments); - } + isDifferentiable: function() { - // Should always call onRender() method. - this.onRender(); + if (!this.previousSegment || !this.subpathStartSegment) return false; + + return !this.start.equals(this.end); + }, + + scale: function() { - // Should always return itself. return this; - }; + }, - return Backbone.View.extend.call(this, protoProps, staticProps); - } -}); + translate: function() { + return this; + }, + type: 'Z', -joint.dia.GraphCells = Backbone.Collection.extend({ + serialize: function() { - cellNamespace: joint.shapes, + return this.type; + }, - initialize: function(models, opt) { + toString: function() { - // Set the optional namespace where all model classes are defined. - if (opt.cellNamespace) { - this.cellNamespace = opt.cellNamespace; + return this.type + ' ' + this.start + ' ' + this.end; } + }; - this.graph = opt.graph; - }, + Object.defineProperty(closepathPrototype, 'start', { + // get a reference to the end point of previous segment - model: function(attrs, options) { + configurable: true, - var collection = options.collection; - var namespace = collection.cellNamespace; + enumerable: true, - // Find the model class in the namespace or use the default one. - var ModelClass = (attrs.type === 'link') - ? joint.dia.Link - : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; + get: function() { - var cell = new ModelClass(attrs, options); - // Add a reference to the graph. It is necessary to do this here because this is the earliest place - // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. - cell.graph = collection.graph; + if (!this.previousSegment) throw new Error('Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)'); - return cell; - }, + return this.previousSegment.end; + } + }); - // `comparator` makes it easy to sort cells based on their `z` index. - comparator: function(model) { + Object.defineProperty(closepathPrototype, 'end', { + // get a reference to the end point of subpath start segment - return model.get('z') || 0; - } -}); + configurable: true, + enumerable: true, -joint.dia.Graph = Backbone.Model.extend({ + get: function() { - _batches: {}, + if (!this.subpathStartSegment) throw new Error('Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)'); - initialize: function(attrs, opt) { + return this.subpathStartSegment.end; + } + }) - opt = opt || {}; + Closepath.prototype = extend(segmentPrototype, Line.prototype, closepathPrototype); - // Passing `cellModel` function in the options object to graph allows for - // setting models based on attribute objects. This is especially handy - // when processing JSON graphs that are in a different than JointJS format. - var cells = new joint.dia.GraphCells([], { - model: opt.cellModel, - cellNamespace: opt.cellNamespace, - graph: this - }); - Backbone.Model.prototype.set.call(this, 'cells', cells); + var segmentTypes = Path.segmentTypes = { + L: Lineto, + C: Curveto, + M: Moveto, + Z: Closepath, + z: Closepath + }; - // Make all the events fired in the `cells` collection available. - // to the outside world. - cells.on('all', this.trigger, this); + Path.regexSupportedData = new RegExp('^[\\s\\d' + Object.keys(segmentTypes).join('') + ',.]*$'); - // Backbone automatically doesn't trigger re-sort if models attributes are changed later when - // they're already in the collection. Therefore, we're triggering sort manually here. - this.on('change:z', this._sortOnChangeZ, this); - this.on('batch:stop', this._onBatchStop, this); + Path.isDataSupported = function(d) { + if (typeof d !== 'string') return false; + return this.regexSupportedData.test(d); + } - // `joint.dia.Graph` keeps an internal data structure (an adjacency list) - // for fast graph queries. All changes that affect the structure of the graph - // must be reflected in the `al` object. This object provides fast answers to - // questions such as "what are the neighbours of this node" or "what - // are the sibling links of this link". + return g; - // Outgoing edges per node. Note that we use a hash-table for the list - // of outgoing edges for a faster lookup. - // [node ID] -> Object [edge] -> true - this._out = {}; - // Ingoing edges per node. - // [node ID] -> Object [edge] -> true - this._in = {}; - // `_nodes` is useful for quick lookup of all the elements in the graph, without - // having to go through the whole cells array. - // [node ID] -> true - this._nodes = {}; - // `_edges` is useful for quick lookup of all the links in the graph, without - // having to go through the whole cells array. - // [edge ID] -> true - this._edges = {}; +})(); - cells.on('add', this._restructureOnAdd, this); - cells.on('remove', this._restructureOnRemove, this); - cells.on('reset', this._restructureOnReset, this); - cells.on('change:source', this._restructureOnChangeSource, this); - cells.on('change:target', this._restructureOnChangeTarget, this); - cells.on('remove', this._removeCell, this); - }, +// Vectorizer. +// ----------- - _sortOnChangeZ: function() { +// A tiny library for making your life easier when dealing with SVG. +// The only Vectorizer dependency is the Geometry library. - if (!this.hasActiveBatch('to-front') && !this.hasActiveBatch('to-back')) { - this.get('cells').sort(); - } - }, +var V; +var Vectorizer; - _onBatchStop: function(data) { +V = Vectorizer = (function() { - var batchName = data && data.batchName; - if ((batchName === 'to-front' || batchName === 'to-back') && !this.hasActiveBatch(batchName)) { - this.get('cells').sort(); - } - }, + 'use strict'; - _restructureOnAdd: function(cell) { + var hasSvg = typeof window === 'object' && + !!( + window.SVGAngle || + document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') + ); - if (cell.isLink()) { - this._edges[cell.id] = true; - var source = cell.get('source'); - var target = cell.get('target'); - if (source.id) { - (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; - } - if (target.id) { - (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; - } - } else { - this._nodes[cell.id] = true; - } - }, + // SVG support is required. + if (!hasSvg) { - _restructureOnRemove: function(cell) { + // Return a function that throws an error when it is used. + return function() { + throw new Error('SVG is required to use Vectorizer.'); + }; + } - if (cell.isLink()) { - delete this._edges[cell.id]; - var source = cell.get('source'); - var target = cell.get('target'); - if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { - delete this._out[source.id][cell.id]; - } - if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { - delete this._in[target.id][cell.id]; - } - } else { - delete this._nodes[cell.id]; - } - }, + // 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', + xhtml: 'http://www.w3.org/1999/xhtml' + }; - _restructureOnReset: function(cells) { + var SVGversion = '1.1'; - // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. - cells = cells.models; + // Declare shorthands to the most used math functions. + var math = Math; + var PI = math.PI; + var atan2 = math.atan2; + var sqrt = math.sqrt; + var min = math.min; + var max = math.max; + var cos = math.cos; + var sin = math.sin; - this._out = {}; - this._in = {}; - this._nodes = {}; - this._edges = {}; + var V = function(el, attrs, children) { - cells.forEach(this._restructureOnAdd, this); - }, + // This allows using V() without the new keyword. + if (!(this instanceof V)) { + return V.apply(Object.create(V.prototype), arguments); + } - _restructureOnChangeSource: function(link) { + if (!el) return; - var prevSource = link.previous('source'); - if (prevSource.id && this._out[prevSource.id]) { - delete this._out[prevSource.id][link.id]; - } - var source = link.get('source'); - if (source.id) { - (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; + if (V.isV(el)) { + el = el.node; } - }, - _restructureOnChangeTarget: function(link) { + attrs = attrs || {}; - var prevTarget = link.previous('target'); - if (prevTarget.id && this._in[prevTarget.id]) { - delete this._in[prevTarget.id][link.id]; - } - var target = link.get('target'); - if (target.id) { - (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; - } - }, + if (V.isString(el)) { - // Return all outbound edges for the node. Return value is an object - // of the form: [edge] -> true - getOutboundEdges: function(node) { + if (el.toLowerCase() === 'svg') { - return (this._out && this._out[node]) || {}; - }, + // Create a new SVG canvas. + el = V.createSvgDocument(); - // Return all inbound edges for the node. Return value is an object - // of the form: [edge] -> true - getInboundEdges: function(node) { + } else if (el[0] === '<') { - return (this._in && this._in[node]) || {}; - }, + // Create element from an SVG string. + // Allows constructs of type: `document.appendChild(V('').node)`. - toJSON: function() { + var svgDoc = V.createSvgDocument(el); - // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. - // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. - var json = Backbone.Model.prototype.toJSON.apply(this, arguments); - json.cells = this.get('cells').toJSON(); - return json; - }, + // 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) { - fromJSON: function(json, opt) { + // Map child nodes to `V`s. + var arrayOfVels = []; + var i, len; - if (!json.cells) { + for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { - throw new Error('Graph JSON must contain cells array.'); - } + var childNode = svgDoc.childNodes[i]; + arrayOfVels.push(new V(document.importNode(childNode, true))); + } - return this.set(json, opt); - }, + return arrayOfVels; + } - set: function(key, val, opt) { + el = document.importNode(svgDoc.firstChild, true); - var attrs; + } else { - // Handle both `key`, value and {key: value} style arguments. - if (typeof key === 'object') { - attrs = key; - opt = val; - } else { - (attrs = {})[key] = val; - } + el = document.createElementNS(ns.xmlns, el); + } - // Make sure that `cells` attribute is handled separately via resetCells(). - if (attrs.hasOwnProperty('cells')) { - this.resetCells(attrs.cells, opt); - attrs = joint.util.omit(attrs, 'cells'); + V.ensureId(el); } - // The rest of the attributes are applied via original set method. - return Backbone.Model.prototype.set.call(this, attrs, opt); - }, - - clear: function(opt) { + this.node = el; - opt = joint.util.assign({}, opt, { clear: true }); + this.setAttributes(attrs); - var collection = this.get('cells'); + if (children) { + this.append(children); + } - if (collection.length === 0) return this; + return this; + }; - this.startBatch('clear', opt); + var VPrototype = V.prototype; - // The elements come after the links. - var cells = collection.sortBy(function(cell) { - return cell.isLink() ? 1 : 2; - }); + Object.defineProperty(VPrototype, 'id', { + enumerable: true, + get: function() { + return this.node.id; + }, + set: function(id) { + this.node.id = id; + } + }); - do { + /** + * @param {SVGGElement} toElem + * @returns {SVGMatrix} + */ + VPrototype.getTransformToElement = function(toElem) { + toElem = V.toNode(toElem); + return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); + }; - // Remove all the cells one by one. - // Note that all the links are removed first, so it's - // safe to remove the elements without removing the connected - // links first. - cells.shift().remove(opt); + /** + * @param {SVGMatrix} matrix + * @param {Object=} opt + * @returns {Vectorizer|SVGMatrix} Setter / Getter + */ + VPrototype.transform = function(matrix, opt) { - } while (cells.length > 0); + var node = this.node; + if (V.isUndefined(matrix)) { + return V.transformStringToMatrix(this.attr('transform')); + } - this.stopBatch('clear'); + if (opt && opt.absolute) { + return this.attr('transform', V.matrixToTransformString(matrix)); + } + var svgTransform = V.createSVGTransform(matrix); + node.transform.baseVal.appendItem(svgTransform); return this; - }, + }; - _prepareCell: function(cell, opt) { + VPrototype.translate = function(tx, ty, opt) { - var attrs; - if (cell instanceof Backbone.Model) { - attrs = cell.attributes; - if (!cell.graph && (!opt || !opt.dry)) { - // An element can not be member of more than one graph. - // A cell stops being the member of the graph after it's explicitely removed. - cell.graph = this; - } - } else { - // In case we're dealing with a plain JS object, we have to set the reference - // to the `graph` right after the actual model is created. This happens in the `model()` function - // of `joint.dia.GraphCells`. - attrs = cell; - } + opt = opt || {}; + ty = ty || 0; - if (!joint.util.isString(attrs.type)) { - throw new TypeError('dia.Graph: cell type must be a string.'); + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; + // Is it a getter? + if (V.isUndefined(tx)) { + return transform.translate; } - return cell; - }, - - maxZIndex: function() { + transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); - var lastCell = this.get('cells').last(); - return lastCell ? (lastCell.get('z') || 0) : 0; - }, + var newTx = opt.absolute ? tx : transform.translate.tx + tx; + var newTy = opt.absolute ? ty : transform.translate.ty + ty; + var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; - addCell: function(cell, opt) { + // Note that `translate()` is always the first transformation. This is + // usually the desired case. + this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); + return this; + }; - if (Array.isArray(cell)) { + VPrototype.rotate = function(angle, cx, cy, opt) { - return this.addCells(cell, opt); - } + opt = opt || {}; - if (cell instanceof Backbone.Model) { + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; - if (!cell.has('z')) { - cell.set('z', this.maxZIndex() + 1); - } + // Is it a getter? + if (V.isUndefined(angle)) { + return transform.rotate; + } - } else if (cell.z === undefined) { + transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); - cell.z = this.maxZIndex() + 1; - } + angle %= 360; - this.get('cells').add(this._prepareCell(cell, opt), opt || {}); + 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; - }, + }; - addCells: function(cells, opt) { + // Note that `scale` as the only transformation does not combine with previous values. + VPrototype.scale = function(sx, sy) { - if (cells.length) { + sy = V.isUndefined(sy) ? sx : sy; - cells = joint.util.flattenDeep(cells); - opt.position = cells.length; + var transformAttr = this.attr('transform') || ''; + var transform = V.parseTransformString(transformAttr); + transformAttr = transform.value; - this.startBatch('add'); - cells.forEach(function(cell) { - opt.position--; - this.addCell(cell, opt); - }, this); - this.stopBatch('add'); + // 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; - }, + }; - // When adding a lot of cells, it is much more efficient to - // reset the entire cells collection in one go. - // Useful for bulk operations and optimizations. - resetCells: function(cells, opt) { + // 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. + VPrototype.bbox = function(withoutTransformations, target) { - var preparedCells = joint.util.toArray(cells).map(function(cell) { - return this._prepareCell(cell, opt); - }, this); - this.get('cells').reset(preparedCells, opt); + var box; + var node = this.node; + var ownerSVGElement = node.ownerSVGElement; - return this; - }, + // 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 new g.Rect(0, 0, 0, 0); + } - removeCells: function(cells, opt) { + try { - if (cells.length) { + box = node.getBBox(); - this.startBatch('remove'); - joint.util.invoke(cells, 'remove', opt); - this.stopBatch('remove'); + } catch (e) { + + // Fallback for IE. + box = { + x: node.clientLeft, + y: node.clientTop, + width: node.clientWidth, + height: node.clientHeight + }; } - return this; - }, + if (withoutTransformations) { + return new g.Rect(box); + } - _removeCell: function(cell, collection, options) { + var matrix = this.getTransformToElement(target || ownerSVGElement); - options = options || {}; + return V.transformRect(box, matrix); + }; - if (!options.clear) { - // Applications might provide a `disconnectLinks` option set to `true` in order to - // disconnect links when a cell is removed rather then removing them. The default - // is to remove all the associated links. - if (options.disconnectLinks) { + // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, + // i.e. after transformations are applied. + // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. + // Takes an (Object) `opt` argument (optional) with the following attributes: + // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this + // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); + VPrototype.getBBox = function(opt) { - this.disconnectLinks(cell, options); + var options = {}; - } else { + var outputBBox; + var node = this.node; + var ownerSVGElement = node.ownerSVGElement; - this.removeLinks(cell, options); - } + // 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 new g.Rect(0, 0, 0, 0); } - // Silently remove the cell from the cells collection. Silently, because - // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is - // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events - // would be triggered on the graph model. - this.get('cells').remove(cell, { silent: true }); - if (cell.graph === this) { - // Remove the element graph reference only if the cell is the member of this graph. - cell.graph = null; + if (opt) { + if (opt.target) { // check if target exists + options.target = V.toNode(opt.target); // works for V objects, jquery objects, and node objects + } + if (opt.recursive) { + options.recursive = opt.recursive; + } } - }, - - // Get a cell by `id`. - getCell: function(id) { - return this.get('cells').get(id); - }, + if (!options.recursive) { + try { + outputBBox = node.getBBox(); + } catch (e) { + // Fallback for IE. + outputBBox = { + x: node.clientLeft, + y: node.clientTop, + width: node.clientWidth, + height: node.clientHeight + }; + } - getCells: function() { + if (!options.target) { + // transform like this (that is, not at all) + return new g.Rect(outputBBox); + } else { + // transform like target + var matrix = this.getTransformToElement(options.target); + return V.transformRect(outputBBox, matrix); + } + } else { // if we want to calculate the bbox recursively + // browsers report correct bbox around svg elements (one that envelops the path lines tightly) + // but some browsers fail to report the same bbox when the elements are in a group (returning a looser bbox that also includes control points, like node.getClientRect()) + // this happens even if we wrap a single svg element into a group! + // this option setting makes the function recursively enter all the groups from this and deeper, get bboxes of the elements inside, then return a union of those bboxes - return this.get('cells').toArray(); - }, + var children = this.children(); + var n = children.length; - getElements: function() { - return Object.keys(this._nodes).map(this.getCell, this); - }, + if (n === 0) { + return this.getBBox({ target: options.target, recursive: false }); + } - getLinks: function() { - return Object.keys(this._edges).map(this.getCell, this); - }, + // recursion's initial pass-through setting: + // recursive passes-through just keep the target as whatever was set up here during the initial pass-through + if (!options.target) { + // transform children/descendants like this (their parent/ancestor) + options.target = this; + } // else transform children/descendants like target - getFirstCell: function() { + for (var i = 0; i < n; i++) { + var currentChild = children[i]; - return this.get('cells').first(); - }, + var childBBox; - getLastCell: function() { + // if currentChild is not a group element, get its bbox with a nonrecursive call + if (currentChild.children().length === 0) { + childBBox = currentChild.getBBox({ target: options.target, recursive: false }); + } + else { + // if currentChild is a group element (determined by checking the number of children), enter it with a recursive call + childBBox = currentChild.getBBox({ target: options.target, recursive: true }); + } - return this.get('cells').last(); - }, + if (!outputBBox) { + // if this is the first iteration + outputBBox = childBBox; + } else { + // make a new bounding box rectangle that contains this child's bounding box and previous bounding box + outputBBox = outputBBox.union(childBBox); + } + } - // Get all inbound and outbound links connected to the cell `model`. - getConnectedLinks: function(model, opt) { + return outputBBox; + } + }; - opt = opt || {}; + // Text() helpers - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; + function createTextPathNode(attrs, vel) { + attrs || (attrs = {}); + var textPathElement = V('textPath'); + var d = attrs.d; + if (d && attrs['xlink:href'] === undefined) { + // If `opt.attrs` 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. + // Wrap the text in the SVG element that points + // to a path defined by `opt.attrs` inside the `` element. + var linkedPath = V('path').attr('d', d).appendTo(vel.defs()); + textPathElement.attr('xlink:href', '#' + linkedPath.id); } + if (V.isObject(attrs)) { + // 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. + textPathElement.attr(attrs); + } + return textPathElement.node; + } - // The final array of connected link models. - var links = []; - // Connected edges. This hash table ([edge] -> true) serves only - // for a quick lookup to check if we already added a link. - var edges = {}; + function annotateTextLine(lineNode, lineAnnotations, opt) { + opt || (opt = {}); + var includeAnnotationIndices = opt.includeAnnotationIndices; + var eol = opt.eol; + var lineHeight = opt.lineHeight; + var baseSize = opt.baseSize; + var maxFontSize = 0; + var fontMetrics = {}; + var lastJ = lineAnnotations.length - 1; + for (var j = 0; j <= lastJ; j++) { + var annotation = lineAnnotations[j]; + var fontSize = null; + if (V.isObject(annotation)) { + var annotationAttrs = annotation.attrs; + var vTSpan = V('tspan', annotationAttrs); + var tspanNode = vTSpan.node; + var t = annotation.t; + if (eol && j === lastJ) t += eol; + tspanNode.textContent = t; + // Per annotation className + var annotationClass = annotationAttrs['class']; + if (annotationClass) vTSpan.addClass(annotationClass); + // 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. + if (includeAnnotationIndices) vTSpan.attr('annotations', annotation.annotations); + // Check for max font size + fontSize = parseFloat(annotationAttrs['font-size']); + if (fontSize === undefined) fontSize = baseSize; + if (fontSize && fontSize > maxFontSize) maxFontSize = fontSize; + } else { + if (eol && j === lastJ) annotation += eol; + tspanNode = document.createTextNode(annotation || ' '); + if (baseSize && baseSize > maxFontSize) maxFontSize = baseSize; + } + lineNode.appendChild(tspanNode); + } - if (outbound) { - joint.util.forIn(this.getOutboundEdges(model.id), function(exists, edge) { - if (!edges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + if (maxFontSize) fontMetrics.maxFontSize = maxFontSize; + if (lineHeight) { + fontMetrics.lineHeight = lineHeight; + } else if (maxFontSize) { + fontMetrics.lineHeight = (maxFontSize * 1.2); } - if (inbound) { - joint.util.forIn(this.getInboundEdges(model.id), function(exists, edge) { - // Skip links that were already added. Those must be self-loop links - // because they are both inbound and outbond edges of the same element. - if (!edges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + return fontMetrics; + } + + var emRegex = /em$/; + + function convertEmToPx(em, fontSize) { + var numerical = parseFloat(em); + if (emRegex.test(em)) return numerical * fontSize; + return numerical; + } + + function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) { + if (!Array.isArray(linesMetrics)) return 0; + var n = linesMetrics.length; + if (!n) return 0; + var lineMetrics = linesMetrics[0]; + var flMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var rLineHeights = 0; + var lineHeightPx = convertEmToPx(lineHeight, baseSizePx); + for (var i = 1; i < n; i++) { + lineMetrics = linesMetrics[i]; + var iLineHeight = convertEmToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx; + rLineHeights += iLineHeight; + } + var llMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var dy; + switch (alignment) { + case 'middle': + dy = (flMaxFont / 2) - (0.15 * llMaxFont) - (rLineHeights / 2); + break; + case 'bottom': + dy = -(0.25 * llMaxFont) - rLineHeights; + break; + default: + case 'top': + dy = (0.8 * flMaxFont) + break; } + return dy; + } - // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells - // and are not descendents themselves. - if (opt.deep) { + VPrototype.text = function(content, opt) { - var embeddedCells = model.getEmbeddedCells({ deep: true }); - // In the first round, we collect all the embedded edges so that we can exclude - // them from the final result. - var embeddedEdges = {}; - embeddedCells.forEach(function(cell) { - if (cell.isLink()) { - embeddedEdges[cell.id] = true; + if (content && typeof content !== 'string') throw new Error('Vectorizer: text() expects the first argument to be a string.'); + + // 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 = {}); + + // End of Line character + var eol = opt.eol; + // Text along path + var textPath = opt.textPath + // Vertical shift + var verticalAnchor = opt.textVerticalAnchor; + var namedVerticalAnchor = (verticalAnchor === 'middle' || verticalAnchor === 'bottom' || verticalAnchor === 'top'); + // Horizontal shift applied to all the lines but the first. + var x = opt.x; + if (x === undefined) x = this.attr('x') || 0; + // Annotations + var iai = opt.includeAnnotationIndices; + var annotations = opt.annotations; + if (annotations && !V.isArray(annotations)) annotations = [annotations]; + // Shift all the but first by one line (`1em`) + var defaultLineHeight = opt.lineHeight; + var autoLineHeight = (defaultLineHeight === 'auto'); + var lineHeight = (autoLineHeight) ? '1.5em' : (defaultLineHeight || '1em'); + // Clearing the element + this.empty(); + this.attr({ + // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. + 'xml:space': 'preserve', + // 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. + 'display': (content) ? null : 'none' + }); + // Set default font-size if none + var fontSize = parseFloat(this.attr('font-size')); + if (!fontSize) { + fontSize = 16; + if (namedVerticalAnchor || annotations) this.attr('font-size', fontSize); + } + + var doc = document; + var containerNode; + if (textPath) { + // Now all the ``s will be inside the ``. + if (typeof textPath === 'string') textPath = { d: textPath }; + containerNode = createTextPathNode(textPath, this); + } else { + containerNode = doc.createDocumentFragment(); + } + var offset = 0; + var lines = content.split('\n'); + var linesMetrics = []; + var annotatedY; + for (var i = 0, lastI = lines.length - 1; i <= lastI; i++) { + var dy = lineHeight; + var lineClassName = 'v-line'; + var lineNode = doc.createElementNS(V.namespace.xmlns, 'tspan'); + var line = lines[i]; + var lineMetrics; + if (line) { + if (annotations) { + // Find the *compacted* annotations for this line. + var lineAnnotations = V.annotateString(line, annotations, { + offset: -offset, + includeAnnotationIndices: iai + }); + lineMetrics = annotateTextLine(lineNode, lineAnnotations, { + includeAnnotationIndices: iai, + eol: (i !== lastI && eol), + lineHeight: (autoLineHeight) ? null : lineHeight, + baseSize: fontSize + }); + // Get the line height based on the biggest font size in the annotations for this line. + var iLineHeight = lineMetrics.lineHeight; + if (iLineHeight && autoLineHeight && i !== 0) dy = iLineHeight; + if (i === 0) annotatedY = lineMetrics.maxFontSize * 0.8; + } else { + if (eol && i !== lastI) line += eol; + lineNode.textContent = line; } - }); - embeddedCells.forEach(function(cell) { - if (cell.isLink()) return; - if (outbound) { - joint.util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { - if (!edges[edge] && !embeddedEdges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + } 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. + lineNode.textContent = '-'; + lineClassName += ' v-empty-line'; + // 'opacity' needs to be specified with fill, stroke. Opacity without specification + // is not applied in Firefox + var lineNodeStyle = lineNode.style; + lineNodeStyle.fillOpacity = 0; + lineNodeStyle.strokeOpacity = 0; + if (annotations) lineMetrics = {}; + } + if (lineMetrics) linesMetrics.push(lineMetrics); + if (i > 0) lineNode.setAttribute('dy', dy); + // Firefox requires 'x' to be set on the first line when inside a text path + if (i > 0 || textPath) lineNode.setAttribute('x', x); + lineNode.className.baseVal = lineClassName; + containerNode.appendChild(lineNode); + offset += line.length + 1; // + 1 = newline character. + } + // Y Alignment calculation + if (namedVerticalAnchor) { + if (annotations) { + dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight); + } else if (verticalAnchor === 'top') { + // A shortcut for top alignment. It does not depend on font-size nor line-height + dy = '0.8em'; + } else { + var rh; // remaining height + if (lastI > 0) { + rh = parseFloat(lineHeight) || 1; + rh *= lastI; + if (!emRegex.test(lineHeight)) rh /= fontSize; + } else { + // Single-line text + rh = 0; } - if (inbound) { - joint.util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { - if (!edges[edge] && !embeddedEdges[edge]) { - links.push(this.getCell(edge)); - edges[edge] = true; - } - }.bind(this)); + switch (verticalAnchor) { + case 'middle': + dy = (0.3 - (rh / 2)) + 'em' + break; + case 'bottom': + dy = (-rh - 0.3) + 'em' + break; } - }, this); + } + } else { + if (verticalAnchor === 0) { + dy = '0em'; + } else if (verticalAnchor) { + dy = verticalAnchor; + } else { + // No vertical anchor is defined + dy = 0; + // Backwards compatibility - we change the `y` attribute instead of `dy`. + if (this.attr('y') === null) this.attr('y', annotatedY || '0.8em'); + } } + containerNode.firstChild.setAttribute('dy', dy); + // Appending lines to the element. + this.append(containerNode); + return this; + }; - return links; - }, - - getNeighbors: function(model, opt) { + /** + * @public + * @param {string} name + * @returns {Vectorizer} + */ + VPrototype.removeAttr = function(name) { - opt = opt || {}; + var qualifiedName = V.qualifyAttr(name); + var el = this.node; - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; + 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; + }; - var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { - - var source = link.get('source'); - var target = link.get('target'); - var loop = link.hasLoop(opt); + VPrototype.attr = function(name, value) { - // Discard if it is a point, or if the neighbor was already added. - if (inbound && joint.util.has(source, 'id') && !res[source.id]) { + if (V.isUndefined(name)) { - var sourceElement = this.getCell(source.id); + // Return all attributes. + var attributes = this.node.attributes; + var attrs = {}; - if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { - res[source.id] = sourceElement; - } + for (var i = 0; i < attributes.length; i++) { + attrs[attributes[i].name] = attributes[i].value; } - // Discard if it is a point, or if the neighbor was already added. - if (outbound && joint.util.has(target, 'id') && !res[target.id]) { + return attrs; + } - var targetElement = this.getCell(target.id); + if (V.isString(name) && V.isUndefined(value)) { + return this.node.getAttribute(name); + } - if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { - res[target.id] = targetElement; + if (typeof name === 'object') { + + for (var attrName in name) { + if (name.hasOwnProperty(attrName)) { + this.setAttribute(attrName, name[attrName]); } } - return res; - }.bind(this), {}); + } else { - return joint.util.toArray(neighbors); - }, + this.setAttribute(name, value); + } - getCommonAncestor: function(/* cells */) { + return this; + }; - var cellsAncestors = Array.from(arguments).map(function(cell) { + VPrototype.normalizePath = function() { - var ancestors = []; - var parentId = cell.get('parent'); + var tagName = this.tagName(); + if (tagName === 'PATH') { + this.attr('d', V.normalizePathData(this.attr('d'))); + } - while (parentId) { + return this; + } - ancestors.push(parentId); - parentId = this.getCell(parentId).get('parent'); - } + VPrototype.remove = function() { - return ancestors; + if (this.node.parentNode) { + this.node.parentNode.removeChild(this.node); + } - }, this); + return this; + }; - cellsAncestors = cellsAncestors.sort(function(a, b) { - return a.length - b.length; - }); + VPrototype.empty = function() { - var commonAncestor = joint.util.toArray(cellsAncestors.shift()).find(function(ancestor) { - return cellsAncestors.every(function(cellAncestors) { - return cellAncestors.includes(ancestor); - }); - }); + while (this.node.firstChild) { + this.node.removeChild(this.node.firstChild); + } - return this.getCell(commonAncestor); - }, + return this; + }; - // Find the whole branch starting at `element`. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. - getSuccessors: function(element, opt) { + /** + * @private + * @param {object} attrs + * @returns {Vectorizer} + */ + VPrototype.setAttributes = function(attrs) { - opt = opt || {}; - var res = []; - // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. - this.search(element, function(el) { - if (el !== element) { - res.push(el); + for (var key in attrs) { + if (attrs.hasOwnProperty(key)) { + this.setAttribute(key, attrs[key]); } - }, joint.util.assign({}, opt, { outbound: true })); - return res; - }, + } - // Clone `cells` returning an object that maps the original cell ID to the clone. The number - // of clones is exactly the same as the `cells.length`. - // This function simply clones all the `cells`. However, it also reconstructs - // all the `source/target` and `parent/embed` references within the `cells`. - // This is the main difference from the `cell.clone()` method. The - // `cell.clone()` method works on one single cell only. - // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` - // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. - // the source and target of the link `L2` is changed to point to `A2` and `B2`. - cloneCells: function(cells) { + return this; + }; - cells = joint.util.uniq(cells); + VPrototype.append = function(els) { - // A map of the form [original cell ID] -> [clone] helping - // us to reconstruct references for source/target and parent/embeds. - // This is also the returned value. - var cloneMap = joint.util.toArray(cells).reduce(function(map, cell) { - map[cell.id] = cell.clone(); - return map; - }, {}); + 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; + }; + + VPrototype.prepend = function(els) { + + var child = this.node.firstChild; + return child ? V(child).before(els) : this.append(els); + }; + + VPrototype.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; + }; + + VPrototype.appendTo = function(node) { + V.toNode(node).appendChild(this.node); + return this; + }, + + VPrototype.svg = function() { + + return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); + }; + + VPrototype.tagName = function() { + + return this.node.tagName.toUpperCase(); + }; + + VPrototype.defs = function() { + var context = this.svg() || this; + var defsNode = context.node.getElementsByTagName('defs')[0]; + if (defsNode) return V(defsNode); + return V('defs').appendTo(context); + }; + + VPrototype.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; + }; + + VPrototype.findOne = function(selector) { + + var found = this.node.querySelector(selector); + return found ? V(found) : undefined; + }; + + VPrototype.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; + }; + + // Returns an array of V elements made from children of this.node. + VPrototype.children = function() { + + var children = this.node.childNodes; + + var outputArray = []; + for (var i = 0; i < children.length; i++) { + var currentChild = children[i]; + if (currentChild.nodeType === 1) { + outputArray.push(V(children[i])); + } + } + return outputArray; + }; + + // Find an index of an element inside its container. + VPrototype.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; + }; + + VPrototype.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 + VPrototype.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. + VPrototype.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); + }; + + VPrototype.translateCenterToPoint = function(p) { + + var bbox = this.getBBox({ target: this.svg() }); + var center = bbox.center(); + + this.translate(p.x - center.x, p.y - center.y); + return this; + }; + + // 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. + VPrototype.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.getBBox({ target: target || svg }); + + // 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 = (new 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 = (new 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 || svg); + 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; + }; + + VPrototype.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(); + } + } + } + return this; + }; + + VPrototype.hasClass = function(className) { + + return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); + }; + + VPrototype.addClass = function(className) { + + if (!this.hasClass(className)) { + var prevClasses = this.node.getAttribute('class') || ''; + this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); + } + + return this; + }; + + VPrototype.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; + }; + + VPrototype.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). + VPrototype.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; + }; + + VPrototype.convertToPath = function() { + + var path = V('path'); + path.attr(this.attr()); + var d = this.convertToPathData(); + if (d) { + path.attr('d', d); + } + return path; + }; + + VPrototype.convertToPathData = function() { + + var tagName = this.tagName(); + + 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.'); + }; + + V.prototype.toGeometryShape = function() { + var x, y, width, height, cx, cy, r, rx, ry, points, d; + switch (this.tagName()) { + + case 'RECT': + x = parseFloat(this.attr('x')) || 0; + y = parseFloat(this.attr('y')) || 0; + width = parseFloat(this.attr('width')) || 0; + height = parseFloat(this.attr('height')) || 0; + return new g.Rect(x, y, width, height); + + case 'CIRCLE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + r = parseFloat(this.attr('r')) || 0; + return new g.Ellipse({ x: cx, y: cy }, r, r); + + case 'ELLIPSE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + rx = parseFloat(this.attr('rx')) || 0; + ry = parseFloat(this.attr('ry')) || 0; + return new g.Ellipse({ x: cx, y: cy }, rx, ry); + + case 'POLYLINE': + points = V.getPointsFromSvgNode(this); + return new g.Polyline(points); + + case 'POLYGON': + points = V.getPointsFromSvgNode(this); + if (points.length > 1) points.push(points[0]); + return new g.Polyline(points); + + case 'PATH': + d = this.attr('d'); + if (!g.Path.isDataSupported(d)) d = V.normalizePathData(d); + return new g.Path(d); + + case 'LINE': + x1 = parseFloat(this.attr('x1')) || 0; + y1 = parseFloat(this.attr('y1')) || 0; + x2 = parseFloat(this.attr('x2')) || 0; + y2 = parseFloat(this.attr('y2')) || 0; + return new g.Line({ x: x1, y: y1 }, { x: x2, y: y2 }); + } + + // Anything else is a rectangle + return this.getBBox(); + }, + + // 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. + VPrototype.findIntersection = function(ref, target) { + + var svg = this.svg().node; + target = target || svg; + var bbox = this.getBBox({ target: target }); + var center = bbox.center(); + + if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; + + var spot; + var tagName = this.tagName(); + + // 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 = new 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 = (new 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 = new 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} + */ + VPrototype.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 `` root element. + V.createSvgDocument = function(content) { + + var svg = '' + (content || '') + ''; + var xml = V.parseXML(svg, { async: false }); + return xml.documentElement; + }; + + V.idCounter = 0; + + // A function returning a unique identifier for this client session with every call. + V.uniqueId = function() { + + return 'v-' + (++V.idCounter); + }; + + V.toNode = function(el) { + + return V.isV(el) ? el.node : (el.nodeName && el || el[0]); + }; + + V.ensureId = function(node) { + + node = V.toNode(node); + return node.id || (node.id = V.uniqueId()); + }; + + // 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. This is used in the text() method but it is + // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests + // when you want to compare the actual DOM text content without having to add the unicode character in + // the place of all spaces. + V.sanitizeText = function(text) { + + return (text || '').replace(/ /g, '\u00A0'); + }; + + V.isUndefined = function(value) { + + return typeof value === 'undefined'; + }; + + V.isString = function(value) { + + return typeof value === 'string'; + }; + + V.isObject = function(value) { + + return value && (typeof value === 'object'); + }; + + V.isArray = Array.isArray; + + V.parseXML = function(data, opt) { + + opt = opt || {}; + + var xml; + + try { + var parser = new DOMParser(); + + if (!V.isUndefined(opt.async)) { + parser.async = opt.async; + } + + xml = parser.parseFromString(data, 'text/xml'); + } catch (error) { + xml = undefined; + } + + if (!xml || xml.getElementsByTagName('parsererror').length) { + throw new Error('Invalid XML: ' + data); + } + + return xml; + }; + + /** + * @param {string} name + * @returns {{ns: string|null, local: string}} namespace and attribute name + */ + V.qualifyAttr = function(name) { + + if (name.indexOf(':') !== -1) { + var combinedKey = name.split(':'); + return { + ns: ns[combinedKey[0]], + local: combinedKey[1] + }; + } + + return { + ns: null, + local: name + }; + }; + + V.transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi; + V.transformSeparatorRegex = /[ ,]+/; + V.transformationListRegex = /^(\w+)\((.*)\)/; + + V.transformStringToMatrix = function(transform) { + + var transformationMatrix = V.createSVGMatrix(); + var matches = transform && transform.match(V.transformRegex); + if (!matches) { + return transformationMatrix; + } + + for (var i = 0, n = matches.length; i < n; i++) { + var transformationString = matches[i]; + + var transformationMatch = transformationString.match(V.transformationListRegex); + if (transformationMatch) { + var sx, sy, tx, ty, angle; + var ctm = V.createSVGMatrix(); + var args = transformationMatch[2].split(V.transformSeparatorRegex); + switch (transformationMatch[1].toLowerCase()) { + case 'scale': + sx = parseFloat(args[0]); + sy = (args[1] === undefined) ? sx : parseFloat(args[1]); + ctm = ctm.scaleNonUniform(sx, sy); + break; + case 'translate': + tx = parseFloat(args[0]); + ty = parseFloat(args[1]); + ctm = ctm.translate(tx, ty); + break; + case 'rotate': + angle = parseFloat(args[0]); + tx = parseFloat(args[1]) || 0; + ty = parseFloat(args[2]) || 0; + if (tx !== 0 || ty !== 0) { + ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty); + } else { + ctm = ctm.rotate(angle); + } + break; + case 'skewx': + angle = parseFloat(args[0]); + ctm = ctm.skewX(angle); + break; + case 'skewy': + angle = parseFloat(args[0]); + ctm = ctm.skewY(angle); + break; + case 'matrix': + ctm.a = parseFloat(args[0]); + ctm.b = parseFloat(args[1]); + ctm.c = parseFloat(args[2]); + ctm.d = parseFloat(args[3]); + ctm.e = parseFloat(args[4]); + ctm.f = parseFloat(args[5]); + break; + default: + continue; + } + + transformationMatrix = transformationMatrix.multiply(ctm); + } + + } + return transformationMatrix; + }; + + V.matrixToTransformString = function(matrix) { + matrix || (matrix = true); + + return 'matrix(' + + (matrix.a !== undefined ? matrix.a : 1) + ',' + + (matrix.b !== undefined ? matrix.b : 0) + ',' + + (matrix.c !== undefined ? matrix.c : 0) + ',' + + (matrix.d !== undefined ? matrix.d : 1) + ',' + + (matrix.e !== undefined ? matrix.e : 0) + ',' + + (matrix.f !== undefined ? matrix.f : 0) + + ')'; + }; + + V.parseTransformString = function(transform) { + + var translate, rotate, scale; + + if (transform) { + + var separator = V.transformSeparatorRegex; + + // Allow reading transform string with a single matrix + if (transform.trim().indexOf('matrix') >= 0) { + + var matrix = V.transformStringToMatrix(transform); + var decomposedMatrix = V.decomposeMatrix(matrix); + + translate = [decomposedMatrix.translateX, decomposedMatrix.translateY]; + scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]; + rotate = [decomposedMatrix.rotation]; + + var transformations = []; + if (translate[0] !== 0 || translate[0] !== 0) { + transformations.push('translate(' + translate + ')'); + } + if (scale[0] !== 1 || scale[1] !== 1) { + transformations.push('scale(' + scale + ')'); + } + if (rotate[0] !== 0) { + transformations.push('rotate(' + rotate + ')'); + } + transform = transformations.join(' '); + + } else { + + var translateMatch = transform.match(/translate\((.*?)\)/); + if (translateMatch) { + translate = translateMatch[1].split(separator); + } + var rotateMatch = transform.match(/rotate\((.*?)\)/); + if (rotateMatch) { + rotate = rotateMatch[1].split(separator); + } + var scaleMatch = transform.match(/scale\((.*?)\)/); + if (scaleMatch) { + scale = scaleMatch[1].split(separator); + } + } + } + + var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; + + return { + value: transform, + translate: { + tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, + ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 + }, + rotate: { + angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, + cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, + cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined + }, + scale: { + sx: sx, + sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx + } + }; + }; + + V.deltaTransformPoint = function(matrix, point) { + + var dx = point.x * matrix.a + point.y * matrix.c + 0; + var dy = point.x * matrix.b + point.y * matrix.d + 0; + return { x: dx, y: dy }; + }; + + V.decomposeMatrix = function(matrix) { + + // @see https://gist.github.com/2052247 + + // calculate delta transform point + var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); + var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); + + // calculate skew + var skewX = ((180 / PI) * atan2(px.y, px.x) - 90); + var skewY = ((180 / PI) * atan2(py.y, py.x)); + + return { + + translateX: matrix.e, + translateY: matrix.f, + scaleX: sqrt(matrix.a * matrix.a + matrix.b * matrix.b), + scaleY: sqrt(matrix.c * matrix.c + matrix.d * matrix.d), + skewX: skewX, + skewY: skewY, + rotation: skewX // rotation is the same as skew x + }; + }; + + // Return the `scale` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToScale = function(matrix) { + + var a,b,c,d; + if (matrix) { + a = V.isUndefined(matrix.a) ? 1 : matrix.a; + d = V.isUndefined(matrix.d) ? 1 : matrix.d; + b = matrix.b; + c = matrix.c; + } else { + a = d = 1; + } + return { + sx: b ? sqrt(a * a + b * b) : a, + sy: c ? sqrt(c * c + d * d) : d + }; + }, + + // Return the `rotate` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToRotate = function(matrix) { + + var p = { x: 0, y: 1 }; + if (matrix) { + p = V.deltaTransformPoint(matrix, p); + } + + return { + angle: g.normalizeAngle(g.toDeg(atan2(p.y, p.x)) - 90) + }; + }, + + // Return the `translate` transformation from the following equation: + // `translate(tx, ty) . rotate(angle) . scale(sx, sy) === matrix(a,b,c,d,e,f)` + V.matrixToTranslate = function(matrix) { + + return { + tx: (matrix && matrix.e) || 0, + ty: (matrix && matrix.f) || 0 + }; + }, + + V.isV = function(object) { + + return object instanceof V; + }; + + // For backwards compatibility: + V.isVElement = V.isV; + + var svgDocument = V('svg').node; + + V.createSVGMatrix = function(matrix) { + + var svgMatrix = svgDocument.createSVGMatrix(); + for (var component in matrix) { + svgMatrix[component] = matrix[component]; + } + + return svgMatrix; + }; + + V.createSVGTransform = function(matrix) { + + if (!V.isUndefined(matrix)) { + + if (!(matrix instanceof SVGMatrix)) { + matrix = V.createSVGMatrix(matrix); + } + + return svgDocument.createSVGTransformFromMatrix(matrix); + } + + return svgDocument.createSVGTransform(); + }; + + V.createSVGPoint = function(x, y) { + + var p = svgDocument.createSVGPoint(); + p.x = x; + p.y = y; + return p; + }; + + V.transformRect = function(r, matrix) { + + var p = svgDocument.createSVGPoint(); + + p.x = r.x; + p.y = r.y; + var corner1 = p.matrixTransform(matrix); + + p.x = r.x + r.width; + p.y = r.y; + var corner2 = p.matrixTransform(matrix); + + p.x = r.x + r.width; + p.y = r.y + r.height; + var corner3 = p.matrixTransform(matrix); + + p.x = r.x; + p.y = r.y + r.height; + var corner4 = p.matrixTransform(matrix); + + var minX = min(corner1.x, corner2.x, corner3.x, corner4.x); + var maxX = max(corner1.x, corner2.x, corner3.x, corner4.x); + var minY = min(corner1.y, corner2.y, corner3.y, corner4.y); + var maxY = max(corner1.y, corner2.y, corner3.y, corner4.y); + + return new g.Rect(minX, minY, maxX - minX, maxY - minY); + }; + + V.transformPoint = function(p, matrix) { + + return new g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); + }; + + V.transformLine = function(l, matrix) { + + return new g.Line( + V.transformPoint(l.start, matrix), + V.transformPoint(l.end, matrix) + ); + }; + + V.transformPolyline = function(p, matrix) { + + var inPoints = (p instanceof g.Polyline) ? p.points : p; + if (!V.isArray(inPoints)) inPoints = []; + var outPoints = []; + for (var i = 0, n = inPoints.length; i < n; i++) outPoints[i] = V.transformPoint(inPoints[i], matrix); + return new g.Polyline(outPoints); + }, + + // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to + // an object (`{ fill: 'blue', stroke: 'red' }`). + V.styleToObject = function(styleString) { + var ret = {}; + var styles = styleString.split(';'); + for (var i = 0; i < styles.length; i++) { + var style = styles[i]; + var pair = style.split('='); + ret[pair[0].trim()] = pair[1].trim(); + } + return ret; + }; + + // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js + V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { + + var svgArcMax = 2 * PI - 1e-6; + var r0 = innerRadius; + var r1 = outerRadius; + var a0 = startAngle; + var a1 = endAngle; + var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); + var df = da < PI ? '0' : '1'; + var c0 = cos(a0); + var s0 = sin(a0); + var c1 = cos(a1); + var s1 = sin(a1); + + return (da >= svgArcMax) + ? (r0 + ? 'M0,' + r1 + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + + 'M0,' + r0 + + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) + + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 + + 'Z' + : 'M0,' + r1 + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + + 'Z') + : (r0 + ? 'M' + r1 * c0 + ',' + r1 * s0 + + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + + 'L' + r0 * c1 + ',' + r0 * s1 + + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 + + 'Z' + : 'M' + r1 * c0 + ',' + r1 * s0 + + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + + 'L0,0' + + 'Z'); + }; + + // Merge attributes from object `b` with attributes in object `a`. + // Note that this modifies the object `a`. + // Also important to note that attributes are merged but CSS classes are concatenated. + V.mergeAttrs = function(a, b) { + + for (var attr in b) { + + if (attr === 'class') { + // Concatenate classes. + a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; + } else if (attr === 'style') { + // `style` attribute can be an object. + if (V.isObject(a[attr]) && V.isObject(b[attr])) { + // `style` stored in `a` is an object. + a[attr] = V.mergeAttrs(a[attr], b[attr]); + } else if (V.isObject(a[attr])) { + // `style` in `a` is an object but it's a string in `b`. + // Convert the style represented as a string to an object in `b`. + a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); + } else if (V.isObject(b[attr])) { + // `style` in `a` is a string, in `b` it's an object. + a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); + } else { + // Both styles are strings. + a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); + } + } else { + a[attr] = b[attr]; + } + } + + return a; + }; + + V.annotateString = function(t, annotations, opt) { + + annotations = annotations || []; + opt = opt || {}; + + var offset = opt.offset || 0; + var compacted = []; + var batch; + var ret = []; + var item; + var prev; + + for (var i = 0; i < t.length; i++) { + + item = ret[i] = t[i]; + + for (var j = 0; j < annotations.length; j++) { + + var annotation = annotations[j]; + var start = annotation.start + offset; + var end = annotation.end + offset; + + if (i >= start && i < end) { + // Annotation applies. + if (V.isObject(item)) { + // There is more than one annotation to be applied => Merge attributes. + item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); + } else { + item = ret[i] = { t: t[i], attrs: annotation.attrs }; + } + if (opt.includeAnnotationIndices) { + (item.annotations || (item.annotations = [])).push(j); + } + } + } + + prev = ret[i - 1]; + + if (!prev) { + + batch = item; + + } else if (V.isObject(item) && V.isObject(prev)) { + // Both previous item and the current one are annotations. If the attributes + // didn't change, merge the text. + if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { + batch.t += item.t; + } else { + compacted.push(batch); + batch = item; + } + + } else if (V.isObject(item)) { + // Previous item was a string, current item is an annotation. + compacted.push(batch); + batch = item; + + } else if (V.isObject(prev)) { + // Previous item was an annotation, current item is a string. + compacted.push(batch); + batch = item; + + } else { + // Both previous and current item are strings. + batch = (batch || '') + item; + } + } + + if (batch) { + compacted.push(batch); + } + + return compacted; + }; + + V.findAnnotationsAtIndex = function(annotations, index) { + + var found = []; + + if (annotations) { + + annotations.forEach(function(annotation) { + + if (annotation.start < index && index <= annotation.end) { + found.push(annotation); + } + }); + } + + return found; + }; + + V.findAnnotationsBetweenIndexes = function(annotations, start, end) { + + var found = []; + + if (annotations) { + + annotations.forEach(function(annotation) { + + if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { + found.push(annotation); + } + }); + } + + return found; + }; + + // Shift all the text annotations after character `index` by `offset` positions. + V.shiftAnnotations = function(annotations, index, offset) { + + if (annotations) { + + annotations.forEach(function(annotation) { + + if (annotation.start < index && annotation.end >= index) { + annotation.end += offset; + } else if (annotation.start >= index) { + annotation.start += offset; + annotation.end += offset; + } + }); + } + + return annotations; + }; + + V.convertLineToPathData = function(line) { + + line = V(line); + var d = [ + 'M', line.attr('x1'), line.attr('y1'), + 'L', line.attr('x2'), line.attr('y2') + ].join(' '); + return d; + }; + + V.convertPolygonToPathData = function(polygon) { + + var points = V.getPointsFromSvgNode(polygon); + if (points.length === 0) return null; + + return V.svgPointsToPath(points) + ' Z'; + }; + + V.convertPolylineToPathData = function(polyline) { + + var points = V.getPointsFromSvgNode(polyline); + if (points.length === 0) return null; + + return V.svgPointsToPath(points); + }; + + V.svgPointsToPath = function(points) { + + for (var i = 0, n = points.length; i < n; i++) { + points[i] = points[i].x + ' ' + points[i].y; + } + + return 'M ' + points.join(' L'); + }; + + V.getPointsFromSvgNode = function(node) { + + node = V.toNode(node); + var points = []; + var nodePoints = node.points; + if (nodePoints) { + for (var i = 0, n = nodePoints.numberOfItems; i < n; i++) { + points.push(nodePoints.getItem(i)); + } + } + + return points; + }; + + V.KAPPA = 0.551784; + + V.convertCircleToPathData = function(circle) { + + circle = V(circle); + var cx = parseFloat(circle.attr('cx')) || 0; + var cy = parseFloat(circle.attr('cy')) || 0; + var r = parseFloat(circle.attr('r')); + var cd = r * V.KAPPA; // Control distance. + + var d = [ + 'M', cx, cy - r, // Move to the first point. + 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. + 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. + 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. + 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. + 'Z' + ].join(' '); + return d; + }; + + V.convertEllipseToPathData = function(ellipse) { + + ellipse = V(ellipse); + var cx = parseFloat(ellipse.attr('cx')) || 0; + var cy = parseFloat(ellipse.attr('cy')) || 0; + var rx = parseFloat(ellipse.attr('rx')); + var ry = parseFloat(ellipse.attr('ry')) || rx; + var cdx = rx * V.KAPPA; // Control distance x. + var cdy = ry * V.KAPPA; // Control distance y. + + var d = [ + 'M', cx, cy - ry, // Move to the first point. + 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. + 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. + 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. + 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. + 'Z' + ].join(' '); + return d; + }; + + V.convertRectToPathData = function(rect) { + + rect = V(rect); + + return V.rectToPath({ + x: parseFloat(rect.attr('x')) || 0, + y: parseFloat(rect.attr('y')) || 0, + width: parseFloat(rect.attr('width')) || 0, + height: parseFloat(rect.attr('height')) || 0, + rx: parseFloat(rect.attr('rx')) || 0, + ry: parseFloat(rect.attr('ry')) || 0 + }); + }; + + // Convert a rectangle to SVG path commands. `r` is an object of the form: + // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, + // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for + // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle + // that has only `rx` and `ry` attributes). + V.rectToPath = function(r) { + + var d; + var x = r.x; + var y = r.y; + var width = r.width; + var height = r.height; + var topRx = min(r.rx || r['top-rx'] || 0, width / 2); + var bottomRx = min(r.rx || r['bottom-rx'] || 0, width / 2); + var topRy = min(r.ry || r['top-ry'] || 0, height / 2); + var bottomRy = min(r.ry || r['bottom-ry'] || 0, height / 2); + + if (topRx || bottomRx || topRy || bottomRy) { + d = [ + 'M', x, y + topRy, + 'v', height - topRy - bottomRy, + 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, + 'h', width - 2 * bottomRx, + 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, + 'v', -(height - bottomRy - topRy), + 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, + 'h', -(width - 2 * topRx), + 'a', topRx, topRy, 0, 0, 0, -topRx, topRy, + 'Z' + ]; + } else { + d = [ + 'M', x, y, + 'H', x + width, + 'V', y + height, + 'H', x, + 'V', y, + 'Z' + ]; + } + + return d.join(' '); + }; + + // Take a path data string + // Return a normalized path data string + // If data cannot be parsed, return 'M 0 0' + // Adapted from Rappid normalizePath polyfill + // Highly inspired by Raphael Library (www.raphael.com) + V.normalizePathData = (function() { + + var spaces = '\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029'; + var pathCommand = new RegExp('([a-z])[' + spaces + ',]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?[' + spaces + ']*,?[' + spaces + ']*)+)', 'ig'); + var pathValues = new RegExp('(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)[' + spaces + ']*,?[' + spaces + ']*', 'ig'); + + var math = Math; + var PI = math.PI; + var sin = math.sin; + var cos = math.cos; + var tan = math.tan; + var asin = math.asin; + var sqrt = math.sqrt; + var abs = math.abs; + + function q2c(x1, y1, ax, ay, x2, y2) { + + var _13 = 1 / 3; + var _23 = 2 / 3; + return [(_13 * x1) + (_23 * ax), (_13 * y1) + (_23 * ay), (_13 * x2) + (_23 * ax), (_13 * y2) + (_23 * ay), x2, y2]; + } + + function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { + // for more information of where this math came from visit: + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + + var _120 = (PI * 120) / 180; + var rad = (PI / 180) * (+angle || 0); + var res = []; + var xy; + + var rotate = function(x, y, rad) { + + var X = (x * cos(rad)) - (y * sin(rad)); + var Y = (x * sin(rad)) + (y * cos(rad)); + return { x: X, y: Y }; + }; + + if (!recursive) { + xy = rotate(x1, y1, -rad); + x1 = xy.x; + y1 = xy.y; + + xy = rotate(x2, y2, -rad); + x2 = xy.x; + y2 = xy.y; + + var x = (x1 - x2) / 2; + var y = (y1 - y2) / 2; + var h = ((x * x) / (rx * rx)) + ((y * y) / (ry * ry)); + + if (h > 1) { + h = sqrt(h); + rx = h * rx; + ry = h * ry; + } + + var rx2 = rx * rx; + var ry2 = ry * ry; + + var k = ((large_arc_flag == sweep_flag) ? -1 : 1) * sqrt(abs(((rx2 * ry2) - (rx2 * y * y) - (ry2 * x * x)) / ((rx2 * y * y) + (ry2 * x * x)))); + + var cx = ((k * rx * y) / ry) + ((x1 + x2) / 2); + var cy = ((k * -ry * x) / rx) + ((y1 + y2) / 2); + + var f1 = asin(((y1 - cy) / ry).toFixed(9)); + var f2 = asin(((y2 - cy) / ry).toFixed(9)); + + f1 = ((x1 < cx) ? (PI - f1) : f1); + f2 = ((x2 < cx) ? (PI - f2) : f2); + + if (f1 < 0) f1 = (PI * 2) + f1; + if (f2 < 0) f2 = (PI * 2) + f2; + + if ((sweep_flag && f1) > f2) f1 = f1 - (PI * 2); + if ((!sweep_flag && f2) > f1) f2 = f2 - (PI * 2); + + } else { + f1 = recursive[0]; + f2 = recursive[1]; + cx = recursive[2]; + cy = recursive[3]; + } + + var df = f2 - f1; + + if (abs(df) > _120) { + var f2old = f2; + var x2old = x2; + var y2old = y2; + + f2 = f1 + (_120 * (((sweep_flag && f2) > f1) ? 1 : -1)); + x2 = cx + (rx * cos(f2)); + y2 = cy + (ry * sin(f2)); + + res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); + } + + df = f2 - f1; + + var c1 = cos(f1); + var s1 = sin(f1); + var c2 = cos(f2); + var s2 = sin(f2); + + var t = tan(df / 4); + + var hx = (4 / 3) * (rx * t); + var hy = (4 / 3) * (ry * t); + + var m1 = [x1, y1]; + var m2 = [x1 + (hx * s1), y1 - (hy * c1)]; + var m3 = [x2 + (hx * s2), y2 - (hy * c2)]; + var m4 = [x2, y2]; + + m2[0] = (2 * m1[0]) - m2[0]; + m2[1] = (2 * m1[1]) - m2[1]; + + if (recursive) { + return [m2, m3, m4].concat(res); + + } else { + res = [m2, m3, m4].concat(res).join().split(','); + + var newres = []; + var ii = res.length; + for (var i = 0; i < ii; i++) { + newres[i] = (i % 2) ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; + } + + return newres; + } + } + + function parsePathString(pathString) { + + if (!pathString) return null; + + var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 }; + var data = []; + + String(pathString).replace(pathCommand, function(a, b, c) { + + var params = []; + var name = b.toLowerCase(); + c.replace(pathValues, function(a, b) { + if (b) params.push(+b); + }); + + if ((name === 'm') && (params.length > 2)) { + data.push([b].concat(params.splice(0, 2))); + name = 'l'; + b = ((b === 'm') ? 'l' : 'L'); + } + + while (params.length >= paramCounts[name]) { + data.push([b].concat(params.splice(0, paramCounts[name]))); + if (!paramCounts[name]) break; + } + }); + + return data; + } + + function pathToAbsolute(pathArray) { + + if (!Array.isArray(pathArray) || !Array.isArray(pathArray && pathArray[0])) { // rough assumption + pathArray = parsePathString(pathArray); + } + + // if invalid string, return 'M 0 0' + if (!pathArray || !pathArray.length) return [['M', 0, 0]]; + + var res = []; + var x = 0; + var y = 0; + var mx = 0; + var my = 0; + var start = 0; + var pa0; + + var ii = pathArray.length; + for (var i = start; i < ii; i++) { + + var r = []; + res.push(r); + + var pa = pathArray[i]; + pa0 = pa[0]; + + if (pa0 != pa0.toUpperCase()) { + r[0] = pa0.toUpperCase(); + + var jj; + var j; + switch (r[0]) { + case 'A': + r[1] = pa[1]; + r[2] = pa[2]; + r[3] = pa[3]; + r[4] = pa[4]; + r[5] = pa[5]; + r[6] = +pa[6] + x; + r[7] = +pa[7] + y; + break; + + case 'V': + r[1] = +pa[1] + y; + break; + + case 'H': + r[1] = +pa[1] + x; + break; + + case 'M': + mx = +pa[1] + x; + my = +pa[2] + y; + + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + + default: + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + } + } else { + var kk = pa.length; + for (var k = 0; k < kk; k++) { + r[k] = pa[k]; + } + } + + switch (r[0]) { + case 'Z': + x = +mx; + y = +my; + break; + + case 'H': + x = r[1]; + break; + + case 'V': + y = r[1]; + break; + + case 'M': + mx = r[r.length - 2]; + my = r[r.length - 1]; + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + + default: + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + } + } + + return res; + } + + function normalize(path) { + + var p = pathToAbsolute(path); + var attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null }; + + function processPath(path, d, pcom) { + + var nx, ny; + + if (!path) return ['C', d.x, d.y, d.x, d.y, d.x, d.y]; + + if (!(path[0] in { T: 1, Q: 1 })) { + d.qx = null; + d.qy = null; + } + + switch (path[0]) { + case 'M': + d.X = path[1]; + d.Y = path[2]; + break; + + case 'A': + path = ['C'].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1)))); + break; + + case 'S': + if (pcom === 'C' || pcom === 'S') { // In 'S' case we have to take into account, if the previous command is C/S. + nx = (d.x * 2) - d.bx; // And reflect the previous + ny = (d.y * 2) - d.by; // command's control point relative to the current point. + } + else { // or some else or nothing + nx = d.x; + ny = d.y; + } + path = ['C', nx, ny].concat(path.slice(1)); + break; + + case 'T': + if (pcom === 'Q' || pcom === 'T') { // In 'T' case we have to take into account, if the previous command is Q/T. + d.qx = (d.x * 2) - d.qx; // And make a reflection similar + d.qy = (d.y * 2) - d.qy; // to case 'S'. + } + else { // or something else or nothing + d.qx = d.x; + d.qy = d.y; + } + path = ['C'].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); + break; + + case 'Q': + d.qx = path[1]; + d.qy = path[2]; + path = ['C'].concat(q2c(d.x, d.y, path[1], path[2], path[3], path[4])); + break; + + case 'H': + path = ['L'].concat(path[1], d.y); + break; + + case 'V': + path = ['L'].concat(d.x, path[1]); + break; + + // leave 'L' & 'Z' commands as they were: + + case 'L': + break; + + case 'Z': + break; + } + + return path; + } + + function fixArc(pp, i) { + + if (pp[i].length > 7) { + + pp[i].shift(); + var pi = pp[i]; + + while (pi.length) { + pcoms[i] = 'A'; // if created multiple 'C's, their original seg is saved + pp.splice(i++, 0, ['C'].concat(pi.splice(0, 6))); + } + + pp.splice(i, 1); + ii = p.length; + } + } + + var pcoms = []; // path commands of original path p + var pfirst = ''; // temporary holder for original path command + var pcom = ''; // holder for previous path command of original path + + var ii = p.length; + for (var i = 0; i < ii; i++) { + if (p[i]) pfirst = p[i][0]; // save current path command + + if (pfirst !== 'C') { // C is not saved yet, because it may be result of conversion + pcoms[i] = pfirst; // Save current path command + if (i > 0) pcom = pcoms[i - 1]; // Get previous path command pcom + } + + p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath + + if (pcoms[i] !== 'A' && pfirst === 'C') pcoms[i] = 'C'; // 'A' is the only command + // which may produce multiple 'C's + // so we have to make sure that 'C' is also 'C' in original path + + fixArc(p, i); // fixArc adds also the right amount of 'A's to pcoms + + var seg = p[i]; + var seglen = seg.length; + + attrs.x = seg[seglen - 2]; + attrs.y = seg[seglen - 1]; + + attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x; + attrs.by = parseFloat(seg[seglen - 3]) || attrs.y; + } + + // make sure normalized path data string starts with an M segment + if (!p[0][0] || p[0][0] !== 'M') { + p.unshift(['M', 0, 0]); + } + + return p; + } + + return function(pathData) { + return normalize(pathData).join(',').split(',').join(' '); + }; + })(); + + V.namespace = ns; + + return V; + +})(); + +// Global namespace. + +var joint = { + + version: '2.1.0', + + config: { + // The class name prefix config is for advanced use only. + // Be aware that if you change the prefix, the JointJS CSS will no longer function properly. + classNamePrefix: 'joint-', + defaultTheme: 'default' + }, + + // `joint.dia` namespace. + dia: {}, + + // `joint.ui` namespace. + ui: {}, + + // `joint.layout` namespace. + layout: {}, + + // `joint.shapes` namespace. + shapes: {}, + + // `joint.format` namespace. + format: {}, + + // `joint.connectors` namespace. + connectors: {}, + + // `joint.highlighters` namespace. + highlighters: {}, + + // `joint.routers` namespace. + routers: {}, + + // `joint.anchors` namespace. + anchors: {}, + + // `joint.connectionPoints` namespace. + connectionPoints: {}, + + // `joint.connectionStrategies` namespace. + connectionStrategies: {}, + + // `joint.linkTools` namespace. + linkTools: {}, + + // `joint.mvc` namespace. + mvc: { + views: {} + }, + + setTheme: function(theme, opt) { + + opt = opt || {}; + + joint.util.invoke(joint.mvc.views, 'setTheme', theme, opt); + + // Update the default theme on the view prototype. + joint.mvc.View.prototype.defaultTheme = theme; + }, + + // `joint.env` namespace. + env: { + + _results: {}, + + _tests: { + + svgforeignobject: function() { + return !!document.createElementNS && + /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); + } + }, + + addTest: function(name, fn) { + + return joint.env._tests[name] = fn; + }, + + test: function(name) { + + var fn = joint.env._tests[name]; + + if (!fn) { + throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); + } + + var result = joint.env._results[name]; + + if (typeof result !== 'undefined') { + return result; + } + + try { + result = fn(); + } catch (error) { + result = false; + } + + // Cache the test result. + joint.env._results[name] = result; + + return result; + } + }, + + util: { + + // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. + hashCode: function(str) { + + var hash = 0; + if (str.length == 0) return hash; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + hash = ((hash << 5) - hash) + c; + hash = hash & hash; // Convert to 32bit integer + } + return hash; + }, + + getByPath: function(obj, path, delim) { + + var keys = Array.isArray(path) ? path.slice() : path.split(delim || '/'); + var key; + + while (keys.length) { + key = keys.shift(); + if (Object(obj) === obj && key in obj) { + obj = obj[key]; + } else { + return undefined; + } + } + return obj; + }, + + setByPath: function(obj, path, value, delim) { + + var keys = Array.isArray(path) ? path : path.split(delim || '/'); + + var diver = obj; + var i = 0; + + for (var len = keys.length; i < len - 1; i++) { + // diver creates an empty object if there is no nested object under such a key. + // This means that one can populate an empty nested object with setByPath(). + diver = diver[keys[i]] || (diver[keys[i]] = {}); + } + diver[keys[len - 1]] = value; + + return obj; + }, + + unsetByPath: function(obj, path, delim) { + + delim = delim || '/'; + + var pathArray = Array.isArray(path) ? path.slice() : path.split(delim); + + var propertyToRemove = pathArray.pop(); + if (pathArray.length > 0) { + + // unsetting a nested attribute + var parent = joint.util.getByPath(obj, pathArray, delim); + + if (parent) { + delete parent[propertyToRemove]; + } + + } else { + + // unsetting a primitive attribute + delete obj[propertyToRemove]; + } + + return obj; + }, + + flattenObject: function(obj, delim, stop) { + + delim = delim || '/'; + var ret = {}; + + for (var key in obj) { + + if (!obj.hasOwnProperty(key)) continue; + + var shouldGoDeeper = typeof obj[key] === 'object'; + if (shouldGoDeeper && stop && stop(obj[key])) { + shouldGoDeeper = false; + } + + if (shouldGoDeeper) { + + var flatObject = this.flattenObject(obj[key], delim, stop); + + for (var flatKey in flatObject) { + if (!flatObject.hasOwnProperty(flatKey)) continue; + ret[key + delim + flatKey] = flatObject[flatKey]; + } + + } else { + + ret[key] = obj[key]; + } + } + + return ret; + }, + + uuid: function() { + + // credit: http://stackoverflow.com/posts/2117523/revisions + + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16|0; + var v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, + + // Generate global unique id for obj and store it as a property of the object. + guid: function(obj) { + + this.guid.id = this.guid.id || 1; + obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); + return obj.id; + }, + + toKebabCase: function(string) { + + return string.replace(/[A-Z]/g, '-$&').toLowerCase(); + }, + + // Copy all the properties to the first argument from the following arguments. + // All the properties will be overwritten by the properties from the following + // arguments. Inherited properties are ignored. + mixin: _.assign, + + // Copy all properties to the first argument from the following + // arguments only in case if they don't exists in the first argument. + // All the function propererties in the first argument will get + // additional property base pointing to the extenders same named + // property function's call method. + supplement: _.defaults, + + // Same as `mixin()` but deep version. + deepMixin: _.mixin, + + // Same as `supplement()` but deep version. + deepSupplement: _.defaultsDeep, + + normalizeEvent: function(evt) { + + var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; + if (touchEvt) { + for (var property in evt) { + // copy all the properties from the input event that are not + // defined on the touch event (functions included). + if (touchEvt[property] === undefined) { + touchEvt[property] = evt[property]; + } + } + return touchEvt; + } + + return evt; + }, + + nextFrame: (function() { + + var raf; + + if (typeof window !== 'undefined') { + + raf = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame; + } + + if (!raf) { + + var lastTime = 0; + + raf = function(callback) { + + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + + lastTime = currTime + timeToCall; + + return id; + }; + } + + return function(callback, context) { + return context + ? raf(callback.bind(context)) + : raf(callback); + }; + + })(), + + cancelFrame: (function() { + + var caf; + var client = typeof window != 'undefined'; + + if (client) { + + caf = window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.webkitCancelRequestAnimationFrame || + window.msCancelAnimationFrame || + window.msCancelRequestAnimationFrame || + window.oCancelAnimationFrame || + window.oCancelRequestAnimationFrame || + window.mozCancelAnimationFrame || + window.mozCancelRequestAnimationFrame; + } + + caf = caf || clearTimeout; + + return client ? caf.bind(window) : caf; + + })(), + + // ** Deprecated ** + shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { + + var bbox; + var spot; + + if (!magnet) { + + // There is no magnet, try to make the best guess what is the + // wrapping SVG element. This is because we want this "smart" + // connection points to work out of the box without the + // programmer to put magnet marks to any of the subelements. + // For example, we want the functoin to work on basic.Path elements + // without any special treatment of such elements. + // The code below guesses the wrapping element based on + // one simple assumption. The wrapping elemnet is the + // first child of the scalable group if such a group exists + // or the first child of the rotatable group if not. + // This makese sense because usually the wrapping element + // is below any other sub element in the shapes. + var scalable = view.$('.scalable')[0]; + var rotatable = view.$('.rotatable')[0]; + + if (scalable && scalable.firstChild) { + + magnet = scalable.firstChild; + + } else if (rotatable && rotatable.firstChild) { + + magnet = rotatable.firstChild; + } + } + + if (magnet) { + + spot = V(magnet).findIntersection(reference, linkView.paper.viewport); + if (!spot) { + bbox = V(magnet).getBBox({ target: linkView.paper.viewport }); + } + + } else { + + bbox = view.model.getBBox(); + spot = bbox.intersectionWithLineFromCenterToPoint(reference); + } + return spot || bbox.center(); + }, + + isPercentage: function(val) { + + return joint.util.isString(val) && val.slice(-1) === '%'; + }, + + parseCssNumeric: function(strValue, restrictUnits) { + + restrictUnits = restrictUnits || []; + var cssNumeric = { value: parseFloat(strValue) }; + + if (Number.isNaN(cssNumeric.value)) { + return null; + } + + var validUnitsExp = restrictUnits.join('|'); + + if (joint.util.isString(strValue)) { + var matches = new RegExp('(\\d+)(' + validUnitsExp + ')$').exec(strValue); + if (!matches) { + return null; + } + if (matches[2]) { + cssNumeric.unit = matches[2]; + } + } + return cssNumeric; + }, + + breakText: function(text, size, styles, opt) { + + opt = opt || {}; + styles = styles || {}; + + var width = size.width; + var height = size.height; + + var svgDocument = opt.svgDocument || V('svg').node; + var textSpan = V('tspan').node; + var textElement = V('text').attr(styles).append(textSpan).node; + var textNode = document.createTextNode(''); + + // Prevent flickering + textElement.style.opacity = 0; + // Prevent FF from throwing an uncaught exception when `getBBox()` + // called on element that is not in the render tree (is not measurable). + // .getComputedTextLength() returns always 0 in this case. + // Note that the `textElement` resp. `textSpan` can become hidden + // when it's appended to the DOM and a `display: none` CSS stylesheet + // rule gets applied. + textElement.style.display = 'block'; + textSpan.style.display = 'block'; + + textSpan.appendChild(textNode); + svgDocument.appendChild(textElement); + + if (!opt.svgDocument) { + + document.body.appendChild(svgDocument); + } + + var separator = opt.separator || ' '; + var eol = opt.eol || '\n'; + + var words = text.split(separator); + var full = []; + var lines = []; + var p; + var lineHeight; + + for (var i = 0, l = 0, len = words.length; i < len; i++) { + + var word = words[i]; + + if (!word) continue; + + if (eol && word.indexOf(eol) >= 0) { + // word cotains end-of-line character + if (word.length > 1) { + // separate word and continue cycle + var eolWords = word.split(eol); + for (var j = 0, jl = eolWords.length - 1; j < jl; j++) { + eolWords.splice(2 * j + 1, 0, eol); + } + Array.prototype.splice.apply(words, [i, 1].concat(eolWords)); + i--; + len += eolWords.length - 1; + } else { + // creates new line + l++; + } + continue; + } + + + textNode.data = lines[l] ? lines[l] + ' ' + word : word; + + if (textSpan.getComputedTextLength() <= width) { + + // the current line fits + lines[l] = textNode.data; + + if (p) { + // We were partitioning. Put rest of the word onto next line + full[l++] = true; + + // cancel partitioning + p = 0; + } + + } else { + + if (!lines[l] || p) { + + var partition = !!p; + + p = word.length - 1; + + if (partition || !p) { + + // word has only one character. + if (!p) { + + if (!lines[l]) { + + // we won't fit this text within our rect + lines = []; + + break; + } + + // partitioning didn't help on the non-empty line + // try again, but this time start with a new line + + // cancel partitions created + words.splice(i, 2, word + words[i + 1]); + + // adjust word length + len--; + + full[l++] = true; + i--; + + continue; + } + + // move last letter to the beginning of the next word + words[i] = word.substring(0, p); + words[i + 1] = word.substring(p) + words[i + 1]; + + } else { + + // We initiate partitioning + // split the long word into two words + words.splice(i, 1, word.substring(0, p), word.substring(p)); + + // adjust words length + len++; + + if (l && !full[l - 1]) { + // if the previous line is not full, try to fit max part of + // the current word there + l--; + } + } + + i--; + + continue; + } + + l++; + i--; + } + + // if size.height is defined we have to check whether the height of the entire + // text exceeds the rect height + if (height !== undefined) { + + if (lineHeight === undefined) { + + var heightValue; + + // use the same defaults as in V.prototype.text + if (styles.lineHeight === 'auto') { + heightValue = { value: 1.5, unit: 'em' }; + } else { + heightValue = joint.util.parseCssNumeric(styles.lineHeight, ['em']) || { value: 1, unit: 'em' }; + } + + lineHeight = heightValue.value; + if (heightValue.unit === 'em' ) { + lineHeight *= textElement.getBBox().height; + } + } + + if (lineHeight * lines.length > height) { + + // remove overflowing lines + lines.splice(Math.floor(height / lineHeight)); + + break; + } + } + } + + if (opt.svgDocument) { + + // svg document was provided, remove the text element only + svgDocument.removeChild(textElement); + + } else { + + // clean svg document + document.body.removeChild(svgDocument); + } + + return lines.join(eol); + }, + + // Sanitize HTML + // Based on https://gist.github.com/ufologist/5a0da51b2b9ef1b861c30254172ac3c9 + // Parses a string into an array of DOM nodes. + // Then outputs it back as a string. + sanitizeHTML: function(html) { + + // Ignores tags that are invalid inside a
tag (e.g. , ) + + // If documentContext (second parameter) is not specified or given as `null` or `undefined`, a new document is used. + // Inline events will not execute when the HTML is parsed; this includes, for example, sending GET requests for images. + + // If keepScripts (last parameter) is `false`, scripts are not executed. + var output = $($.parseHTML('
' + html + '
', null, false)); + + output.find('*').each(function() { // for all nodes + var currentNode = this; + + $.each(currentNode.attributes, function() { // for all attributes in each node + var currentAttribute = this; + + var attrName = currentAttribute.name; + var attrValue = currentAttribute.value; + + // Remove attribute names that start with "on" (e.g. onload, onerror...). + // Remove attribute values that start with "javascript:" pseudo protocol (e.g. `href="javascript:alert(1)"`). + if (attrName.indexOf('on') === 0 || attrValue.indexOf('javascript:') === 0) { + $(currentNode).removeAttr(attrName); + } + }); + }); + + return output.html(); + }, + + // Download `blob` as file with `fileName`. + // Does not work in IE9. + downloadBlob: function(blob, fileName) { + + if (window.navigator.msSaveBlob) { // requires IE 10+ + // pulls up a save dialog + window.navigator.msSaveBlob(blob, fileName); + + } else { // other browsers + // downloads directly in Chrome and Safari + + // presents a save/open dialog in Firefox + // Firefox bug: `from` field in save dialog always shows `from:blob:` + // https://bugzilla.mozilla.org/show_bug.cgi?id=1053327 + + var url = window.URL.createObjectURL(blob); + var link = document.createElement('a'); + + link.href = url; + link.download = fileName; + document.body.appendChild(link); + + link.click(); + + document.body.removeChild(link); + window.URL.revokeObjectURL(url); // mark the url for garbage collection + } + }, + + // Download `dataUri` as file with `fileName`. + // Does not work in IE9. + downloadDataUri: function(dataUri, fileName) { + + var blob = joint.util.dataUriToBlob(dataUri); + joint.util.downloadBlob(blob, fileName); + }, + + // Convert an uri-encoded data component (possibly also base64-encoded) to a blob. + dataUriToBlob: function(dataUri) { + + // first, make sure there are no newlines in the data uri + dataUri = dataUri.replace(/\s/g, ''); + dataUri = decodeURIComponent(dataUri); + + var firstCommaIndex = dataUri.indexOf(','); // split dataUri as `dataTypeString`,`data` + + var dataTypeString = dataUri.slice(0, firstCommaIndex); // e.g. 'data:image/jpeg;base64' + var mimeString = dataTypeString.split(':')[1].split(';')[0]; // e.g. 'image/jpeg' + + var data = dataUri.slice(firstCommaIndex + 1); + var decodedString; + if (dataTypeString.indexOf('base64') >= 0) { // data may be encoded in base64 + decodedString = atob(data); // decode data + } else { + // convert the decoded string to UTF-8 + decodedString = unescape(encodeURIComponent(data)); + } + // write the bytes of the string to a typed array + var ia = new window.Uint8Array(decodedString.length); + for (var i = 0; i < decodedString.length; i++) { + ia[i] = decodedString.charCodeAt(i); + } + + return new Blob([ia], { type: mimeString }); // return the typed array as Blob + }, + + // Read an image at `url` and return it as base64-encoded data uri. + // The mime type of the image is inferred from the `url` file extension. + // If data uri is provided as `url`, it is returned back unchanged. + // `callback` is a method with `err` as first argument and `dataUri` as second argument. + // Works with IE9. + imageToDataUri: function(url, callback) { + + if (!url || url.substr(0, 'data:'.length) === 'data:') { + // No need to convert to data uri if it is already in data uri. + + // This not only convenient but desired. For example, + // IE throws a security error if data:image/svg+xml is used to render + // an image to the canvas and an attempt is made to read out data uri. + // Now if our image is already in data uri, there is no need to render it to the canvas + // and so we can bypass this error. + + // Keep the async nature of the function. + return setTimeout(function() { + callback(null, url); + }, 0); + } + + // chrome, IE10+ + var modernHandler = function(xhr, callback) { + + if (xhr.status === 200) { + + var reader = new FileReader(); + + reader.onload = function(evt) { + var dataUri = evt.target.result; + callback(null, dataUri); + }; + + reader.onerror = function() { + callback(new Error('Failed to load image ' + url)); + }; + + reader.readAsDataURL(xhr.response); + } else { + callback(new Error('Failed to load image ' + url)); + } + }; + + var legacyHandler = function(xhr, callback) { + + var Uint8ToString = function(u8a) { + var CHUNK_SZ = 0x8000; + var c = []; + for (var i = 0; i < u8a.length; i += CHUNK_SZ) { + c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ))); + } + return c.join(''); + }; + + if (xhr.status === 200) { + + var bytes = new Uint8Array(xhr.response); + + var suffix = (url.split('.').pop()) || 'png'; + var map = { + 'svg': 'svg+xml' + }; + var meta = 'data:image/' + (map[suffix] || suffix) + ';base64,'; + var b64encoded = meta + btoa(Uint8ToString(bytes)); + callback(null, b64encoded); + } else { + callback(new Error('Failed to load image ' + url)); + } + }; + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url, true); + xhr.addEventListener('error', function() { + callback(new Error('Failed to load image ' + url)); + }); + + xhr.responseType = window.FileReader ? 'blob' : 'arraybuffer'; + + xhr.addEventListener('load', function() { + if (window.FileReader) { + modernHandler(xhr, callback); + } else { + legacyHandler(xhr, callback); + } + }); + + xhr.send(); + }, + + getElementBBox: function(el) { + + var $el = $(el); + if ($el.length === 0) { + throw new Error('Element not found') + } + + var element = $el[0]; + var doc = element.ownerDocument; + var clientBBox = element.getBoundingClientRect(); + + var strokeWidthX = 0; + var strokeWidthY = 0; + + // Firefox correction + if (element.ownerSVGElement) { + + var vel = V(element); + var bbox = vel.getBBox({ target: vel.svg() }); + + // if FF getBoundingClientRect includes stroke-width, getBBox doesn't. + // To unify this across all browsers we need to adjust the final bBox with `stroke-width` value. + strokeWidthX = (clientBBox.width - bbox.width); + strokeWidthY = (clientBBox.height - bbox.height); + } + + return { + x: clientBBox.left + window.pageXOffset - doc.documentElement.offsetLeft + strokeWidthX / 2, + y: clientBBox.top + window.pageYOffset - doc.documentElement.offsetTop + strokeWidthY / 2, + width: clientBBox.width - strokeWidthX, + height: clientBBox.height - strokeWidthY + }; + }, + + + // Highly inspired by the jquery.sortElements plugin by Padolsey. + // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. + sortElements: function(elements, comparator) { + + var $elements = $(elements); + var placements = $elements.map(function() { + + var sortElement = this; + var parentNode = sortElement.parentNode; + // Since the element itself will change position, we have + // to have some way of storing it's original position in + // the DOM. The easiest way is to have a 'flag' node: + var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); + + return function() { + + if (parentNode === this) { + throw new Error('You can\'t sort elements if any one is a descendant of another.'); + } + + // Insert before flag: + parentNode.insertBefore(this, nextSibling); + // Remove flag: + parentNode.removeChild(nextSibling); + }; + }); + + return Array.prototype.sort.call($elements, comparator).each(function(i) { + placements[i].call(this); + }); + }, + + // Sets attributes on the given element and its descendants based on the selector. + // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} + setAttributesBySelector: function(element, attrs) { + + var $element = $(element); + + joint.util.forIn(attrs, function(attrs, selector) { + var $elements = $element.find(selector).addBack().filter(selector); + // Make a special case for setting classes. + // We do not want to overwrite any existing class. + if (joint.util.has(attrs, 'class')) { + $elements.addClass(attrs['class']); + attrs = joint.util.omit(attrs, 'class'); + } + $elements.attr(attrs); + }); + }, + + // Return a new object with all four sides (top, right, bottom, left) in it. + // Value of each side is taken from the given argument (either number or object). + // Default value for a side is 0. + // Examples: + // joint.util.normalizeSides(5) --> { top: 5, right: 5, bottom: 5, left: 5 } + // joint.util.normalizeSides({ horizontal: 5 }) --> { top: 0, right: 5, bottom: 0, left: 5 } + // joint.util.normalizeSides({ left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 } + // joint.util.normalizeSides({ horizontal: 10, left: 5 }) --> { top: 0, right: 10, bottom: 0, left: 5 } + // joint.util.normalizeSides({ horizontal: 0, left: 5 }) --> { top: 0, right: 0, bottom: 0, left: 5 } + normalizeSides: function(box) { + + if (Object(box) !== box) { // `box` is not an object + var val = 0; // `val` left as 0 if `box` cannot be understood as finite number + if (isFinite(box)) val = +box; // actually also accepts string numbers (e.g. '100') + + return { top: val, right: val, bottom: val, left: val }; + } + + // `box` is an object + var top, right, bottom, left; + top = right = bottom = left = 0; + + if (isFinite(box.vertical)) top = bottom = +box.vertical; + if (isFinite(box.horizontal)) right = left = +box.horizontal; + + if (isFinite(box.top)) top = +box.top; // overwrite vertical + if (isFinite(box.right)) right = +box.right; // overwrite horizontal + if (isFinite(box.bottom)) bottom = +box.bottom; // overwrite vertical + if (isFinite(box.left)) left = +box.left; // overwrite horizontal + + return { top: top, right: right, bottom: bottom, left: left }; + }, + + timing: { + + linear: function(t) { + return t; + }, + + quad: function(t) { + return t * t; + }, + + cubic: function(t) { + return t * t * t; + }, + + inout: function(t) { + if (t <= 0) return 0; + if (t >= 1) return 1; + var t2 = t * t; + var t3 = t2 * t; + return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); + }, + + exponential: function(t) { + return Math.pow(2, 10 * (t - 1)); + }, + + bounce: function(t) { + for (var a = 0, b = 1; 1; a += b, b /= 2) { + if (t >= (7 - 4 * a) / 11) { + var q = (11 - 6 * a - 11 * t) / 4; + return -q * q + b * b; + } + } + }, + + reverse: function(f) { + return function(t) { + return 1 - f(1 - t); + }; + }, + + reflect: function(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); + }; + }, + + clamp: function(f, n, x) { + n = n || 0; + x = x || 1; + return function(t) { + var r = f(t); + return r < n ? n : r > x ? x : r; + }; + }, + + back: function(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + }, + + elastic: function(x) { + if (!x) x = 1.5; + return function(t) { + return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); + }; + } + }, + + interpolate: { + + number: function(a, b) { + var d = b - a; + return function(t) { return a + d * t; }; + }, + + object: function(a, b) { + var s = Object.keys(a); + return function(t) { + var i, p; + var r = {}; + for (i = s.length - 1; i != -1; i--) { + p = s[i]; + r[p] = a[p] + (b[p] - a[p]) * t; + } + return r; + }; + }, + + hexColor: function(a, b) { + + var ca = parseInt(a.slice(1), 16); + var cb = parseInt(b.slice(1), 16); + var ra = ca & 0x0000ff; + var rd = (cb & 0x0000ff) - ra; + var ga = ca & 0x00ff00; + var gd = (cb & 0x00ff00) - ga; + var ba = ca & 0xff0000; + var bd = (cb & 0xff0000) - ba; + + return function(t) { + + var r = (ra + rd * t) & 0x000000ff; + var g = (ga + gd * t) & 0x0000ff00; + var b = (ba + bd * t) & 0x00ff0000; + + return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); + }; + }, + + unit: function(a, b) { + + var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; + var ma = r.exec(a); + var mb = r.exec(b); + var p = mb[1].indexOf('.'); + var f = p > 0 ? mb[1].length - p - 1 : 0; + a = +ma[1]; + var d = +mb[1] - a; + var u = ma[2]; + + return function(t) { + return (a + d * t).toFixed(f) + u; + }; + } + }, + + // SVG filters. + filter: { + + // `color` ... outline color + // `width`... outline width + // `opacity` ... outline opacity + // `margin` ... gap between outline and the element + outline: function(args) { + + var tpl = ''; + + var margin = Number.isFinite(args.margin) ? args.margin : 2; + var width = Number.isFinite(args.width) ? args.width : 1; + + return joint.util.template(tpl)({ + color: args.color || 'blue', + opacity: Number.isFinite(args.opacity) ? args.opacity : 1, + outerRadius: margin + width, + innerRadius: margin + }); + }, + + // `color` ... color + // `width`... width + // `blur` ... blur + // `opacity` ... opacity + highlight: function(args) { + + var tpl = ''; + + return joint.util.template(tpl)({ + color: args.color || 'red', + width: Number.isFinite(args.width) ? args.width : 1, + blur: Number.isFinite(args.blur) ? args.blur : 0, + opacity: Number.isFinite(args.opacity) ? args.opacity : 1 + }); + }, + + // `x` ... horizontal blur + // `y` ... vertical blur (optional) + blur: function(args) { + + var x = Number.isFinite(args.x) ? args.x : 2; + + return joint.util.template('')({ + stdDeviation: Number.isFinite(args.y) ? [x, args.y] : x + }); + }, + + // `dx` ... horizontal shift + // `dy` ... vertical shift + // `blur` ... blur + // `color` ... color + // `opacity` ... opacity + dropShadow: function(args) { + + var tpl = 'SVGFEDropShadowElement' in window + ? '' + : ''; + + return joint.util.template(tpl)({ + dx: args.dx || 0, + dy: args.dy || 0, + opacity: Number.isFinite(args.opacity) ? args.opacity : 1, + color: args.color || 'black', + blur: Number.isFinite(args.blur) ? args.blur : 4 + }); + }, + + // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. + grayscale: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + a: 0.2126 + 0.7874 * (1 - amount), + b: 0.7152 - 0.7152 * (1 - amount), + c: 0.0722 - 0.0722 * (1 - amount), + d: 0.2126 - 0.2126 * (1 - amount), + e: 0.7152 + 0.2848 * (1 - amount), + f: 0.0722 - 0.0722 * (1 - amount), + g: 0.2126 - 0.2126 * (1 - amount), + h: 0.0722 + 0.9278 * (1 - amount) + }); + }, + + // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. + sepia: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + a: 0.393 + 0.607 * (1 - amount), + b: 0.769 - 0.769 * (1 - amount), + c: 0.189 - 0.189 * (1 - amount), + d: 0.349 - 0.349 * (1 - amount), + e: 0.686 + 0.314 * (1 - amount), + f: 0.168 - 0.168 * (1 - amount), + g: 0.272 - 0.272 * (1 - amount), + h: 0.534 - 0.534 * (1 - amount), + i: 0.131 + 0.869 * (1 - amount) + }); + }, + + // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. + saturate: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + amount: 1 - amount + }); + }, + + // `angle` ... the number of degrees around the color circle the input samples will be adjusted. + hueRotate: function(args) { + + return joint.util.template('')({ + angle: args.angle || 0 + }); + }, + + // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. + invert: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + amount: amount, + amount2: 1 - amount + }); + }, + + // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + brightness: function(args) { + + return joint.util.template('')({ + amount: Number.isFinite(args.amount) ? args.amount : 1 + }); + }, + + // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. + contrast: function(args) { + + var amount = Number.isFinite(args.amount) ? args.amount : 1; + + return joint.util.template('')({ + amount: amount, + amount2: .5 - amount / 2 + }); + } + }, + + format: { + + // Formatting numbers via the Python Format Specification Mini-language. + // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. + // Heavilly inspired by the D3.js library implementation. + number: function(specifier, value, locale) { + + locale = locale || { + + currency: ['$', ''], + decimal: '.', + thousands: ',', + grouping: [3] + }; + + // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. + // [[fill]align][sign][symbol][0][width][,][.precision][type] + var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + + var match = re.exec(specifier); + var fill = match[1] || ' '; + var align = match[2] || '>'; + var sign = match[3] || ''; + var symbol = match[4] || ''; + var zfill = match[5]; + var width = +match[6]; + var comma = match[7]; + var precision = match[8]; + var type = match[9]; + var scale = 1; + var prefix = ''; + var suffix = ''; + var integer = false; + + if (precision) precision = +precision.substring(1); + + if (zfill || fill === '0' && align === '=') { + zfill = fill = '0'; + align = '='; + if (comma) width -= Math.floor((width - 1) / 4); + } + + switch (type) { + case 'n': + comma = true; type = 'g'; + break; + case '%': + scale = 100; suffix = '%'; type = 'f'; + break; + case 'p': + scale = 100; suffix = '%'; type = 'r'; + break; + case 'b': + case 'o': + case 'x': + case 'X': + if (symbol === '#') prefix = '0' + type.toLowerCase(); + break; + case 'c': + case 'd': + integer = true; precision = 0; + break; + case 's': + scale = -1; type = 'r'; + break; + } + + if (symbol === '$') { + prefix = locale.currency[0]; + suffix = locale.currency[1]; + } + + // If no precision is specified for `'r'`, fallback to general notation. + if (type == 'r' && !precision) type = 'g'; + + // Ensure that the requested precision is in the supported range. + if (precision != null) { + if (type == 'g') precision = Math.max(1, Math.min(21, precision)); + else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); + } + + var zcomma = zfill && comma; + + // Return the empty string for floats formatted as ints. + if (integer && (value % 1)) return ''; + + // Convert negative to positive, and record the sign prefix. + var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; + + var fullSuffix = suffix; + + // Apply the scale, computing it from the value's exponent for si format. + // Preserve the existing suffix, if any, such as the currency symbol. + if (scale < 0) { + var unit = this.prefix(value, precision); + value = unit.scale(value); + fullSuffix = unit.symbol + suffix; + } else { + value *= scale; + } + + // Convert to the desired precision. + value = this.convert(type, value, precision); + + // Break the value into the integer part (before) and decimal part (after). + var i = value.lastIndexOf('.'); + var before = i < 0 ? value : value.substring(0, i); + var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); + + function formatGroup(value) { + + var i = value.length; + var t = []; + var j = 0; + var g = locale.grouping[0]; + while (i > 0 && g > 0) { + t.push(value.substring(i -= g, i + g)); + g = locale.grouping[j = (j + 1) % locale.grouping.length]; + } + return t.reverse().join(locale.thousands); + } + + // If the fill character is not `'0'`, grouping is applied before padding. + if (!zfill && comma && locale.grouping) { + + before = formatGroup(before); + } + + var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); + var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; + + // If the fill character is `'0'`, grouping is applied after padding. + if (zcomma) before = formatGroup(padding + before); + + // Apply prefix. + negative += prefix; + + // Rejoin integer and decimal parts. + value = before + after; + + return (align === '<' ? negative + value + padding + : align === '>' ? padding + negative + value + : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) + : negative + (zcomma ? value : padding + value)) + fullSuffix; + }, + + // Formatting string via the Python Format string. + // See https://docs.python.org/2/library/string.html#format-string-syntax) + string: function(formatString, value) { + + var fieldDelimiterIndex; + var fieldDelimiter = '{'; + var endPlaceholder = false; + var formattedStringArray = []; + + while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { + + var pieceFormatedString, formatSpec, fieldName; + + pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); + + if (endPlaceholder) { + formatSpec = pieceFormatedString.split(':'); + fieldName = formatSpec.shift().split('.'); + pieceFormatedString = value; + + for (var i = 0; i < fieldName.length; i++) + pieceFormatedString = pieceFormatedString[fieldName[i]]; + + if (formatSpec.length) + pieceFormatedString = this.number(formatSpec, pieceFormatedString); + } + + formattedStringArray.push(pieceFormatedString); + + formatString = formatString.slice(fieldDelimiterIndex + 1); + fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; + } + formattedStringArray.push(formatString); + + return formattedStringArray.join(''); + }, + + convert: function(type, value, precision) { + + switch (type) { + case 'b': return value.toString(2); + case 'c': return String.fromCharCode(value); + case 'o': return value.toString(8); + case 'x': return value.toString(16); + case 'X': return value.toString(16).toUpperCase(); + case 'g': return value.toPrecision(precision); + case 'e': return value.toExponential(precision); + case 'f': return value.toFixed(precision); + case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); + default: return value + ''; + } + }, + + round: function(value, precision) { + + return precision + ? Math.round(value * (precision = Math.pow(10, precision))) / precision + : Math.round(value); + }, + + precision: function(value, precision) { + + return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); + }, + + prefix: function(value, precision) { + + var prefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'].map(function(d, i) { + var k = Math.pow(10, Math.abs(8 - i) * 3); + return { + scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, + symbol: d + }; + }); + + var i = 0; + if (value) { + if (value < 0) value *= -1; + if (precision) value = this.round(value, this.precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); + } + return prefixes[8 + i / 3]; + } + }, + + /* + Pre-compile the HTML to be used as a template. + */ + template: function(html) { + + /* + Must support the variation in templating syntax found here: + https://lodash.com/docs#template + */ + var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; + + return function(data) { + + data = data || {}; + + return html.replace(regex, function(match) { + + var args = Array.from(arguments); + var attr = args.slice(1, 4).find(function(_attr) { + return !!_attr; + }); + + var attrArray = attr.split('.'); + var value = data[attrArray.shift()]; + + while (value !== undefined && attrArray.length) { + value = value[attrArray.shift()]; + } + + return value !== undefined ? value : ''; + }); + }; + }, + + /** + * @param {Element=} el Element, which content is intent to display in full-screen mode, 'window.top.document.body' is default. + */ + toggleFullScreen: function(el) { + + var topDocument = window.top.document; + el = el || topDocument.body; + + function prefixedResult(el, prop) { + + var prefixes = ['webkit', 'moz', 'ms', 'o', '']; + for (var i = 0; i < prefixes.length; i++) { + var prefix = prefixes[i]; + var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); + if (el[propName] !== undefined) { + return joint.util.isFunction(el[propName]) ? el[propName]() : el[propName]; + } + } + } + + if (prefixedResult(topDocument, 'FullscreenElement') || prefixedResult(topDocument, 'FullScreenElement')) { + prefixedResult(topDocument, 'ExitFullscreen') || // Spec. + prefixedResult(topDocument, 'CancelFullScreen'); // Firefox + } else { + prefixedResult(el, 'RequestFullscreen') || // Spec. + prefixedResult(el, 'RequestFullScreen'); // Firefox + } + }, + + addClassNamePrefix: function(className) { + + if (!className) return className; + + return className.toString().split(' ').map(function(_className) { + + if (_className.substr(0, joint.config.classNamePrefix.length) !== joint.config.classNamePrefix) { + _className = joint.config.classNamePrefix + _className; + } + + return _className; + + }).join(' '); + }, + + removeClassNamePrefix: function(className) { + + if (!className) return className; + + return className.toString().split(' ').map(function(_className) { + + if (_className.substr(0, joint.config.classNamePrefix.length) === joint.config.classNamePrefix) { + _className = _className.substr(joint.config.classNamePrefix.length); + } + + return _className; + + }).join(' '); + }, + + wrapWith: function(object, methods, wrapper) { + + if (joint.util.isString(wrapper)) { + + if (!joint.util.wrappers[wrapper]) { + throw new Error('Unknown wrapper: "' + wrapper + '"'); + } + + wrapper = joint.util.wrappers[wrapper]; + } + + if (!joint.util.isFunction(wrapper)) { + throw new Error('Wrapper must be a function.'); + } + + this.toArray(methods).forEach(function(method) { + object[method] = wrapper(object[method]); + }); + }, + + wrappers: { + + /* + Prepares a function with the following usage: + + fn([cell, cell, cell], opt); + fn([cell, cell, cell]); + fn(cell, cell, cell, opt); + fn(cell, cell, cell); + fn(cell); + */ + cells: function(fn) { + + return function() { + + var args = Array.from(arguments); + var n = args.length; + var cells = n > 0 && args[0] || []; + var opt = n > 1 && args[n - 1] || {}; + + if (!Array.isArray(cells)) { + + if (opt instanceof joint.dia.Cell) { + cells = args; + } else if (cells instanceof joint.dia.Cell) { + if (args.length > 1) { + args.pop(); + } + cells = args; + } + } + + if (opt instanceof joint.dia.Cell) { + opt = {}; + } + + return fn.call(this, cells, opt); + }; + } + }, + + parseDOMJSON: function(json, namespace) { + + var selectors = {}; + var svgNamespace = V.namespace.xmlns; + var ns = namespace || svgNamespace; + var fragment = document.createDocumentFragment(); + var queue = [json, fragment, ns]; + while (queue.length > 0) { + ns = queue.pop(); + var parentNode = queue.pop(); + var siblingsDef = queue.pop(); + for (var i = 0, n = siblingsDef.length; i < n; i++) { + var nodeDef = siblingsDef[i]; + // TagName + if (!nodeDef.hasOwnProperty('tagName')) throw new Error('json-dom-parser: missing tagName'); + var tagName = nodeDef.tagName; + // Namespace URI + if (nodeDef.hasOwnProperty('namespaceURI')) ns = nodeDef.namespaceURI; + var node = document.createElementNS(ns, tagName); + var svg = (ns === svgNamespace); + var wrapper = (svg) ? V : $; + // Attributes + var attributes = nodeDef.attributes; + if (attributes) wrapper(node).attr(attributes); + // Style + var style = nodeDef.style; + if (style) $(node).css(style); + // ClassName + if (nodeDef.hasOwnProperty('className')) { + var className = nodeDef.className; + if (svg) { + node.className.baseVal = className; + } else { + node.className = className; + } + } + // Selector + if (nodeDef.hasOwnProperty('selector')) { + var nodeSelector = nodeDef.selector; + if (selectors[nodeSelector]) throw new Error('json-dom-parser: selector must be unique'); + selectors[nodeSelector] = node; + wrapper(node).attr('joint-selector', nodeSelector); + } + parentNode.appendChild(node); + // Children + var childrenDef = nodeDef.children; + if (Array.isArray(childrenDef)) queue.push(childrenDef, node, ns); + } + } + return { + fragment: fragment, + selectors: selectors + } + }, + + // lodash 3 vs 4 incompatible + sortedIndex: _.sortedIndexBy || _.sortedIndex, + uniq: _.uniqBy || _.uniq, + uniqueId: _.uniqueId, + sortBy: _.sortBy, + isFunction: _.isFunction, + result: _.result, + union: _.union, + invoke: _.invokeMap || _.invoke, + difference: _.difference, + intersection: _.intersection, + omit: _.omit, + pick: _.pick, + has: _.has, + bindAll: _.bindAll, + assign: _.assign, + defaults: _.defaults, + defaultsDeep: _.defaultsDeep, + isPlainObject: _.isPlainObject, + isEmpty: _.isEmpty, + isEqual: _.isEqual, + noop: function() {}, + cloneDeep: _.cloneDeep, + toArray: _.toArray, + flattenDeep: _.flattenDeep, + camelCase: _.camelCase, + groupBy: _.groupBy, + forIn: _.forIn, + without: _.without, + debounce: _.debounce, + clone: _.clone, + + isBoolean: function(value) { + var toString = Object.prototype.toString; + return value === true || value === false || (!!value && typeof value === 'object' && toString.call(value) === '[object Boolean]'); + }, + + isObject: function(value) { + return !!value && (typeof value === 'object' || typeof value === 'function'); + }, + + isNumber: function(value) { + var toString = Object.prototype.toString; + return typeof value === 'number' || (!!value && typeof value === 'object' && toString.call(value) === '[object Number]'); + }, + + isString: function(value) { + var toString = Object.prototype.toString; + return typeof value === 'string' || (!!value && typeof value === 'object' && toString.call(value) === '[object String]'); + }, + + merge: function() { + if (_.mergeWith) { + var args = Array.from(arguments); + var last = args[args.length - 1]; + + var customizer = this.isFunction(last) ? last : this.noop; + args.push(function(a,b) { + var customResult = customizer(a, b); + if (customResult !== undefined) { + return customResult; + } + + if (Array.isArray(a) && !Array.isArray(b)) { + return b; + } + }); + + return _.mergeWith.apply(this, args) + } + return _.merge.apply(this, arguments); + } + } +}; + + +joint.mvc.View = Backbone.View.extend({ + + options: {}, + theme: null, + themeClassNamePrefix: joint.util.addClassNamePrefix('theme-'), + requireSetThemeOverride: false, + defaultTheme: joint.config.defaultTheme, + children: null, + childNodes: null, + + constructor: function(options) { + + this.requireSetThemeOverride = options && !!options.theme; + this.options = joint.util.assign({}, this.options, options); + + Backbone.View.call(this, options); + }, + + initialize: function(options) { + + joint.util.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); + + joint.mvc.views[this.cid] = this; + + this.setTheme(this.options.theme || this.defaultTheme); + this.init(); + }, + + renderChildren: function(children) { + children || (children = this.children); + if (children) { + var namespace = V.namespace[this.svgElement ? 'xmlns' : 'xhtml']; + var doc = joint.util.parseDOMJSON(children, namespace); + this.vel.empty().append(doc.fragment); + this.childNodes = doc.selectors; + } + return this; + }, + + // Override the Backbone `_ensureElement()` method in order to create an + // svg element (e.g., ``) node that wraps all the nodes of the Cell view. + // Expose class name setter as a separate method. + _ensureElement: function() { + if (!this.el) { + var tagName = joint.util.result(this, 'tagName'); + var attrs = joint.util.assign({}, joint.util.result(this, 'attributes')); + if (this.id) attrs.id = joint.util.result(this, 'id'); + this.setElement(this._createElement(tagName)); + this._setAttributes(attrs); + } else { + this.setElement(joint.util.result(this, 'el')); + } + this._ensureElClassName(); + }, + + _setAttributes: function(attrs) { + if (this.svgElement) { + this.vel.attr(attrs); + } else { + this.$el.attr(attrs); + } + }, + + _createElement: function(tagName) { + if (this.svgElement) { + return document.createElementNS(V.namespace.xmlns, tagName); + } else { + return document.createElement(tagName); + } + }, + + // Utilize an alternative DOM manipulation API by + // adding an element reference wrapped in Vectorizer. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + if (this.svgElement) this.vel = V(this.el); + }, + + _ensureElClassName: function() { + var className = joint.util.result(this, 'className'); + var prefixedClassName = joint.util.addClassNamePrefix(className); + // Note: className removal here kept for backwards compatibility only + if (this.svgElement) { + this.vel.removeClass(className).addClass(prefixedClassName); + } else { + this.$el.removeClass(className).addClass(prefixedClassName); + } + }, + + init: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, + + onRender: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, + + setTheme: function(theme, opt) { + + opt = opt || {}; + + // Theme is already set, override is required, and override has not been set. + // Don't set the theme. + if (this.theme && this.requireSetThemeOverride && !opt.override) { + return this; + } + + this.removeThemeClassName(); + this.addThemeClassName(theme); + this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); + this.theme = theme; + + return this; + }, + + addThemeClassName: function(theme) { + + theme = theme || this.theme; + + var className = this.themeClassNamePrefix + theme; + + if (this.svgElement) { + this.vel.addClass(className); + } else { + this.$el.addClass(className); + } + + return this; + }, + + removeThemeClassName: function(theme) { + + theme = theme || this.theme; + + var className = this.themeClassNamePrefix + theme; + + if (this.svgElement) { + this.vel.removeClass(className); + } else { + this.$el.removeClass(className); + } + + return this; + }, + + onSetTheme: function(oldTheme, newTheme) { + // Intentionally empty. + // This method is meant to be overriden. + }, + + remove: function() { + + this.onRemove(); + this.undelegateDocumentEvents(); + + joint.mvc.views[this.cid] = null; + + Backbone.View.prototype.remove.apply(this, arguments); + + return this; + }, + + onRemove: function() { + // Intentionally empty. + // This method is meant to be overriden. + }, + + getEventNamespace: function() { + // Returns a per-session unique namespace + return '.joint-event-ns-' + this.cid; + }, + + delegateElementEvents: function(element, events, data) { + if (!events) return this; + data || (data = {}); + var eventNS = this.getEventNamespace(); + for (var eventName in events) { + var method = events[eventName]; + if (typeof method !== 'function') method = this[method]; + if (!method) continue; + $(element).on(eventName + eventNS, data, method.bind(this)); + } + return this; + }, + + undelegateElementEvents: function(element) { + $(element).off(this.getEventNamespace()); + return this; + }, + + delegateDocumentEvents: function(events, data) { + events || (events = joint.util.result(this, 'documentEvents')); + return this.delegateElementEvents(document, events, data); + }, + + undelegateDocumentEvents: function() { + return this.undelegateElementEvents(document); + }, + + eventData: function(evt, data) { + if (!evt) throw new Error('eventData(): event object required.'); + var currentData = evt.data; + var key = '__' + this.cid + '__'; + if (data === undefined) { + if (!currentData) return {}; + return currentData[key] || {}; + } + currentData || (currentData = evt.data = {}); + currentData[key] || (currentData[key] = {}); + joint.util.assign(currentData[key], data); + return this; + } + +}, { + + extend: function() { + + var args = Array.from(arguments); + + // Deep clone the prototype and static properties objects. + // This prevents unexpected behavior where some properties are overwritten outside of this function. + var protoProps = args[0] && joint.util.assign({}, args[0]) || {}; + var staticProps = args[1] && joint.util.assign({}, args[1]) || {}; + + // Need the real render method so that we can wrap it and call it later. + var renderFn = protoProps.render || (this.prototype && this.prototype.render) || null; + + /* + Wrap the real render method so that: + .. `onRender` is always called. + .. `this` is always returned. + */ + protoProps.render = function() { + + if (renderFn) { + // Call the original render method. + renderFn.apply(this, arguments); + } + + // Should always call onRender() method. + this.onRender(); + + // Should always return itself. + return this; + }; + + return Backbone.View.extend.call(this, protoProps, staticProps); + } +}); + + + +joint.dia.GraphCells = Backbone.Collection.extend({ + + cellNamespace: joint.shapes, + + initialize: function(models, opt) { + + // Set the optional namespace where all model classes are defined. + if (opt.cellNamespace) { + this.cellNamespace = opt.cellNamespace; + } + + this.graph = opt.graph; + }, + + model: function(attrs, opt) { + + var collection = opt.collection; + var namespace = collection.cellNamespace; + + // Find the model class in the namespace or use the default one. + var ModelClass = (attrs.type === 'link') + ? joint.dia.Link + : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; + + var cell = new ModelClass(attrs, opt); + // Add a reference to the graph. It is necessary to do this here because this is the earliest place + // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. + if (!opt.dry) { + cell.graph = collection.graph; + } + + return cell; + }, + + // `comparator` makes it easy to sort cells based on their `z` index. + comparator: function(model) { + + return model.get('z') || 0; + } +}); + + +joint.dia.Graph = Backbone.Model.extend({ + + _batches: {}, + + initialize: function(attrs, opt) { + + opt = opt || {}; + + // Passing `cellModel` function in the options object to graph allows for + // setting models based on attribute objects. This is especially handy + // when processing JSON graphs that are in a different than JointJS format. + var cells = new joint.dia.GraphCells([], { + model: opt.cellModel, + cellNamespace: opt.cellNamespace, + graph: this + }); + Backbone.Model.prototype.set.call(this, 'cells', cells); + + // Make all the events fired in the `cells` collection available. + // to the outside world. + cells.on('all', this.trigger, this); + + // Backbone automatically doesn't trigger re-sort if models attributes are changed later when + // they're already in the collection. Therefore, we're triggering sort manually here. + this.on('change:z', this._sortOnChangeZ, this); + + // `joint.dia.Graph` keeps an internal data structure (an adjacency list) + // for fast graph queries. All changes that affect the structure of the graph + // must be reflected in the `al` object. This object provides fast answers to + // questions such as "what are the neighbours of this node" or "what + // are the sibling links of this link". + + // Outgoing edges per node. Note that we use a hash-table for the list + // of outgoing edges for a faster lookup. + // [node ID] -> Object [edge] -> true + this._out = {}; + // Ingoing edges per node. + // [node ID] -> Object [edge] -> true + this._in = {}; + // `_nodes` is useful for quick lookup of all the elements in the graph, without + // having to go through the whole cells array. + // [node ID] -> true + this._nodes = {}; + // `_edges` is useful for quick lookup of all the links in the graph, without + // having to go through the whole cells array. + // [edge ID] -> true + this._edges = {}; + + cells.on('add', this._restructureOnAdd, this); + cells.on('remove', this._restructureOnRemove, this); + cells.on('reset', this._restructureOnReset, this); + cells.on('change:source', this._restructureOnChangeSource, this); + cells.on('change:target', this._restructureOnChangeTarget, this); + cells.on('remove', this._removeCell, this); + }, + + _sortOnChangeZ: function() { + + this.get('cells').sort(); + }, + + _restructureOnAdd: function(cell) { + + if (cell.isLink()) { + this._edges[cell.id] = true; + var source = cell.get('source'); + var target = cell.get('target'); + if (source.id) { + (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; + } + if (target.id) { + (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; + } + } else { + this._nodes[cell.id] = true; + } + }, + + _restructureOnRemove: function(cell) { + + if (cell.isLink()) { + delete this._edges[cell.id]; + var source = cell.get('source'); + var target = cell.get('target'); + if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { + delete this._out[source.id][cell.id]; + } + if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { + delete this._in[target.id][cell.id]; + } + } else { + delete this._nodes[cell.id]; + } + }, + + _restructureOnReset: function(cells) { + + // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. + cells = cells.models; + + this._out = {}; + this._in = {}; + this._nodes = {}; + this._edges = {}; + + cells.forEach(this._restructureOnAdd, this); + }, + + _restructureOnChangeSource: function(link) { + + var prevSource = link.previous('source'); + if (prevSource.id && this._out[prevSource.id]) { + delete this._out[prevSource.id][link.id]; + } + var source = link.get('source'); + if (source.id) { + (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; + } + }, + + _restructureOnChangeTarget: function(link) { + + var prevTarget = link.previous('target'); + if (prevTarget.id && this._in[prevTarget.id]) { + delete this._in[prevTarget.id][link.id]; + } + var target = link.get('target'); + if (target.id) { + (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; + } + }, + + // Return all outbound edges for the node. Return value is an object + // of the form: [edge] -> true + getOutboundEdges: function(node) { + + return (this._out && this._out[node]) || {}; + }, + + // Return all inbound edges for the node. Return value is an object + // of the form: [edge] -> true + getInboundEdges: function(node) { + + return (this._in && this._in[node]) || {}; + }, + + toJSON: function() { + + // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. + // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. + var json = Backbone.Model.prototype.toJSON.apply(this, arguments); + json.cells = this.get('cells').toJSON(); + return json; + }, + + fromJSON: function(json, opt) { + + if (!json.cells) { + + throw new Error('Graph JSON must contain cells array.'); + } + + return this.set(json, opt); + }, + + set: function(key, val, opt) { + + var attrs; + + // Handle both `key`, value and {key: value} style arguments. + if (typeof key === 'object') { + attrs = key; + opt = val; + } else { + (attrs = {})[key] = val; + } + + // Make sure that `cells` attribute is handled separately via resetCells(). + if (attrs.hasOwnProperty('cells')) { + this.resetCells(attrs.cells, opt); + attrs = joint.util.omit(attrs, 'cells'); + } + + // The rest of the attributes are applied via original set method. + return Backbone.Model.prototype.set.call(this, attrs, opt); + }, + + clear: function(opt) { + + opt = joint.util.assign({}, opt, { clear: true }); + + var collection = this.get('cells'); + + if (collection.length === 0) return this; + + this.startBatch('clear', opt); + + // The elements come after the links. + var cells = collection.sortBy(function(cell) { + return cell.isLink() ? 1 : 2; + }); + + do { + + // Remove all the cells one by one. + // Note that all the links are removed first, so it's + // safe to remove the elements without removing the connected + // links first. + cells.shift().remove(opt); + + } while (cells.length > 0); + + this.stopBatch('clear'); + + return this; + }, + + _prepareCell: function(cell, opt) { + + var attrs; + if (cell instanceof Backbone.Model) { + attrs = cell.attributes; + if (!cell.graph && (!opt || !opt.dry)) { + // An element can not be member of more than one graph. + // A cell stops being the member of the graph after it's explicitely removed. + cell.graph = this; + } + } else { + // In case we're dealing with a plain JS object, we have to set the reference + // to the `graph` right after the actual model is created. This happens in the `model()` function + // of `joint.dia.GraphCells`. + attrs = cell; + } + + if (!joint.util.isString(attrs.type)) { + throw new TypeError('dia.Graph: cell type must be a string.'); + } + + return cell; + }, + + minZIndex: function() { + + var firstCell = this.get('cells').first(); + return firstCell ? (firstCell.get('z') || 0) : 0; + }, + + maxZIndex: function() { + + var lastCell = this.get('cells').last(); + return lastCell ? (lastCell.get('z') || 0) : 0; + }, + + addCell: function(cell, opt) { + + if (Array.isArray(cell)) { + + return this.addCells(cell, opt); + } + + if (cell instanceof Backbone.Model) { + + if (!cell.has('z')) { + cell.set('z', this.maxZIndex() + 1); + } + + } else if (cell.z === undefined) { + + cell.z = this.maxZIndex() + 1; + } + + this.get('cells').add(this._prepareCell(cell, opt), opt || {}); + + return this; + }, + + addCells: function(cells, opt) { + + if (cells.length) { + + cells = joint.util.flattenDeep(cells); + opt.position = cells.length; + + this.startBatch('add'); + cells.forEach(function(cell) { + opt.position--; + this.addCell(cell, opt); + }, this); + this.stopBatch('add'); + } + + return this; + }, + + // When adding a lot of cells, it is much more efficient to + // reset the entire cells collection in one go. + // Useful for bulk operations and optimizations. + resetCells: function(cells, opt) { + + var preparedCells = joint.util.toArray(cells).map(function(cell) { + return this._prepareCell(cell, opt); + }, this); + this.get('cells').reset(preparedCells, opt); + + return this; + }, + + removeCells: function(cells, opt) { + + if (cells.length) { + + this.startBatch('remove'); + joint.util.invoke(cells, 'remove', opt); + this.stopBatch('remove'); + } + + return this; + }, + + _removeCell: function(cell, collection, options) { + + options = options || {}; + + if (!options.clear) { + // Applications might provide a `disconnectLinks` option set to `true` in order to + // disconnect links when a cell is removed rather then removing them. The default + // is to remove all the associated links. + if (options.disconnectLinks) { + + this.disconnectLinks(cell, options); + + } else { + + this.removeLinks(cell, options); + } + } + // Silently remove the cell from the cells collection. Silently, because + // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is + // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events + // would be triggered on the graph model. + this.get('cells').remove(cell, { silent: true }); + + if (cell.graph === this) { + // Remove the element graph reference only if the cell is the member of this graph. + cell.graph = null; + } + }, + + // Get a cell by `id`. + getCell: function(id) { + + return this.get('cells').get(id); + }, + + getCells: function() { + + return this.get('cells').toArray(); + }, + + getElements: function() { + return Object.keys(this._nodes).map(this.getCell, this); + }, + + getLinks: function() { + return Object.keys(this._edges).map(this.getCell, this); + }, + + getFirstCell: function() { + + return this.get('cells').first(); + }, + + getLastCell: function() { + + return this.get('cells').last(); + }, + + // Get all inbound and outbound links connected to the cell `model`. + getConnectedLinks: function(model, opt) { + + opt = opt || {}; + + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } + + // The final array of connected link models. + var links = []; + // Connected edges. This hash table ([edge] -> true) serves only + // for a quick lookup to check if we already added a link. + var edges = {}; + + if (outbound) { + joint.util.forIn(this.getOutboundEdges(model.id), function(exists, edge) { + if (!edges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + if (inbound) { + joint.util.forIn(this.getInboundEdges(model.id), function(exists, edge) { + // Skip links that were already added. Those must be self-loop links + // because they are both inbound and outbond edges of the same element. + if (!edges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + + // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells + // and are not descendents themselves. + if (opt.deep) { + + var embeddedCells = model.getEmbeddedCells({ deep: true }); + // In the first round, we collect all the embedded edges so that we can exclude + // them from the final result. + var embeddedEdges = {}; + embeddedCells.forEach(function(cell) { + if (cell.isLink()) { + embeddedEdges[cell.id] = true; + } + }); + embeddedCells.forEach(function(cell) { + if (cell.isLink()) return; + if (outbound) { + joint.util.forIn(this.getOutboundEdges(cell.id), function(exists, edge) { + if (!edges[edge] && !embeddedEdges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + if (inbound) { + joint.util.forIn(this.getInboundEdges(cell.id), function(exists, edge) { + if (!edges[edge] && !embeddedEdges[edge]) { + links.push(this.getCell(edge)); + edges[edge] = true; + } + }.bind(this)); + } + }, this); + } + + return links; + }, + + getNeighbors: function(model, opt) { + + opt = opt || {}; + + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } + + var neighbors = this.getConnectedLinks(model, opt).reduce(function(res, link) { + + var source = link.get('source'); + var target = link.get('target'); + var loop = link.hasLoop(opt); + + // Discard if it is a point, or if the neighbor was already added. + if (inbound && joint.util.has(source, 'id') && !res[source.id]) { + + var sourceElement = this.getCell(source.id); + + if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { + res[source.id] = sourceElement; + } + } + + // Discard if it is a point, or if the neighbor was already added. + if (outbound && joint.util.has(target, 'id') && !res[target.id]) { + + var targetElement = this.getCell(target.id); + + if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { + res[target.id] = targetElement; + } + } + + return res; + }.bind(this), {}); + + return joint.util.toArray(neighbors); + }, + + getCommonAncestor: function(/* cells */) { + + var cellsAncestors = Array.from(arguments).map(function(cell) { + + var ancestors = []; + var parentId = cell.get('parent'); + + while (parentId) { + + ancestors.push(parentId); + parentId = this.getCell(parentId).get('parent'); + } + + return ancestors; + + }, this); + + cellsAncestors = cellsAncestors.sort(function(a, b) { + return a.length - b.length; + }); + + var commonAncestor = joint.util.toArray(cellsAncestors.shift()).find(function(ancestor) { + return cellsAncestors.every(function(cellAncestors) { + return cellAncestors.includes(ancestor); + }); + }); + + return this.getCell(commonAncestor); + }, + + // Find the whole branch starting at `element`. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. + getSuccessors: function(element, opt) { + + opt = opt || {}; + var res = []; + // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. + this.search(element, function(el) { + if (el !== element) { + res.push(el); + } + }, joint.util.assign({}, opt, { outbound: true })); + return res; + }, + + // Clone `cells` returning an object that maps the original cell ID to the clone. The number + // of clones is exactly the same as the `cells.length`. + // This function simply clones all the `cells`. However, it also reconstructs + // all the `source/target` and `parent/embed` references within the `cells`. + // This is the main difference from the `cell.clone()` method. The + // `cell.clone()` method works on one single cell only. + // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` + // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. + // the source and target of the link `L2` is changed to point to `A2` and `B2`. + cloneCells: function(cells) { + + cells = joint.util.uniq(cells); + + // A map of the form [original cell ID] -> [clone] helping + // us to reconstruct references for source/target and parent/embeds. + // This is also the returned value. + var cloneMap = joint.util.toArray(cells).reduce(function(map, cell) { + map[cell.id] = cell.clone(); + return map; + }, {}); + + joint.util.toArray(cells).forEach(function(cell) { + + var clone = cloneMap[cell.id]; + // assert(clone exists) + + if (clone.isLink()) { + var source = clone.get('source'); + var target = clone.get('target'); + if (source.id && cloneMap[source.id]) { + // Source points to an element and the element is among the clones. + // => Update the source of the cloned link. + clone.prop('source/id', cloneMap[source.id].id); + } + if (target.id && cloneMap[target.id]) { + // Target points to an element and the element is among the clones. + // => Update the target of the cloned link. + clone.prop('target/id', cloneMap[target.id].id); + } + } + + // Find the parent of the original cell + var parent = cell.get('parent'); + if (parent && cloneMap[parent]) { + clone.set('parent', cloneMap[parent].id); + } + + // Find the embeds of the original cell + var embeds = joint.util.toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) { + // Embedded cells that are not being cloned can not be carried + // over with other embedded cells. + if (cloneMap[embed]) { + newEmbeds.push(cloneMap[embed].id); + } + return newEmbeds; + }, []); + + if (!joint.util.isEmpty(embeds)) { + clone.set('embeds', embeds); + } + }); + + return cloneMap; + }, + + // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). + // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. + // Return a map of the form: [original cell ID] -> [clone]. + cloneSubgraph: function(cells, opt) { + + var subgraph = this.getSubgraph(cells, opt); + return this.cloneCells(subgraph); + }, + + // Return `cells` and all the connected links that connect cells in the `cells` array. + // If `opt.deep` is `true`, return all the cells including all their embedded cells + // and all the links that connect any of the returned cells. + // For example, for a single shallow element, the result is that very same element. + // For two elements connected with a link: `A --- L ---> B`, the result for + // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. + getSubgraph: function(cells, opt) { + + opt = opt || {}; + + var subgraph = []; + // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. + var cellMap = {}; + var elements = []; + var links = []; joint.util.toArray(cells).forEach(function(cell) { + if (!cellMap[cell.id]) { + subgraph.push(cell); + cellMap[cell.id] = cell; + if (cell.isLink()) { + links.push(cell); + } else { + elements.push(cell); + } + } + + if (opt.deep) { + var embeds = cell.getEmbeddedCells({ deep: true }); + embeds.forEach(function(embed) { + if (!cellMap[embed.id]) { + subgraph.push(embed); + cellMap[embed.id] = embed; + if (embed.isLink()) { + links.push(embed); + } else { + elements.push(embed); + } + } + }); + } + }); + + links.forEach(function(link) { + // For links, return their source & target (if they are elements - not points). + var source = link.get('source'); + var target = link.get('target'); + if (source.id && !cellMap[source.id]) { + var sourceElement = this.getCell(source.id); + subgraph.push(sourceElement); + cellMap[sourceElement.id] = sourceElement; + elements.push(sourceElement); + } + if (target.id && !cellMap[target.id]) { + var targetElement = this.getCell(target.id); + subgraph.push(this.getCell(target.id)); + cellMap[targetElement.id] = targetElement; + elements.push(targetElement); + } + }, this); + + elements.forEach(function(element) { + // For elements, include their connected links if their source/target is in the subgraph; + var links = this.getConnectedLinks(element, opt); + links.forEach(function(link) { + var source = link.get('source'); + var target = link.get('target'); + if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { + subgraph.push(link); + cellMap[link.id] = link; + } + }); + }, this); + + return subgraph; + }, + + // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. + getPredecessors: function(element, opt) { + + opt = opt || {}; + var res = []; + // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. + this.search(element, function(el) { + if (el !== element) { + res.push(el); + } + }, joint.util.assign({}, opt, { inbound: true })); + return res; + }, + + // Perform search on the graph. + // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. + // By setting `opt.inbound` to `true`, you can reverse the direction of the search. + // If `opt.deep` is `true`, take into account embedded elements too. + // `iteratee` is a function of the form `function(element) {}`. + // If `iteratee` explicitely returns `false`, the searching stops. + search: function(element, iteratee, opt) { + + opt = opt || {}; + if (opt.breadthFirst) { + this.bfs(element, iteratee, opt); + } else { + this.dfs(element, iteratee, opt); + } + }, + + // Breadth-first search. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). + // `iteratee` is a function of the form `function(element, distance) {}`. + // where `element` is the currently visited element and `distance` is the distance of that element + // from the root `element` passed the `bfs()`, i.e. the element we started the search from. + // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels + // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. + // If `iteratee` explicitely returns `false`, the searching stops. + bfs: function(element, iteratee, opt) { + + opt = opt || {}; + var visited = {}; + var distance = {}; + var queue = []; + + queue.push(element); + distance[element.id] = 0; + + while (queue.length > 0) { + var next = queue.shift(); + if (!visited[next.id]) { + visited[next.id] = true; + if (iteratee(next, distance[next.id]) === false) return; + this.getNeighbors(next, opt).forEach(function(neighbor) { + distance[neighbor.id] = distance[next.id] + 1; + queue.push(neighbor); + }); + } + } + }, + + // Depth-first search. + // If `opt.deep` is `true`, take into account embedded elements too. + // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). + // `iteratee` is a function of the form `function(element, distance) {}`. + // If `iteratee` explicitely returns `false`, the search stops. + dfs: function(element, iteratee, opt, _visited, _distance) { + + opt = opt || {}; + var visited = _visited || {}; + var distance = _distance || 0; + if (iteratee(element, distance) === false) return; + visited[element.id] = true; + + this.getNeighbors(element, opt).forEach(function(neighbor) { + if (!visited[neighbor.id]) { + this.dfs(neighbor, iteratee, opt, visited, distance + 1); + } + }, this); + }, + + // Get all the roots of the graph. Time complexity: O(|V|). + getSources: function() { + + var sources = []; + joint.util.forIn(this._nodes, function(exists, node) { + if (!this._in[node] || joint.util.isEmpty(this._in[node])) { + sources.push(this.getCell(node)); + } + }.bind(this)); + return sources; + }, + + // Get all the leafs of the graph. Time complexity: O(|V|). + getSinks: function() { + + var sinks = []; + joint.util.forIn(this._nodes, function(exists, node) { + if (!this._out[node] || joint.util.isEmpty(this._out[node])) { + sinks.push(this.getCell(node)); + } + }.bind(this)); + return sinks; + }, + + // Return `true` if `element` is a root. Time complexity: O(1). + isSource: function(element) { + + return !this._in[element.id] || joint.util.isEmpty(this._in[element.id]); + }, + + // Return `true` if `element` is a leaf. Time complexity: O(1). + isSink: function(element) { + + return !this._out[element.id] || joint.util.isEmpty(this._out[element.id]); + }, + + // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. + isSuccessor: function(elementA, elementB) { + + var isSuccessor = false; + this.search(elementA, function(element) { + if (element === elementB && element !== elementA) { + isSuccessor = true; + return false; + } + }, { outbound: true }); + return isSuccessor; + }, + + // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. + isPredecessor: function(elementA, elementB) { + + var isPredecessor = false; + this.search(elementA, function(element) { + if (element === elementB && element !== elementA) { + isPredecessor = true; + return false; + } + }, { inbound: true }); + return isPredecessor; + }, + + // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. + // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` + // for more details. + // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. + // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. + isNeighbor: function(elementA, elementB, opt) { + + opt = opt || {}; + + var inbound = opt.inbound; + var outbound = opt.outbound; + if (inbound === undefined && outbound === undefined) { + inbound = outbound = true; + } + + var isNeighbor = false; + + this.getConnectedLinks(elementA, opt).forEach(function(link) { + + var source = link.get('source'); + var target = link.get('target'); + + // Discard if it is a point. + if (inbound && joint.util.has(source, 'id') && source.id === elementB.id) { + isNeighbor = true; + return false; + } + + // Discard if it is a point, or if the neighbor was already added. + if (outbound && joint.util.has(target, 'id') && target.id === elementB.id) { + isNeighbor = true; + return false; + } + }); + + return isNeighbor; + }, + + // Disconnect links connected to the cell `model`. + disconnectLinks: function(model, opt) { + + this.getConnectedLinks(model).forEach(function(link) { + + link.set(link.get('source').id === model.id ? 'source' : 'target', { x: 0, y: 0 }, opt); + }); + }, + + // Remove links connected to the cell `model` completely. + removeLinks: function(model, opt) { + + joint.util.invoke(this.getConnectedLinks(model), 'remove', opt); + }, + + // Find all elements at given point + findModelsFromPoint: function(p) { + + return this.getElements().filter(function(el) { + return el.getBBox().containsPoint(p); + }); + }, + + // Find all elements in given area + findModelsInArea: function(rect, opt) { + + rect = g.rect(rect); + opt = joint.util.defaults(opt || {}, { strict: false }); + + var method = opt.strict ? 'containsRect' : 'intersect'; + + return this.getElements().filter(function(el) { + return rect[method](el.getBBox()); + }); + }, + + // Find all elements under the given element. + findModelsUnderElement: function(element, opt) { + + opt = joint.util.defaults(opt || {}, { searchBy: 'bbox' }); + + var bbox = element.getBBox(); + var elements = (opt.searchBy === 'bbox') + ? this.findModelsInArea(bbox) + : this.findModelsFromPoint(bbox[opt.searchBy]()); + + // don't account element itself or any of its descendents + return elements.filter(function(el) { + return element.id !== el.id && !el.isEmbeddedIn(element); + }); + }, + + + // Return bounding box of all elements. + getBBox: function(cells, opt) { + + return this.getCellsBBox(cells || this.getElements(), opt); + }, + + // Return the bounding box of all cells in array provided. + // Links are being ignored. + getCellsBBox: function(cells, opt) { + + return joint.util.toArray(cells).reduce(function(memo, cell) { + if (cell.isLink()) return memo; + if (memo) { + return memo.union(cell.getBBox(opt)); + } else { + return cell.getBBox(opt); + } + }, null); + }, + + translate: function(dx, dy, opt) { + + // Don't translate cells that are embedded in any other cell. + var cells = this.getCells().filter(function(cell) { + return !cell.isEmbedded(); + }); + + joint.util.invoke(cells, 'translate', dx, dy, opt); + + return this; + }, + + resize: function(width, height, opt) { + + return this.resizeCells(width, height, this.getCells(), opt); + }, + + resizeCells: function(width, height, cells, opt) { + + // `getBBox` method returns `null` if no elements provided. + // i.e. cells can be an array of links + var bbox = this.getCellsBBox(cells); + if (bbox) { + var sx = Math.max(width / bbox.width, 0); + var sy = Math.max(height / bbox.height, 0); + joint.util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); + } + + return this; + }, + + startBatch: function(name, data) { + + data = data || {}; + this._batches[name] = (this._batches[name] || 0) + 1; + + return this.trigger('batch:start', joint.util.assign({}, data, { batchName: name })); + }, + + stopBatch: function(name, data) { + + data = data || {}; + this._batches[name] = (this._batches[name] || 0) - 1; + + return this.trigger('batch:stop', joint.util.assign({}, data, { batchName: name })); + }, + + hasActiveBatch: function(name) { + if (arguments.length === 0) { + return joint.util.toArray(this._batches).some(function(batches) { + return batches > 0; + }); + } + if (Array.isArray(name)) { + return name.some(function(name) { + return !!this._batches[name]; + }, this); + } + return !!this._batches[name]; + } + +}, { + + validations: { + + multiLinks: function(graph, link) { + + // Do not allow multiple links to have the same source and target. + var source = link.get('source'); + var target = link.get('target'); + + if (source.id && target.id) { + + var sourceModel = link.getSourceElement(); + if (sourceModel) { + + var connectedLinks = graph.getConnectedLinks(sourceModel, { outbound: true }); + var sameLinks = connectedLinks.filter(function(_link) { + + var _source = _link.get('source'); + var _target = _link.get('target'); + + return _source && _source.id === source.id && + (!_source.port || (_source.port === source.port)) && + _target && _target.id === target.id && + (!_target.port || (_target.port === target.port)); + + }); + + if (sameLinks.length > 1) { + return false; + } + } + } + + return true; + }, + + linkPinning: function(graph, link) { + return link.source().id && link.target().id; + } + } + +}); + +joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); + +(function(joint, V, g, $, util) { + + function setWrapper(attrName, dimension) { + return function(value, refBBox) { + var isValuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (isValuePercentage) { + value /= 100; + } + + var attrs = {}; + if (isFinite(value)) { + var attrValue = (isValuePercentage || value >= 0 && value <= 1) + ? value * refBBox[dimension] + : Math.max(value + refBBox[dimension], 0); + attrs[attrName] = attrValue; + } + + return attrs; + }; + } + + function positionWrapper(axis, dimension, origin) { + return function(value, refBBox) { + var valuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (valuePercentage) { + value /= 100; + } + + var delta; + if (isFinite(value)) { + var refOrigin = refBBox[origin](); + if (valuePercentage || value > 0 && value < 1) { + delta = refOrigin[axis] + refBBox[dimension] * value; + } else { + delta = refOrigin[axis] + value; + } + } + + var point = g.Point(); + point[axis] = delta || 0; + return point; + }; + } + + function offsetWrapper(axis, dimension, corner) { + return function(value, nodeBBox) { + var delta; + if (value === 'middle') { + delta = nodeBBox[dimension] / 2; + } else if (value === corner) { + delta = nodeBBox[dimension]; + } else if (isFinite(value)) { + // TODO: or not to do a breaking change? + delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value; + } else if (util.isPercentage(value)) { + delta = nodeBBox[dimension] * parseFloat(value) / 100; + } else { + delta = 0; + } + + var point = g.Point(); + point[axis] = -(nodeBBox[axis] + delta); + return point; + }; + } + + function shapeWrapper(shapeConstructor, opt) { + var cacheName = 'joint-shape'; + var resetOffset = opt && opt.resetOffset; + return function(value, refBBox, node) { + var $node = $(node); + var cache = $node.data(cacheName); + if (!cache || cache.value !== value) { + // only recalculate if value has changed + var cachedShape = shapeConstructor(value); + cache = { + value: value, + shape: cachedShape, + shapeBBox: cachedShape.bbox() + }; + $node.data(cacheName, cache); + } + + var shape = cache.shape.clone(); + var shapeBBox = cache.shapeBBox.clone(); + var shapeOrigin = shapeBBox.origin(); + var refOrigin = refBBox.origin(); + + shapeBBox.x = refOrigin.x; + shapeBBox.y = refOrigin.y; + + var fitScale = refBBox.maxRectScaleToFit(shapeBBox, refOrigin); + // `maxRectScaleToFit` can give Infinity if width or height is 0 + var sx = (shapeBBox.width === 0 || refBBox.width === 0) ? 1 : fitScale.sx; + var sy = (shapeBBox.height === 0 || refBBox.height === 0) ? 1 : fitScale.sy; + + shape.scale(sx, sy, shapeOrigin); + if (resetOffset) { + shape.translate(-shapeOrigin.x, -shapeOrigin.y); + } + + return shape; + }; + } + + // `d` attribute for SVGPaths + function dWrapper(opt) { + function pathConstructor(value) { + return new g.Path(V.normalizePathData(value)); + } + var shape = shapeWrapper(pathConstructor, opt); + return function(value, refBBox, node) { + var path = shape(value, refBBox, node); + return { + d: path.serialize() + }; + }; + } + + // `points` attribute for SVGPolylines and SVGPolygons + function pointsWrapper(opt) { + var shape = shapeWrapper(g.Polyline, opt); + return function(value, refBBox, node) { + var polyline = shape(value, refBBox, node); + return { + points: polyline.serialize() + }; + }; + } + + function atConnectionWrapper(method, opt) { + var zeroVector = new g.Point(1, 0); + return function(value) { + var p, angle; + var tangent = this[method](value); + if (tangent) { + angle = (opt.rotate) ? tangent.vector().vectorAngle(zeroVector) : 0; + p = tangent.start; + } else { + p = this.path.start; + angle = 0; + } + if (angle === 0) return { transform: 'translate(' + p.x + ',' + p.y + ')' }; + return { transform: 'translate(' + p.x + ',' + p.y + ') rotate(' + angle + ')' }; + } + } + + function isTextInUse(lineHeight, node, attrs) { + return (attrs.text !== undefined); + } + + function isLinkView() { + return this instanceof joint.dia.LinkView; + } + + function contextMarker(context) { + var marker = {}; + // Stroke + // The context 'fill' is disregared here. The usual case is to use the marker with a connection + // (for which 'fill' attribute is set to 'none'). + var stroke = context.stroke; + if (typeof stroke === 'string') { + marker['stroke'] = stroke; + marker['fill'] = stroke; + } + // Opacity + // Again the context 'fill-opacity' is ignored. + var strokeOpacity = context.strokeOpacity; + if (strokeOpacity === undefined) strokeOpacity = context['stroke-opacity']; + if (strokeOpacity === undefined) strokeOpacity = context.opacity + if (strokeOpacity !== undefined) { + marker['stroke-opacity'] = strokeOpacity; + marker['fill-opacity'] = strokeOpacity; + } + return marker; + } + + var attributesNS = joint.dia.attributes = { + + xlinkHref: { + set: 'xlink:href' + }, + + xlinkShow: { + set: 'xlink:show' + }, + + xlinkRole: { + set: 'xlink:role' + }, + + xlinkType: { + set: 'xlink:type' + }, + + xlinkArcrole: { + set: 'xlink:arcrole' + }, + + xlinkTitle: { + set: 'xlink:title' + }, + + xlinkActuate: { + set: 'xlink:actuate' + }, + + xmlSpace: { + set: 'xml:space' + }, + + xmlBase: { + set: 'xml:base' + }, + + xmlLang: { + set: 'xml:lang' + }, + + preserveAspectRatio: { + set: 'preserveAspectRatio' + }, + + requiredExtension: { + set: 'requiredExtension' + }, + + requiredFeatures: { + set: 'requiredFeatures' + }, + + systemLanguage: { + set: 'systemLanguage' + }, + + externalResourcesRequired: { + set: 'externalResourceRequired' + }, + + filter: { + qualify: util.isPlainObject, + set: function(filter) { + return 'url(#' + this.paper.defineFilter(filter) + ')'; + } + }, + + fill: { + qualify: util.isPlainObject, + set: function(fill) { + return 'url(#' + this.paper.defineGradient(fill) + ')'; + } + }, + + stroke: { + qualify: util.isPlainObject, + set: function(stroke) { + return 'url(#' + this.paper.defineGradient(stroke) + ')'; + } + }, + + sourceMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), marker); + return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, + + targetMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), { 'transform': 'rotate(180)' }, marker); + return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, + + vertexMarker: { + qualify: util.isPlainObject, + set: function(marker, refBBox, node, attrs) { + marker = util.assign(contextMarker(attrs), marker); + return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' }; + } + }, + + text: { + qualify: function(text, node, attrs) { + return !attrs.textWrap || !util.isPlainObject(attrs.textWrap); + }, + set: function(text, refBBox, node, attrs) { + var $node = $(node); + var cacheName = 'joint-text'; + var cache = $node.data(cacheName); + var textAttrs = joint.util.pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'textVerticalAnchor', 'eol'); + var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize']; + var textHash = JSON.stringify([text, textAttrs]); + // Update the text only if there was a change in the string + // or any of its attributes. + if (cache === undefined || cache !== textHash) { + // Chrome bug: + // Tspans positions defined as `em` are not updated + // when container `font-size` change. + if (fontSize) node.setAttribute('font-size', fontSize); + // Text Along Path Selector + var textPath = textAttrs.textPath; + if (util.isObject(textPath)) { + var pathSelector = textPath.selector; + if (typeof pathSelector === 'string') { + var pathNode = this.findBySelector(pathSelector)[0]; + if (pathNode instanceof SVGPathElement) { + textAttrs.textPath = util.assign({ 'xlink:href': '#' + pathNode.id }, textPath); + } + } + } + V(node).text('' + text, textAttrs); + $node.data(cacheName, textHash); + } + } + }, + + textWrap: { + qualify: util.isPlainObject, + set: function(value, refBBox, node, attrs) { + // option `width` + var width = value.width || 0; + if (util.isPercentage(width)) { + refBBox.width *= parseFloat(width) / 100; + } else if (width <= 0) { + refBBox.width += width; + } else { + refBBox.width = width; + } + // option `height` + var height = value.height || 0; + if (util.isPercentage(height)) { + refBBox.height *= parseFloat(height) / 100; + } else if (height <= 0) { + refBBox.height += height; + } else { + refBBox.height = height; + } + // option `text` + var text = value.text; + if (text === undefined) text = attr.text; + if (text !== undefined) { + var wrappedText = joint.util.breakText('' + text, refBBox, { + 'font-weight': attrs['font-weight'] || attrs.fontWeight, + 'font-size': attrs['font-size'] || attrs.fontSize, + 'font-family': attrs['font-family'] || attrs.fontFamily, + 'lineHeight': attrs.lineHeight + }, { + // Provide an existing SVG Document here + // instead of creating a temporary one over again. + svgDocument: this.paper.svg + }); + } + joint.dia.attributes.text.set.call(this, wrappedText, refBBox, node, attrs); + } + }, + + title: { + qualify: function(title, node) { + // HTMLElement title is specified via an attribute (i.e. not an element) + return node instanceof SVGElement; + }, + set: function(title, refBBox, node) { + var $node = $(node); + var cacheName = 'joint-title'; + var cache = $node.data(cacheName); + if (cache === undefined || cache !== title) { + $node.data(cacheName, title); + // Generally element should be the first child element of its parent. + var firstChild = node.firstChild; + if (firstChild && firstChild.tagName.toUpperCase() === 'TITLE') { + // Update an existing title + firstChild.textContent = title; + } else { + // Create a new title + var titleNode = document.createElementNS(node.namespaceURI, 'title'); + titleNode.textContent = title; + node.insertBefore(titleNode, firstChild); + } + } + } + }, + + lineHeight: { + qualify: isTextInUse + }, + + textVerticalAnchor: { + qualify: isTextInUse + }, + + textPath: { + qualify: isTextInUse + }, + + annotations: { + qualify: isTextInUse + }, + + // `port` attribute contains the `id` of the port that the underlying magnet represents. + port: { + set: function(port) { + return (port === null || port.id === undefined) ? port : port.id; + } + }, + + // `style` attribute is special in the sense that it sets the CSS style of the subelement. + style: { + qualify: util.isPlainObject, + set: function(styles, refBBox, node) { + $(node).css(styles); + } + }, + + html: { + set: function(html, refBBox, node) { + $(node).html(html + ''); + } + }, + + ref: { + // We do not set `ref` attribute directly on an element. + // The attribute itself does not qualify for relative positioning. + }, + + // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width + // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box + // otherwise, `refX` is the left coordinate of the bounding box + + refX: { + position: positionWrapper('x', 'width', 'origin') + }, + + refY: { + position: positionWrapper('y', 'height', 'origin') + }, + + // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom + // coordinate of the reference element. + + refDx: { + position: positionWrapper('x', 'width', 'corner') + }, + + refDy: { + position: positionWrapper('y', 'height', 'corner') + }, + + // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to + // the reference element size + // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width + // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 + + refWidth: { + set: setWrapper('width', 'width') + }, + + refHeight: { + set: setWrapper('height', 'height') + }, + + refRx: { + set: setWrapper('rx', 'width') + }, + + refRy: { + set: setWrapper('ry', 'height') + }, + + refRInscribed: { + set: (function(attrName) { + var widthFn = setWrapper(attrName, 'width'); + var heightFn = setWrapper(attrName, 'height'); + return function(value, refBBox) { + var fn = (refBBox.height > refBBox.width) ? widthFn : heightFn; + return fn(value, refBBox); + } + })('r') + }, + + refRCircumscribed: { + set: function(value, refBBox) { + var isValuePercentage = util.isPercentage(value); + value = parseFloat(value); + if (isValuePercentage) { + value /= 100; + } + + var diagonalLength = Math.sqrt((refBBox.height * refBBox.height) + (refBBox.width * refBBox.width)); + + var rValue; + if (isFinite(value)) { + if (isValuePercentage || value >= 0 && value <= 1) rValue = value * diagonalLength; + else rValue = Math.max(value + diagonalLength, 0); + } + + return { r: rValue }; + } + }, + + refCx: { + set: setWrapper('cx', 'width') + }, + + refCy: { + set: setWrapper('cy', 'height') + }, + + // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. + // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. + + xAlignment: { + offset: offsetWrapper('x', 'width', 'right') + }, + + // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. + // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. + + yAlignment: { + offset: offsetWrapper('y', 'height', 'bottom') + }, + + resetOffset: { + offset: function(val, nodeBBox) { + return (val) + ? { x: -nodeBBox.x, y: -nodeBBox.y } + : { x: 0, y: 0 }; + } + + }, + + refDResetOffset: { + set: dWrapper({ resetOffset: true }) + }, + + refDKeepOffset: { + set: dWrapper({ resetOffset: false }) + }, + + refPointsResetOffset: { + set: pointsWrapper({ resetOffset: true }) + }, + + refPointsKeepOffset: { + set: pointsWrapper({ resetOffset: false }) + }, + + // LinkView Attributes + + connection: { + qualify: isLinkView, + set: function() { + return { d: this.getSerializedConnection() }; + } + }, + + atConnectionLengthKeepGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtLength', { rotate: true }) + }, + + atConnectionLengthIgnoreGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtLength', { rotate: false }) + }, + + atConnectionRatioKeepGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtRatio', { rotate: true }) + }, + + atConnectionRatioIgnoreGradient: { + qualify: isLinkView, + set: atConnectionWrapper('getTangentAtRatio', { rotate: false }) + } + }; + + // Aliases + attributesNS.refR = attributesNS.refRInscribed; + attributesNS.refD = attributesNS.refDResetOffset; + attributesNS.refPoints = attributesNS.refPointsResetOffset; + attributesNS.atConnectionLength = attributesNS.atConnectionLengthKeepGradient; + attributesNS.atConnectionRatio = attributesNS.atConnectionRatioKeepGradient; + + // This allows to combine both absolute and relative positioning + // refX: 50%, refX2: 20 + attributesNS.refX2 = attributesNS.refX; + attributesNS.refY2 = attributesNS.refY; + + // Aliases for backwards compatibility + attributesNS['ref-x'] = attributesNS.refX; + attributesNS['ref-y'] = attributesNS.refY; + attributesNS['ref-dy'] = attributesNS.refDy; + attributesNS['ref-dx'] = attributesNS.refDx; + attributesNS['ref-width'] = attributesNS.refWidth; + attributesNS['ref-height'] = attributesNS.refHeight; + attributesNS['x-alignment'] = attributesNS.xAlignment; + attributesNS['y-alignment'] = attributesNS.yAlignment; + +})(joint, V, g, $, joint.util); + +(function(joint, util) { + + var ToolView = joint.mvc.View.extend({ + name: null, + tagName: 'g', + className: 'tool', + svgElement: true, + _visible: true, + + init: function() { + var name = this.name; + if (name) this.vel.attr('data-tool-name', name); + }, + + configure: function(view, toolsView) { + this.relatedView = view; + this.paper = view.paper; + this.parentView = toolsView; + this.simulateRelatedView(this.el); + return this; + }, + + simulateRelatedView: function(el) { + if (el) el.setAttribute('model-id', this.relatedView.model.id); + }, + + getName: function() { + return this.name; + }, + + show: function() { + this.el.style.display = ''; + this._visible = true; + }, + + hide: function() { + this.el.style.display = 'none'; + this._visible = false; + }, + + isVisible: function() { + return !!this._visible; + }, + + focus: function() { + var opacity = this.options.focusOpacity; + if (isFinite(opacity)) this.el.style.opacity = opacity; + this.parentView.focusTool(this); + }, + + blur: function() { + this.el.style.opacity = ''; + this.parentView.blurTool(this); + }, + + update: function() { + // to be overriden + } + }); + + var ToolsView = joint.mvc.View.extend({ + tagName: 'g', + className: 'tools', + svgElement: true, + tools: null, + options: { + tools: null, + relatedView: null, + name: null, + component: false + }, + + configure: function(options) { + options = util.assign(this.options, options); + var tools = options.tools; + if (!Array.isArray(tools)) return this; + var relatedView = options.relatedView; + if (!(relatedView instanceof joint.dia.CellView)) return this; + var views = this.tools = []; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (!(tool instanceof ToolView)) continue; + tool.configure(relatedView, this); + tool.render(); + this.vel.append(tool.el); + views.push(tool); + } + return this; + }, + + getName: function() { + return this.options.name; + }, + + update: function(opt) { + + opt || (opt = {}); + var tools = this.tools; + if (!tools) return; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (opt.tool !== tool.cid && tool.isVisible()) { + tool.update(); + } + } + return this; + }, + + focusTool: function(focusedTool) { + + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (focusedTool === tool) { + tool.show(); + } else { + tool.hide(); + } + } + return this; + }, + + blurTool: function(blurredTool) { + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + var tool = tools[i]; + if (tool !== blurredTool && !tool.isVisible()) { + tool.show(); + tool.update(); + } + } + return this; + }, + + hide: function() { + return this.focusTool(null); + }, + + show: function() { + return this.blurTool(null); + }, + + onRemove: function() { + + var tools = this.tools; + if (!tools) return this; + for (var i = 0, n = tools.length; i < n; i++) { + tools[i].remove(); + } + this.tools = null; + }, + + mount: function() { + var options = this.options; + var relatedView = options.relatedView; + if (relatedView) { + var container = (options.component) ? relatedView.el : relatedView.paper.tools; + container.appendChild(this.el); + } + return this; + } + + }); + + joint.dia.ToolsView = ToolsView; + joint.dia.ToolView = ToolView; + +})(joint, joint.util); + + +// joint.dia.Cell base model. +// -------------------------- + +joint.dia.Cell = Backbone.Model.extend({ + + // This is the same as Backbone.Model with the only difference that is uses joint.util.merge + // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. + constructor: function(attributes, options) { + + var defaults; + var attrs = attributes || {}; + this.cid = joint.util.uniqueId('c'); + this.attributes = {}; + if (options && options.collection) this.collection = options.collection; + if (options && options.parse) attrs = this.parse(attrs, options) || {}; + if ((defaults = joint.util.result(this, 'defaults'))) { + //<custom code> + // Replaced the call to _.defaults with joint.util.merge. + attrs = joint.util.merge({}, defaults, attrs); + //</custom code> + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }, + + translate: function(dx, dy, opt) { + + throw new Error('Must define a translate() method.'); + }, + + toJSON: function() { + + var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; + var attrs = this.attributes.attrs; + var finalAttrs = {}; + + // Loop through all the attributes and + // omit the default attributes as they are implicitly reconstructable by the cell 'type'. + joint.util.forIn(attrs, function(attr, selector) { + + var defaultAttr = defaultAttrs[selector]; + + joint.util.forIn(attr, function(value, name) { + + // attr is mainly flat though it might have one more level (consider the `style` attribute). + // Check if the `value` is object and if yes, go one level deep. + if (joint.util.isObject(value) && !Array.isArray(value)) { + + joint.util.forIn(value, function(value2, name2) { + + if (!defaultAttr || !defaultAttr[name] || !joint.util.isEqual(defaultAttr[name][name2], value2)) { + + finalAttrs[selector] = finalAttrs[selector] || {}; + (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; + } + }); + + } else if (!defaultAttr || !joint.util.isEqual(defaultAttr[name], value)) { + // `value` is not an object, default attribute for such a selector does not exist + // or it is different than the attribute value set on the model. + + finalAttrs[selector] = finalAttrs[selector] || {}; + finalAttrs[selector][name] = value; + } + }); + }); + + var attributes = joint.util.cloneDeep(joint.util.omit(this.attributes, 'attrs')); + //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); + attributes.attrs = finalAttrs; + + return attributes; + }, + + initialize: function(options) { + + if (!options || !options.id) { + + this.set('id', joint.util.uuid(), { silent: true }); + } + + this._transitionIds = {}; + + // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. + this.processPorts(); + this.on('change:attrs', this.processPorts, this); + }, + + /** + * @deprecated + */ + processPorts: function() { + + // Whenever `attrs` changes, we extract ports from the `attrs` object and store it + // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` + // set to that port, we remove those links as well (to follow the same behaviour as + // with a removed element). + + var previousPorts = this.ports; + + // Collect ports from the `attrs` object. + var ports = {}; + joint.util.forIn(this.get('attrs'), function(attrs, selector) { + + if (attrs && attrs.port) { + + // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). + if (attrs.port.id !== undefined) { + ports[attrs.port.id] = attrs.port; + } else { + ports[attrs.port] = { id: attrs.port }; + } + } + }); + + // Collect ports that have been removed (compared to the previous ports) - if any. + // Use hash table for quick lookup. + var removedPorts = {}; + joint.util.forIn(previousPorts, function(port, id) { + + if (!ports[id]) removedPorts[id] = true; + }); + + // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. + if (this.graph && !joint.util.isEmpty(removedPorts)) { + + var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); + inboundLinks.forEach(function(link) { + + if (removedPorts[link.get('target').port]) link.remove(); + }); + + var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); + outboundLinks.forEach(function(link) { + + if (removedPorts[link.get('source').port]) link.remove(); + }); + } + + // Update the `ports` object. + this.ports = ports; + }, - var clone = cloneMap[cell.id]; - // assert(clone exists) + remove: function(opt) { - if (clone.isLink()) { - var source = clone.get('source'); - var target = clone.get('target'); - if (source.id && cloneMap[source.id]) { - // Source points to an element and the element is among the clones. - // => Update the source of the cloned link. - clone.prop('source/id', cloneMap[source.id].id); - } - if (target.id && cloneMap[target.id]) { - // Target points to an element and the element is among the clones. - // => Update the target of the cloned link. - clone.prop('target/id', cloneMap[target.id].id); - } + opt = opt || {}; + + // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. + var graph = this.graph; + if (graph) { + graph.startBatch('remove'); + } + + // First, unembed this cell from its parent cell if there is one. + var parentCell = this.getParentCell(); + if (parentCell) parentCell.unembed(this); + + joint.util.invoke(this.getEmbeddedCells(), 'remove', opt); + + this.trigger('remove', this, this.collection, opt); + + if (graph) { + graph.stopBatch('remove'); + } + + return this; + }, + + toFront: function(opt) { + + var graph = this.graph; + if (graph) { + + opt = opt || {}; + + var z = graph.maxZIndex(); + + var cells; + + if (opt.deep) { + cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + cells.unshift(this); + } else { + cells = [this]; } - // Find the parent of the original cell - var parent = cell.get('parent'); - if (parent && cloneMap[parent]) { - clone.set('parent', cloneMap[parent].id); + z = z - cells.length + 1; + + var collection = graph.get('cells'); + var shouldUpdate = (collection.indexOf(this) !== (collection.length - cells.length)); + if (!shouldUpdate) { + shouldUpdate = cells.some(function(cell, index) { + return cell.get('z') !== z + index; + }); } - // Find the embeds of the original cell - var embeds = joint.util.toArray(cell.get('embeds')).reduce(function(newEmbeds, embed) { - // Embedded cells that are not being cloned can not be carried - // over with other embedded cells. - if (cloneMap[embed]) { - newEmbeds.push(cloneMap[embed].id); + if (shouldUpdate) { + this.startBatch('to-front'); + + z = z + cells.length; + + cells.forEach(function(cell, index) { + cell.set('z', z + index, opt); + }); + + this.stopBatch('to-front'); + } + } + + return this; + }, + + toBack: function(opt) { + + var graph = this.graph; + if (graph) { + + opt = opt || {}; + + var z = graph.minZIndex(); + + var cells; + + if (opt.deep) { + cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + cells.unshift(this); + } else { + cells = [this]; + } + + var collection = graph.get('cells'); + var shouldUpdate = (collection.indexOf(this) !== 0); + if (!shouldUpdate) { + shouldUpdate = cells.some(function(cell, index) { + return cell.get('z') !== z + index; + }); + } + + if (shouldUpdate) { + this.startBatch('to-back'); + + z -= cells.length; + + cells.forEach(function(cell, index) { + cell.set('z', z + index, opt); + }); + + this.stopBatch('to-back'); + } + } + + return this; + }, + + parent: function(parent, opt) { + + // getter + if (parent === undefined) return this.get('parent'); + // setter + return this.set('parent', parent, opt); + }, + + embed: function(cell, opt) { + + if (this === cell || this.isEmbeddedIn(cell)) { + + throw new Error('Recursive embedding not allowed.'); + + } else { + + this.startBatch('embed'); + + var embeds = joint.util.assign([], this.get('embeds')); + + // We keep all element ids after link ids. + embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); + + cell.parent(this.id, opt); + this.set('embeds', joint.util.uniq(embeds), opt); + + this.stopBatch('embed'); + } + + return this; + }, + + unembed: function(cell, opt) { + + this.startBatch('unembed'); + + cell.unset('parent', opt); + this.set('embeds', joint.util.without(this.get('embeds'), cell.id), opt); + + this.stopBatch('unembed'); + + return this; + }, + + getParentCell: function() { + + // unlike link.source/target, cell.parent stores id directly as a string + var parentId = this.parent(); + var graph = this.graph; + + return (parentId && graph && graph.getCell(parentId)) || null; + }, + + // Return an array of ancestor cells. + // The array is ordered from the parent of the cell + // to the most distant ancestor. + getAncestors: function() { + + var ancestors = []; + + if (!this.graph) { + return ancestors; + } + + var parentCell = this.getParentCell(); + while (parentCell) { + ancestors.push(parentCell); + parentCell = parentCell.getParentCell(); + } + + return ancestors; + }, + + getEmbeddedCells: function(opt) { + + opt = opt || {}; + + // Cell models can only be retrieved when this element is part of a collection. + // There is no way this element knows about other cells otherwise. + // This also means that calling e.g. `translate()` on an element with embeds before + // adding it to a graph does not translate its embeds. + if (this.graph) { + + var cells; + + if (opt.deep) { + + if (opt.breadthFirst) { + + // breadthFirst algorithm + cells = []; + var queue = this.getEmbeddedCells(); + + while (queue.length > 0) { + + var parent = queue.shift(); + cells.push(parent); + queue.push.apply(queue, parent.getEmbeddedCells()); + } + + } else { + + // depthFirst algorithm + cells = this.getEmbeddedCells(); + cells.forEach(function(cell) { + cells.push.apply(cells, cell.getEmbeddedCells(opt)); + }); } - return newEmbeds; - }, []); - if (!joint.util.isEmpty(embeds)) { - clone.set('embeds', embeds); - } - }); + } else { + + cells = joint.util.toArray(this.get('embeds')).map(this.graph.getCell, this.graph); + } + + return cells; + } + return []; + }, + + isEmbeddedIn: function(cell, opt) { + + var cellId = joint.util.isString(cell) ? cell : cell.id; + var parentId = this.parent(); + + opt = joint.util.defaults({ deep: true }, opt); + + // See getEmbeddedCells(). + if (this.graph && opt.deep) { + + while (parentId) { + if (parentId === cellId) { + return true; + } + parentId = this.graph.getCell(parentId).parent(); + } + + return false; + + } else { + + // When this cell is not part of a collection check + // at least whether it's a direct child of given cell. + return parentId === cellId; + } + }, + + // Whether or not the cell is embedded in any other cell. + isEmbedded: function() { + + return !!this.parent(); + }, + + // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). + // Shallow cloning simply clones the cell and returns a new cell with different ID. + // Deep cloning clones the cell and all its embedded cells recursively. + clone: function(opt) { + + opt = opt || {}; + + if (!opt.deep) { + // Shallow cloning. + + var clone = Backbone.Model.prototype.clone.apply(this, arguments); + // We don't want the clone to have the same ID as the original. + clone.set('id', joint.util.uuid()); + // A shallow cloned element does not carry over the original embeds. + clone.unset('embeds'); + // And can not be embedded in any cell + // as the clone is not part of the graph. + clone.unset('parent'); + + return clone; - return cloneMap; + } else { + // Deep cloning. + + // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. + return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); + } }, - // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). - // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. - // Return a map of the form: [original cell ID] -> [clone]. - cloneSubgraph: function(cells, opt) { + // A convenient way to set nested properties. + // This method merges the properties you'd like to set with the ones + // stored in the cell and makes sure change events are properly triggered. + // You can either set a nested property with one object + // or use a property path. + // The most simple use case is: + // `cell.prop('name/first', 'John')` or + // `cell.prop({ name: { first: 'John' } })`. + // Nested arrays are supported too: + // `cell.prop('series/0/data/0/degree', 50)` or + // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. + prop: function(props, value, opt) { - var subgraph = this.getSubgraph(cells, opt); - return this.cloneCells(subgraph); - }, + var delim = '/'; + var isString = joint.util.isString(props); - // Return `cells` and all the connected links that connect cells in the `cells` array. - // If `opt.deep` is `true`, return all the cells including all their embedded cells - // and all the links that connect any of the returned cells. - // For example, for a single shallow element, the result is that very same element. - // For two elements connected with a link: `A --- L ---> B`, the result for - // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. - getSubgraph: function(cells, opt) { + if (isString || Array.isArray(props)) { + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. - opt = opt || {}; + if (arguments.length > 1) { - var subgraph = []; - // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. - var cellMap = {}; - var elements = []; - var links = []; + var path; + var pathArray; - joint.util.toArray(cells).forEach(function(cell) { - if (!cellMap[cell.id]) { - subgraph.push(cell); - cellMap[cell.id] = cell; - if (cell.isLink()) { - links.push(cell); + if (isString) { + path = props; + pathArray = path.split('/') } else { - elements.push(cell); + path = props.join(delim); + pathArray = props.slice(); } - } - if (opt.deep) { - var embeds = cell.getEmbeddedCells({ deep: true }); - embeds.forEach(function(embed) { - if (!cellMap[embed.id]) { - subgraph.push(embed); - cellMap[embed.id] = embed; - if (embed.isLink()) { - links.push(embed); - } else { - elements.push(embed); - } - } - }); - } - }); + var property = pathArray[0]; + var pathArrayLength = pathArray.length; - links.forEach(function(link) { - // For links, return their source & target (if they are elements - not points). - var source = link.get('source'); - var target = link.get('target'); - if (source.id && !cellMap[source.id]) { - var sourceElement = this.getCell(source.id); - subgraph.push(sourceElement); - cellMap[sourceElement.id] = sourceElement; - elements.push(sourceElement); - } - if (target.id && !cellMap[target.id]) { - var targetElement = this.getCell(target.id); - subgraph.push(this.getCell(target.id)); - cellMap[targetElement.id] = targetElement; - elements.push(targetElement); - } - }, this); + opt = opt || {}; + opt.propertyPath = path; + opt.propertyValue = value; + opt.propertyPathArray = pathArray; - elements.forEach(function(element) { - // For elements, include their connected links if their source/target is in the subgraph; - var links = this.getConnectedLinks(element, opt); - links.forEach(function(link) { - var source = link.get('source'); - var target = link.get('target'); - if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { - subgraph.push(link); - cellMap[link.id] = link; + if (pathArrayLength === 1) { + // Property is not nested. We can simply use `set()`. + return this.set(property, value, opt); } - }); - }, this); - return subgraph; - }, + var update = {}; + // Initialize the nested object. Subobjects are either arrays or objects. + // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. + // Note that this imposes a limitation on object keys one can use with Inspector. + // Pure integer keys will cause issues and are therefore not allowed. + var initializer = update; + var prevProperty = property; - // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. - getPredecessors: function(element, opt) { + for (var i = 1; i < pathArrayLength; i++) { + var pathItem = pathArray[i]; + var isArrayIndex = Number.isFinite(isString ? Number(pathItem) : pathItem); + initializer = initializer[prevProperty] = isArrayIndex ? [] : {}; + prevProperty = pathItem; + } - opt = opt || {}; - var res = []; - // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. - this.search(element, function(el) { - if (el !== element) { - res.push(el); - } - }, joint.util.assign({}, opt, { inbound: true })); - return res; - }, + // Fill update with the `value` on `path`. + update = joint.util.setByPath(update, pathArray, value, '/'); - // Perform search on the graph. - // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. - // By setting `opt.inbound` to `true`, you can reverse the direction of the search. - // If `opt.deep` is `true`, take into account embedded elements too. - // `iteratee` is a function of the form `function(element) {}`. - // If `iteratee` explicitely returns `false`, the searching stops. - search: function(element, iteratee, opt) { + var baseAttributes = joint.util.merge({}, this.attributes); + // if rewrite mode enabled, we replace value referenced by path with + // the new one (we don't merge). + opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); - opt = opt || {}; - if (opt.breadthFirst) { - this.bfs(element, iteratee, opt); - } else { - this.dfs(element, iteratee, opt); + // Merge update with the model attributes. + var attributes = joint.util.merge(baseAttributes, update); + // Finally, set the property to the updated attributes. + return this.set(property, attributes[property], opt); + + } else { + + return joint.util.getByPath(this.attributes, props, delim); + } } + + return this.set(joint.util.merge({}, this.attributes, props), value); }, - // Breadth-first search. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). - // `iteratee` is a function of the form `function(element, distance) {}`. - // where `element` is the currently visited element and `distance` is the distance of that element - // from the root `element` passed the `bfs()`, i.e. the element we started the search from. - // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels - // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. - // If `iteratee` explicitely returns `false`, the searching stops. - bfs: function(element, iteratee, opt) { + // A convient way to unset nested properties + removeProp: function(path, opt) { + // Once a property is removed from the `attrs` attribute + // the cellView will recognize a `dirty` flag and rerender itself + // in order to remove the attribute from SVG element. opt = opt || {}; - var visited = {}; - var distance = {}; - var queue = []; + opt.dirty = true; - queue.push(element); - distance[element.id] = 0; + var pathArray = Array.isArray(path) ? path : path.split('/'); - while (queue.length > 0) { - var next = queue.shift(); - if (!visited[next.id]) { - visited[next.id] = true; - if (iteratee(next, distance[next.id]) === false) return; - this.getNeighbors(next, opt).forEach(function(neighbor) { - distance[neighbor.id] = distance[next.id] + 1; - queue.push(neighbor); - }); - } + if (pathArray.length === 1) { + // A top level property + return this.unset(path, opt); } - }, - // Depth-first search. - // If `opt.deep` is `true`, take into account embedded elements too. - // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). - // `iteratee` is a function of the form `function(element, distance) {}`. - // If `iteratee` explicitely returns `false`, the search stops. - dfs: function(element, iteratee, opt, _visited, _distance) { + // A nested property + var property = pathArray[0]; + var nestedPath = pathArray.slice(1); + var propertyValue = joint.util.cloneDeep(this.get(property)); - opt = opt || {}; - var visited = _visited || {}; - var distance = _distance || 0; - if (iteratee(element, distance) === false) return; - visited[element.id] = true; + joint.util.unsetByPath(propertyValue, nestedPath, '/'); - this.getNeighbors(element, opt).forEach(function(neighbor) { - if (!visited[neighbor.id]) { - this.dfs(neighbor, iteratee, opt, visited, distance + 1); - } - }, this); + return this.set(property, propertyValue, opt); }, - // Get all the roots of the graph. Time complexity: O(|V|). - getSources: function() { + // A convenient way to set nested attributes. + attr: function(attrs, value, opt) { - var sources = []; - joint.util.forIn(this._nodes, function(exists, node) { - if (!this._in[node] || joint.util.isEmpty(this._in[node])) { - sources.push(this.getCell(node)); - } - }.bind(this)); - return sources; - }, + var args = Array.from(arguments); + if (args.length === 0) { + return this.get('attrs'); + } + + if (Array.isArray(attrs)) { + args[0] = ['attrs'].concat(attrs); + } else if (joint.util.isString(attrs)) { + // Get/set an attribute by a special path syntax that delimits + // nested objects by the colon character. + args[0] = 'attrs/' + attrs; - // Get all the leafs of the graph. Time complexity: O(|V|). - getSinks: function() { + } else { - var sinks = []; - joint.util.forIn(this._nodes, function(exists, node) { - if (!this._out[node] || joint.util.isEmpty(this._out[node])) { - sinks.push(this.getCell(node)); - } - }.bind(this)); - return sinks; + args[0] = { 'attrs' : attrs }; + } + + return this.prop.apply(this, args); }, - // Return `true` if `element` is a root. Time complexity: O(1). - isSource: function(element) { + // A convenient way to unset nested attributes + removeAttr: function(path, opt) { - return !this._in[element.id] || joint.util.isEmpty(this._in[element.id]); - }, + if (Array.isArray(path)) { - // Return `true` if `element` is a leaf. Time complexity: O(1). - isSink: function(element) { + return this.removeProp(['attrs'].concat(path)); + } - return !this._out[element.id] || joint.util.isEmpty(this._out[element.id]); + return this.removeProp('attrs/' + path, opt); }, - // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. - isSuccessor: function(elementA, elementB) { + transition: function(path, value, opt, delim) { - var isSuccessor = false; - this.search(elementA, function(element) { - if (element === elementB && element !== elementA) { - isSuccessor = true; - return false; - } - }, { outbound: true }); - return isSuccessor; - }, + delim = delim || '/'; - // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. - isPredecessor: function(elementA, elementB) { + var defaults = { + duration: 100, + delay: 10, + timingFunction: joint.util.timing.linear, + valueFunction: joint.util.interpolate.number + }; - var isPredecessor = false; - this.search(elementA, function(element) { - if (element === elementB && element !== elementA) { - isPredecessor = true; - return false; + opt = joint.util.assign(defaults, opt); + + var firstFrameTime = 0; + var interpolatingFunction; + + var setter = function(runtime) { + + var id, progress, propertyValue; + + firstFrameTime = firstFrameTime || runtime; + runtime -= firstFrameTime; + progress = runtime / opt.duration; + + if (progress < 1) { + this._transitionIds[path] = id = joint.util.nextFrame(setter); + } else { + progress = 1; + delete this._transitionIds[path]; } - }, { inbound: true }); - return isPredecessor; - }, - // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. - // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` - // for more details. - // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. - // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. - isNeighbor: function(elementA, elementB, opt) { + propertyValue = interpolatingFunction(opt.timingFunction(progress)); - opt = opt || {}; + opt.transitionId = id; - var inbound = opt.inbound; - var outbound = opt.outbound; - if (inbound === undefined && outbound === undefined) { - inbound = outbound = true; - } + this.prop(path, propertyValue, opt); - var isNeighbor = false; + if (!id) this.trigger('transition:end', this, path); - this.getConnectedLinks(elementA, opt).forEach(function(link) { + }.bind(this); - var source = link.get('source'); - var target = link.get('target'); + var initiator = function(callback) { - // Discard if it is a point. - if (inbound && joint.util.has(source, 'id') && source.id === elementB.id) { - isNeighbor = true; - return false; - } + this.stopTransitions(path); - // Discard if it is a point, or if the neighbor was already added. - if (outbound && joint.util.has(target, 'id') && target.id === elementB.id) { - isNeighbor = true; - return false; - } - }); + interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); - return isNeighbor; - }, + this._transitionIds[path] = joint.util.nextFrame(callback); - // Disconnect links connected to the cell `model`. - disconnectLinks: function(model, opt) { + this.trigger('transition:start', this, path); - this.getConnectedLinks(model).forEach(function(link) { + }.bind(this); - link.set(link.get('source').id === model.id ? 'source' : 'target', { x: 0, y: 0 }, opt); - }); + return setTimeout(initiator, opt.delay, setter); }, - // Remove links connected to the cell `model` completely. - removeLinks: function(model, opt) { + getTransitions: function() { - joint.util.invoke(this.getConnectedLinks(model), 'remove', opt); + return Object.keys(this._transitionIds); }, - // Find all elements at given point - findModelsFromPoint: function(p) { + stopTransitions: function(path, delim) { - return this.getElements().filter(function(el) { - return el.getBBox().containsPoint(p); - }); - }, + delim = delim || '/'; - // Find all elements in given area - findModelsInArea: function(rect, opt) { + var pathArray = path && path.split(delim); - rect = g.rect(rect); - opt = joint.util.defaults(opt || {}, { strict: false }); + Object.keys(this._transitionIds).filter(pathArray && function(key) { - var method = opt.strict ? 'containsRect' : 'intersect'; + return joint.util.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); - return this.getElements().filter(function(el) { - return rect[method](el.getBBox()); - }); - }, + }).forEach(function(key) { - // Find all elements under the given element. - findModelsUnderElement: function(element, opt) { + joint.util.cancelFrame(this._transitionIds[key]); - opt = joint.util.defaults(opt || {}, { searchBy: 'bbox' }); + delete this._transitionIds[key]; - var bbox = element.getBBox(); - var elements = (opt.searchBy === 'bbox') - ? this.findModelsInArea(bbox) - : this.findModelsFromPoint(bbox[opt.searchBy]()); + this.trigger('transition:end', this, key); - // don't account element itself or any of its descendents - return elements.filter(function(el) { - return element.id !== el.id && !el.isEmbeddedIn(element); - }); + }, this); + + return this; }, + // A shorcut making it easy to create constructs like the following: + // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. + addTo: function(graph, opt) { - // Return bounding box of all elements. - getBBox: function(cells, opt) { + graph.addCell(this, opt); + return this; + }, - return this.getCellsBBox(cells || this.getElements(), opt); + // A shortcut for an equivalent call: `paper.findViewByModel(cell)` + // making it easy to create constructs like the following: + // `cell.findView(paper).highlight()` + findView: function(paper) { + + return paper.findViewByModel(this); }, - // Return the bounding box of all cells in array provided. - // Links are being ignored. - getCellsBBox: function(cells, opt) { + isElement: function() { - return joint.util.toArray(cells).reduce(function(memo, cell) { - if (cell.isLink()) return memo; - if (memo) { - return memo.union(cell.getBBox(opt)); - } else { - return cell.getBBox(opt); - } - }, null); + return false; }, - translate: function(dx, dy, opt) { + isLink: function() { - // Don't translate cells that are embedded in any other cell. - var cells = this.getCells().filter(function(cell) { - return !cell.isEmbedded(); - }); + return false; + }, - joint.util.invoke(cells, 'translate', dx, dy, opt); + startBatch: function(name, opt) { + if (this.graph) { this.graph.startBatch(name, joint.util.assign({}, opt, { cell: this })); } return this; }, - resize: function(width, height, opt) { + stopBatch: function(name, opt) { - return this.resizeCells(width, height, this.getCells(), opt); - }, + if (this.graph) { this.graph.stopBatch(name, joint.util.assign({}, opt, { cell: this })); } + return this; + } - resizeCells: function(width, height, cells, opt) { +}, { - // `getBBox` method returns `null` if no elements provided. - // i.e. cells can be an array of links - var bbox = this.getCellsBBox(cells); - if (bbox) { - var sx = Math.max(width / bbox.width, 0); - var sy = Math.max(height / bbox.height, 0); - joint.util.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); - } + getAttributeDefinition: function(attrName) { - return this; + var defNS = this.attributes; + var globalDefNS = joint.dia.attributes; + return (defNS && defNS[attrName]) || globalDefNS[attrName]; }, - startBatch: function(name, data) { + define: function(type, defaults, protoProps, staticProps) { - data = data || {}; - this._batches[name] = (this._batches[name] || 0) + 1; + protoProps = joint.util.assign({ + defaults: joint.util.defaultsDeep({ type: type }, defaults, this.prototype.defaults) + }, protoProps); - return this.trigger('batch:start', joint.util.assign({}, data, { batchName: name })); - }, + var Cell = this.extend(protoProps, staticProps); + joint.util.setByPath(joint.shapes, type, Cell, '.'); + return Cell; + } +}); - stopBatch: function(name, data) { +// joint.dia.CellView base view and controller. +// -------------------------------------------- - data = data || {}; - this._batches[name] = (this._batches[name] || 0) - 1; +// This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. + +joint.dia.CellView = joint.mvc.View.extend({ + + tagName: 'g', + + svgElement: true, + + selector: 'root', + + className: function() { + + var classNames = ['cell']; + var type = this.model.get('type'); - return this.trigger('batch:stop', joint.util.assign({}, data, { batchName: name })); - }, + if (type) { - hasActiveBatch: function(name) { - if (name) { - return !!this._batches[name]; - } else { - return joint.util.toArray(this._batches).some(function(batches) { - return batches > 0; + type.toLowerCase().split('.').forEach(function(value, index, list) { + classNames.push('type-' + list.slice(0, index + 1).join('-')); }); } - } -}); -joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); - -(function(joint, _, g, $, util) { - - function isPercentage(val) { - return util.isString(val) && val.slice(-1) === '%'; - } + return classNames.join(' '); + }, - function setWrapper(attrName, dimension) { - return function(value, refBBox) { - var isValuePercentage = isPercentage(value); - value = parseFloat(value); - if (isValuePercentage) { - value /= 100; - } + attributes: function() { - var attrs = {}; - if (isFinite(value)) { - var attrValue = (isValuePercentage || value >= 0 && value <= 1) - ? value * refBBox[dimension] - : Math.max(value + refBBox[dimension], 0); - attrs[attrName] = attrValue; - } + return { 'model-id': this.model.id }; + }, - return attrs; - }; - } + constructor: function(options) { - function positionWrapper(axis, dimension, origin) { - return function(value, refBBox) { - var valuePercentage = isPercentage(value); - value = parseFloat(value); - if (valuePercentage) { - value /= 100; - } + // Make sure a global unique id is assigned to this view. Store this id also to the properties object. + // The global unique id makes sure that the same view can be rendered on e.g. different machines and + // still be associated to the same object among all those clients. This is necessary for real-time + // collaboration mechanism. + options.id = options.id || joint.util.guid(this); - var delta; - if (isFinite(value)) { - var refOrigin = refBBox[origin](); - if (valuePercentage || value > 0 && value < 1) { - delta = refOrigin[axis] + refBBox[dimension] * value; - } else { - delta = refOrigin[axis] + value; - } - } + joint.mvc.View.call(this, options); + }, - var point = g.Point(); - point[axis] = delta || 0; - return point; - }; - } + init: function() { - function offsetWrapper(axis, dimension, corner) { - return function(value, nodeBBox) { - var delta; - if (value === 'middle') { - delta = nodeBBox[dimension] / 2; - } else if (value === corner) { - delta = nodeBBox[dimension]; - } else if (isFinite(value)) { - // TODO: or not to do a breaking change? - delta = (value > -1 && value < 1) ? (-nodeBBox[dimension] * value) : -value; - } else if (isPercentage(value)) { - delta = nodeBBox[dimension] * parseFloat(value) / 100; - } else { - delta = 0; - } + joint.util.bindAll(this, 'remove', 'update'); - var point = g.Point(); - point[axis] = -(nodeBBox[axis] + delta); - return point; - }; - } + // Store reference to this to the <g> DOM element so that the view is accessible through the DOM tree. + this.$el.data('view', this); - var attributesNS = joint.dia.attributes = { + // Add the cell's type to the view's element as a data attribute. + this.$el.attr('data-type', this.model.get('type')); - xlinkHref: { - set: 'xlink:href' - }, + this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); + }, - xlinkShow: { - set: 'xlink:show' - }, + onChangeAttrs: function(cell, attrs, opt) { - xlinkRole: { - set: 'xlink:role' - }, + if (opt.dirty) { - xlinkType: { - set: 'xlink:type' - }, + // dirty flag could be set when a model attribute was removed and it needs to be cleared + // also from the DOM element. See cell.removeAttr(). + return this.render(); + } - xlinkArcrole: { - set: 'xlink:arcrole' - }, + return this.update(cell, attrs, opt); + }, - xlinkTitle: { - set: 'xlink:title' - }, + // Return `true` if cell link is allowed to perform a certain UI `feature`. + // Example: `can('vertexMove')`, `can('labelMove')`. + can: function(feature) { - xlinkActuate: { - set: 'xlink:actuate' - }, + var interactive = joint.util.isFunction(this.options.interactive) + ? this.options.interactive(this) + : this.options.interactive; - xmlSpace: { - set: 'xml:space' - }, + return (joint.util.isObject(interactive) && interactive[feature] !== false) || + (joint.util.isBoolean(interactive) && interactive !== false); + }, - xmlBase: { - set: 'xml:base' - }, + findBySelector: function(selector, root, selectors) { - xmlLang: { - set: 'xml:lang' - }, + root || (root = this.el); + selectors || (selectors = this.selectors); - preserveAspectRatio: { - set: 'preserveAspectRatio' - }, + // These are either descendants of `this.$el` of `this.$el` itself. + // `.` is a special selector used to select the wrapping `<g>` element. + if (!selector || selector === '.') return [root]; + if (selectors && selectors[selector]) return [selectors[selector]]; + // Maintaining backwards compatibility + // e.g. `circle:first` would fail with querySelector() call + return $(root).find(selector).toArray(); + }, - requiredExtension: { - set: 'requiredExtension' - }, + notify: function(eventName) { - requiredFeatures: { - set: 'requiredFeatures' - }, + if (this.paper) { - systemLanguage: { - set: 'systemLanguage' - }, + var args = Array.prototype.slice.call(arguments, 1); - externalResourcesRequired: { - set: 'externalResourceRequired' - }, + // Trigger the event on both the element itself and also on the paper. + this.trigger.apply(this, [eventName].concat(args)); - filter: { - qualify: util.isPlainObject, - set: function(filter) { - return 'url(#' + this.paper.defineFilter(filter) + ')'; - } - }, + // Paper event handlers receive the view object as the first argument. + this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); + } + }, - fill: { - qualify: util.isPlainObject, - set: function(fill) { - return 'url(#' + this.paper.defineGradient(fill) + ')'; - } - }, + // ** Deprecated ** + getStrokeBBox: function(el) { + // Return a bounding box rectangle that takes into account stroke. + // Note that this is a naive and ad-hoc implementation that does not + // works only in certain cases and should be replaced as soon as browsers will + // start supporting the getStrokeBBox() SVG method. + // @TODO any better solution is very welcome! - stroke: { - qualify: util.isPlainObject, - set: function(stroke) { - return 'url(#' + this.paper.defineGradient(stroke) + ')'; - } - }, + var isMagnet = !!el; - sourceMarker: { - qualify: util.isPlainObject, - set: function(marker) { - return { 'marker-start': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + el = el || this.el; + var bbox = V(el).getBBox({ target: this.paper.viewport }); + var strokeWidth; + if (isMagnet) { - targetMarker: { - qualify: util.isPlainObject, - set: function(marker) { - marker = util.assign({ transform: 'rotate(180)' }, marker); - return { 'marker-end': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + strokeWidth = V(el).attr('stroke-width'); - vertexMarker: { - qualify: util.isPlainObject, - set: function(marker) { - return { 'marker-mid': 'url(#' + this.paper.defineMarker(marker) + ')' }; - } - }, + } else { - text: { - set: function(text, refBBox, node, attrs) { - var $node = $(node); - var cacheName = 'joint-text'; - var cache = $node.data(cacheName); - var textAttrs = joint.util.pick(attrs, 'lineHeight', 'annotations', 'textPath', 'x', 'eol'); - var fontSize = textAttrs.fontSize = attrs['font-size'] || attrs['fontSize']; - var textHash = JSON.stringify([text, textAttrs]); - // Update the text only if there was a change in the string - // or any of its attributes. - if (cache === undefined || cache !== textHash) { - // Chrome bug: - // Tspans positions defined as `em` are not updated - // when container `font-size` change. - if (fontSize) { - node.setAttribute('font-size', fontSize); - } - V(node).text('' + text, textAttrs); - $node.data(cacheName, textHash); - } - } - }, + strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); + } - textWrap: { - qualify: util.isPlainObject, - set: function(value, refBBox, node, attrs) { - // option `width` - var width = value.width || 0; - if (isPercentage(width)) { - refBBox.width *= parseFloat(width) / 100; - } else if (width <= 0) { - refBBox.width += width; - } else { - refBBox.width = width; - } - // option `height` - var height = value.height || 0; - if (isPercentage(height)) { - refBBox.height *= parseFloat(height) / 100; - } else if (height <= 0) { - refBBox.height += height; - } else { - refBBox.height = height; - } - // option `text` - var wrappedText = joint.util.breakText('' + value.text, refBBox, { - 'font-weight': attrs['font-weight'] || attrs.fontWeight, - 'font-size': attrs['font-size'] || attrs.fontSize, - 'font-family': attrs['font-family'] || attrs.fontFamily - }, { - // Provide an existing SVG Document here - // instead of creating a temporary one over again. - svgDocument: this.paper.svg - }); + strokeWidth = parseFloat(strokeWidth) || 0; - V(node).text(wrappedText); - } - }, + return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); + }, - lineHeight: { - qualify: function(lineHeight, node, attrs) { - return (attrs.text !== undefined); - } - }, + getBBox: function() { - textPath: { - qualify: function(textPath, node, attrs) { - return (attrs.text !== undefined); - } - }, + return this.vel.getBBox({ target: this.paper.svg }); + }, - annotations: { - qualify: function(annotations, node, attrs) { - return (attrs.text !== undefined); - } - }, + highlight: function(el, opt) { - // `port` attribute contains the `id` of the port that the underlying magnet represents. - port: { - set: function(port) { - return (port === null || port.id === undefined) ? port : port.id; - } - }, + el = !el ? this.el : this.$(el)[0] || this.el; - // `style` attribute is special in the sense that it sets the CSS style of the subelement. - style: { - qualify: util.isPlainObject, - set: function(styles, refBBox, node) { - $(node).css(styles); - } - }, + // set partial flag if the highlighted element is not the entire view. + opt = opt || {}; + opt.partial = (el !== this.el); - html: { - set: function(html, refBBox, node) { - $(node).html(html + ''); - } - }, + this.notify('cell:highlight', el, opt); + return this; + }, - ref: { - // We do not set `ref` attribute directly on an element. - // The attribute itself does not qualify for relative positioning. - }, + unhighlight: function(el, opt) { - // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width - // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box - // otherwise, `refX` is the left coordinate of the bounding box + el = !el ? this.el : this.$(el)[0] || this.el; - refX: { - position: positionWrapper('x', 'width', 'origin') - }, + opt = opt || {}; + opt.partial = el != this.el; - refY: { - position: positionWrapper('y', 'height', 'origin') - }, + this.notify('cell:unhighlight', el, opt); + return this; + }, - // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom - // coordinate of the reference element. + // Find the closest element that has the `magnet` attribute set to `true`. If there was not such + // an element found, return the root element of the cell view. + findMagnet: function(el) { - refDx: { - position: positionWrapper('x', 'width', 'corner') - }, + var $el = this.$(el); + var $rootEl = this.$el; - refDy: { - position: positionWrapper('y', 'height', 'corner') - }, + if ($el.length === 0) { + $el = $rootEl; + } - // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to - // the reference element size - // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width - // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 + do { - refWidth: { - set: setWrapper('width', 'width') - }, + var magnet = $el.attr('magnet'); + if ((magnet || $el.is($rootEl)) && magnet !== 'false') { + return $el[0]; + } - refHeight: { - set: setWrapper('height', 'height') - }, + $el = $el.parent(); - refRx: { - set: setWrapper('rx', 'width') - }, + } while ($el.length > 0); - refRy: { - set: setWrapper('ry', 'height') - }, + // If the overall cell has set `magnet === false`, then return `undefined` to + // announce there is no magnet found for this cell. + // This is especially useful to set on cells that have 'ports'. In this case, + // only the ports have set `magnet === true` and the overall element has `magnet === false`. + return undefined; + }, - refCx: { - set: setWrapper('cx', 'width') - }, + // Construct a unique selector for the `el` element within this view. + // `prevSelector` is being collected through the recursive call. + // No value for `prevSelector` is expected when using this method. + getSelector: function(el, prevSelector) { - refCy: { - set: setWrapper('cy', 'height') - }, + if (el === this.el) { + return prevSelector; + } - // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. - // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. + var selector; - xAlignment: { - offset: offsetWrapper('x', 'width', 'right') - }, + if (el) { - // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. - // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. + var nthChild = V(el).index() + 1; + selector = el.tagName + ':nth-child(' + nthChild + ')'; - yAlignment: { - offset: offsetWrapper('y', 'height', 'bottom') - }, + if (prevSelector) { + selector += ' > ' + prevSelector; + } - resetOffset: { - offset: function(val, nodeBBox) { - return (val) - ? { x: -nodeBBox.x, y: -nodeBBox.y } - : { x: 0, y: 0 }; + selector = this.getSelector(el.parentNode, selector); + } + + return selector; + }, + + getLinkEnd: function(magnet, x, y, link, endType) { + + var model = this.model; + var id = model.id; + var port = this.findAttribute('port', magnet); + // Find a unique `selector` of the element under pointer that is a magnet. + var selector = magnet.getAttribute('joint-selector'); + + var end = { id: id }; + if (selector != null) end.magnet = selector; + if (port != null) { + end.port = port; + if (!model.hasPort(port) && !selector) { + // port created via the `port` attribute (not API) + end.selector = this.getSelector(magnet); } + } else if (selector == null && this.el !== magnet) { + end.selector = this.getSelector(magnet); + } + var paper = this.paper; + var connectionStrategy = paper.options.connectionStrategy; + if (typeof connectionStrategy === 'function') { + var strategy = connectionStrategy.call(paper, end, this, magnet, new g.Point(x, y), link, endType); + if (strategy) end = strategy; } - }; - // This allows to combine both absolute and relative positioning - // refX: 50%, refX2: 20 - attributesNS.refX2 = attributesNS.refX; - attributesNS.refY2 = attributesNS.refY; + return end; + }, - // Aliases for backwards compatibility - attributesNS['ref-x'] = attributesNS.refX; - attributesNS['ref-y'] = attributesNS.refY; - attributesNS['ref-dy'] = attributesNS.refDy; - attributesNS['ref-dx'] = attributesNS.refDx; - attributesNS['ref-width'] = attributesNS.refWidth; - attributesNS['ref-height'] = attributesNS.refHeight; - attributesNS['x-alignment'] = attributesNS.xAlignment; - attributesNS['y-alignment'] = attributesNS.yAlignment; + getMagnetFromLinkEnd: function(end) { -})(joint, _, g, $, joint.util); + var root = this.el; + var port = end.port; + var selector = end.magnet; + var magnet; + if (port != null && this.model.hasPort(port)) { + magnet = this.findPortNode(port, selector) || root; + } else { + magnet = this.findBySelector(selector || end.selector, root, this.selectors)[0]; + } + return magnet; + }, -// joint.dia.Cell base model. -// -------------------------- + findAttribute: function(attributeName, node) { -joint.dia.Cell = Backbone.Model.extend({ + if (!node) return null; - // This is the same as Backbone.Model with the only difference that is uses joint.util.merge - // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. - constructor: function(attributes, options) { + var attributeValue = node.getAttribute(attributeName); + if (attributeValue === null) { + if (node === this.el) return null; + var currentNode = node.parentNode; + while (currentNode && currentNode !== this.el && currentNode.nodeType === 1) { + attributeValue = currentNode.getAttribute(attributeName) + if (attributeValue !== null) break; + currentNode = currentNode.parentNode; + } + } + return attributeValue; + }, - var defaults; - var attrs = attributes || {}; - this.cid = joint.util.uniqueId('c'); - this.attributes = {}; - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attrs = this.parse(attrs, options) || {}; - if ((defaults = joint.util.result(this, 'defaults'))) { - //<custom code> - // Replaced the call to _.defaults with joint.util.merge. - attrs = joint.util.merge({}, defaults, attrs); - //</custom code> + getAttributeDefinition: function(attrName) { + + return this.model.constructor.getAttributeDefinition(attrName); + }, + + setNodeAttributes: function(node, attrs) { + + if (!joint.util.isEmpty(attrs)) { + if (node instanceof SVGElement) { + V(node).attr(attrs); + } else { + $(node).attr(attrs); + } } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); }, - translate: function(dx, dy, opt) { + processNodeAttributes: function(node, attrs) { - throw new Error('Must define a translate() method.'); + var attrName, attrVal, def, i, n; + var normalAttrs, setAttrs, positionAttrs, offsetAttrs; + var relatives = []; + // divide the attributes between normal and special + for (attrName in attrs) { + if (!attrs.hasOwnProperty(attrName)) continue; + attrVal = attrs[attrName]; + def = this.getAttributeDefinition(attrName); + if (def && (!joint.util.isFunction(def.qualify) || def.qualify.call(this, attrVal, node, attrs))) { + if (joint.util.isString(def.set)) { + normalAttrs || (normalAttrs = {}); + normalAttrs[def.set] = attrVal; + } + if (attrVal !== null) { + relatives.push(attrName, def); + } + } else { + normalAttrs || (normalAttrs = {}); + normalAttrs[joint.util.toKebabCase(attrName)] = attrVal; + } + } + + // handle the rest of attributes via related method + // from the special attributes namespace. + for (i = 0, n = relatives.length; i < n; i+=2) { + attrName = relatives[i]; + def = relatives[i+1]; + attrVal = attrs[attrName]; + if (joint.util.isFunction(def.set)) { + setAttrs || (setAttrs = {}); + setAttrs[attrName] = attrVal; + } + if (joint.util.isFunction(def.position)) { + positionAttrs || (positionAttrs = {}); + positionAttrs[attrName] = attrVal; + } + if (joint.util.isFunction(def.offset)) { + offsetAttrs || (offsetAttrs = {}); + offsetAttrs[attrName] = attrVal; + } + } + + return { + raw: attrs, + normal: normalAttrs, + set: setAttrs, + position: positionAttrs, + offset: offsetAttrs + }; }, - toJSON: function() { - - var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; - var attrs = this.attributes.attrs; - var finalAttrs = {}; + updateRelativeAttributes: function(node, attrs, refBBox, opt) { - // Loop through all the attributes and - // omit the default attributes as they are implicitly reconstructable by the cell 'type'. - joint.util.forIn(attrs, function(attr, selector) { + opt || (opt = {}); - var defaultAttr = defaultAttrs[selector]; + var attrName, attrVal, def; + var rawAttrs = attrs.raw || {}; + var nodeAttrs = attrs.normal || {}; + var setAttrs = attrs.set; + var positionAttrs = attrs.position; + var offsetAttrs = attrs.offset; - joint.util.forIn(attr, function(value, name) { + for (attrName in setAttrs) { + attrVal = setAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // SET - set function should return attributes to be set on the node, + // which will affect the node dimensions based on the reference bounding + // box. e.g. `width`, `height`, `d`, `rx`, `ry`, `points + var setResult = def.set.call(this, attrVal, refBBox.clone(), node, rawAttrs); + if (joint.util.isObject(setResult)) { + joint.util.assign(nodeAttrs, setResult); + } else if (setResult !== undefined) { + nodeAttrs[attrName] = setResult; + } + } - // attr is mainly flat though it might have one more level (consider the `style` attribute). - // Check if the `value` is object and if yes, go one level deep. - if (joint.util.isObject(value) && !Array.isArray(value)) { + if (node instanceof HTMLElement) { + // TODO: setting the `transform` attribute on HTMLElements + // via `node.style.transform = 'matrix(...)';` would introduce + // a breaking change (e.g. basic.TextBlock). + this.setNodeAttributes(node, nodeAttrs); + return; + } - joint.util.forIn(value, function(value2, name2) { + // The final translation of the subelement. + var nodeTransform = nodeAttrs.transform; + var nodeMatrix = V.transformStringToMatrix(nodeTransform); + var nodePosition = g.Point(nodeMatrix.e, nodeMatrix.f); + if (nodeTransform) { + nodeAttrs = joint.util.omit(nodeAttrs, 'transform'); + nodeMatrix.e = nodeMatrix.f = 0; + } - if (!defaultAttr || !defaultAttr[name] || !joint.util.isEqual(defaultAttr[name][name2], value2)) { + // Calculate node scale determined by the scalable group + // only if later needed. + var sx, sy, translation; + if (positionAttrs || offsetAttrs) { + var nodeScale = this.getNodeScale(node, opt.scalableNode); + sx = nodeScale.sx; + sy = nodeScale.sy; + } - finalAttrs[selector] = finalAttrs[selector] || {}; - (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; - } - }); + var positioned = false; + for (attrName in positionAttrs) { + attrVal = positionAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // POSITION - position function should return a point from the + // reference bounding box. The default position of the node is x:0, y:0 of + // the reference bounding box or could be further specify by some + // SVG attributes e.g. `x`, `y` + translation = def.position.call(this, attrVal, refBBox.clone(), node, rawAttrs); + if (translation) { + nodePosition.offset(g.Point(translation).scale(sx, sy)); + positioned || (positioned = true); + } + } - } else if (!defaultAttr || !joint.util.isEqual(defaultAttr[name], value)) { - // `value` is not an object, default attribute for such a selector does not exist - // or it is different than the attribute value set on the model. + // The node bounding box could depend on the `size` set from the previous loop. + // Here we know, that all the size attributes have been already set. + this.setNodeAttributes(node, nodeAttrs); - finalAttrs[selector] = finalAttrs[selector] || {}; - finalAttrs[selector][name] = value; + var offseted = false; + if (offsetAttrs) { + // Check if the node is visible + var nodeClientRect = node.getBoundingClientRect(); + if (nodeClientRect.width > 0 && nodeClientRect.height > 0) { + var nodeBBox = V.transformRect(node.getBBox(), nodeMatrix).scale(1 / sx, 1 / sy); + for (attrName in offsetAttrs) { + attrVal = offsetAttrs[attrName]; + def = this.getAttributeDefinition(attrName); + // OFFSET - offset function should return a point from the element + // bounding box. The default offset point is x:0, y:0 (origin) or could be further + // specify with some SVG attributes e.g. `text-anchor`, `cx`, `cy` + translation = def.offset.call(this, attrVal, nodeBBox, node, rawAttrs); + if (translation) { + nodePosition.offset(g.Point(translation).scale(sx, sy)); + offseted || (offseted = true); + } } - }); - }); - - var attributes = joint.util.cloneDeep(joint.util.omit(this.attributes, 'attrs')); - //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); - attributes.attrs = finalAttrs; + } + } - return attributes; + // Do not touch node's transform attribute if there is no transformation applied. + if (nodeTransform !== undefined || positioned || offseted) { + // Round the coordinates to 1 decimal point. + nodePosition.round(1); + nodeMatrix.e = nodePosition.x; + nodeMatrix.f = nodePosition.y; + node.setAttribute('transform', V.matrixToTransformString(nodeMatrix)); + // TODO: store nodeMatrix metrics? + } }, - initialize: function(options) { - - if (!options || !options.id) { + getNodeScale: function(node, scalableNode) { - this.set('id', joint.util.uuid(), { silent: true }); + // Check if the node is a descendant of the scalable group. + var sx, sy; + if (scalableNode && scalableNode.contains(node)) { + var scale = scalableNode.scale(); + sx = 1 / scale.sx; + sy = 1 / scale.sy; + } else { + sx = 1; + sy = 1; } - this._transitionIds = {}; - - // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. - this.processPorts(); - this.on('change:attrs', this.processPorts, this); + return { sx: sx, sy: sy }; }, - /** - * @deprecated - */ - processPorts: function() { + findNodesAttributes: function(attrs, root, selectorCache, selectors) { - // Whenever `attrs` changes, we extract ports from the `attrs` object and store it - // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` - // set to that port, we remove those links as well (to follow the same behaviour as - // with a removed element). + // TODO: merge attributes in order defined by `index` property - var previousPorts = this.ports; + // most browsers sort elements in attrs by order of addition + // which is useful but not required - // Collect ports from the `attrs` object. - var ports = {}; - joint.util.forIn(this.get('attrs'), function(attrs, selector) { + // link.updateLabels() relies on that assumption for merging label attrs over default label attrs - if (attrs && attrs.port) { + var nodesAttrs = {}; - // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). - if (attrs.port.id !== undefined) { - ports[attrs.port.id] = attrs.port; + for (var selector in attrs) { + if (!attrs.hasOwnProperty(selector)) continue; + var selected = selectorCache[selector] = this.findBySelector(selector, root, selectors); + for (var i = 0, n = selected.length; i < n; i++) { + var node = selected[i]; + var nodeId = V.ensureId(node); + var nodeAttrs = attrs[selector]; + var prevNodeAttrs = nodesAttrs[nodeId]; + if (prevNodeAttrs) { + if (!prevNodeAttrs.merged) { + prevNodeAttrs.merged = true; + // if prevNode attrs is `null`, replace with `{}` + prevNodeAttrs.attributes = joint.util.cloneDeep(prevNodeAttrs.attributes) || {}; + } + // if prevNode attrs not set (or `null` or`{}`), use node attrs + // if node attrs not set (or `null` or `{}`), use prevNode attrs + joint.util.merge(prevNodeAttrs.attributes, nodeAttrs); } else { - ports[attrs.port] = { id: attrs.port }; + nodesAttrs[nodeId] = { + attributes: nodeAttrs, + node: node, + merged: false + }; } } - }); - - // Collect ports that have been removed (compared to the previous ports) - if any. - // Use hash table for quick lookup. - var removedPorts = {}; - joint.util.forIn(previousPorts, function(port, id) { - - if (!ports[id]) removedPorts[id] = true; - }); - - // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. - if (this.graph && !joint.util.isEmpty(removedPorts)) { - - var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); - inboundLinks.forEach(function(link) { - - if (removedPorts[link.get('target').port]) link.remove(); - }); - - var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); - outboundLinks.forEach(function(link) { - - if (removedPorts[link.get('source').port]) link.remove(); - }); } - // Update the `ports` object. - this.ports = ports; + return nodesAttrs; }, - remove: function(opt) { - - opt = opt || {}; - - // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. - var graph = this.graph; - if (graph) { - graph.startBatch('remove'); - } - - // First, unembed this cell from its parent cell if there is one. - var parentCellId = this.get('parent'); - if (parentCellId) { - - var parentCell = graph && graph.getCell(parentCellId); - parentCell.unembed(this); - } - - joint.util.invoke(this.getEmbeddedCells(), 'remove', opt); + // Default is to process the `model.attributes.attrs` object and set attributes on subelements based on the selectors, + // unless `attrs` parameter was passed. + updateDOMSubtreeAttributes: function(rootNode, attrs, opt) { - this.trigger('remove', this, this.collection, opt); + opt || (opt = {}); + opt.rootBBox || (opt.rootBBox = g.Rect()); + opt.selectors || (opt.selectors = this.selectors); // selector collection to use - if (graph) { - graph.stopBatch('remove'); - } + // Cache table for query results and bounding box calculation. + // Note that `selectorCache` needs to be invalidated for all + // `updateAttributes` calls, as the selectors might pointing + // to nodes designated by an attribute or elements dynamically + // created. + var selectorCache = {}; + var bboxCache = {}; + var relativeItems = []; + var item, node, nodeAttrs, nodeData, processedAttrs; - return this; - }, + var roAttrs = opt.roAttributes; + var nodesAttrs = this.findNodesAttributes(roAttrs || attrs, rootNode, selectorCache, opt.selectors); + // `nodesAttrs` are different from all attributes, when + // rendering only attributes sent to this method. + var nodesAllAttrs = (roAttrs) + ? nodesAllAttrs = this.findNodesAttributes(attrs, rootNode, selectorCache, opt.selectors) + : nodesAttrs; - toFront: function(opt) { + for (var nodeId in nodesAttrs) { + nodeData = nodesAttrs[nodeId]; + nodeAttrs = nodeData.attributes; + node = nodeData.node; + processedAttrs = this.processNodeAttributes(node, nodeAttrs); - if (this.graph) { + if (!processedAttrs.set && !processedAttrs.position && !processedAttrs.offset) { + // Set all the normal attributes right on the SVG/HTML element. + this.setNodeAttributes(node, processedAttrs.normal); - opt = opt || {}; + } else { - var z = (this.graph.getLastCell().get('z') || 0) + 1; + var nodeAllAttrs = nodesAllAttrs[nodeId] && nodesAllAttrs[nodeId].attributes; + var refSelector = (nodeAllAttrs && (nodeAttrs.ref === undefined)) + ? nodeAllAttrs.ref + : nodeAttrs.ref; - this.startBatch('to-front').set('z', z, opt); + var refNode; + if (refSelector) { + refNode = (selectorCache[refSelector] || this.findBySelector(refSelector, rootNode, opt.selectors))[0]; + if (!refNode) { + throw new Error('dia.ElementView: "' + refSelector + '" reference does not exist.'); + } + } else { + refNode = null; + } - if (opt.deep) { + item = { + node: node, + refNode: refNode, + processedAttributes: processedAttrs, + allAttributes: nodeAllAttrs + }; - var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - cells.forEach(function(cell) { cell.set('z', ++z, opt); }); + // If an element in the list is positioned relative to this one, then + // we want to insert this one before it in the list. + var itemIndex = relativeItems.findIndex(function(item) { + return item.refNode === node; + }); + if (itemIndex > -1) { + relativeItems.splice(itemIndex, 0, item); + } else { + relativeItems.push(item); + } } - - this.stopBatch('to-front'); } - return this; - }, + for (var i = 0, n = relativeItems.length; i < n; i++) { + item = relativeItems[i]; + node = item.node; + refNode = item.refNode; - toBack: function(opt) { + // Find the reference element bounding box. If no reference was provided, we + // use the optional bounding box. + var refNodeId = refNode ? V.ensureId(refNode) : ''; + var refBBox = bboxCache[refNodeId]; + if (!refBBox) { + // Get the bounding box of the reference element relative to the `rotatable` `<g>` (without rotation) + // or to the root `<g>` element if no rotatable group present if reference node present. + // Uses the bounding box provided. + refBBox = bboxCache[refNodeId] = (refNode) + ? V(refNode).getBBox({ target: (opt.rotatableNode || rootNode) }) + : opt.rootBBox; + } - if (this.graph) { + if (roAttrs) { + // if there was a special attribute affecting the position amongst passed-in attributes + // we have to merge it with the rest of the element's attributes as they are necessary + // to update the position relatively (i.e `ref-x` && 'ref-dx') + processedAttrs = this.processNodeAttributes(node, item.allAttributes); + this.mergeProcessedAttributes(processedAttrs, item.processedAttributes); - opt = opt || {}; + } else { + processedAttrs = item.processedAttributes; + } - var z = (this.graph.getFirstCell().get('z') || 0) - 1; + this.updateRelativeAttributes(node, processedAttrs, refBBox, opt); + } + }, - this.startBatch('to-back'); + mergeProcessedAttributes: function(processedAttrs, roProcessedAttrs) { - if (opt.deep) { + processedAttrs.set || (processedAttrs.set = {}); + processedAttrs.position || (processedAttrs.position = {}); + processedAttrs.offset || (processedAttrs.offset = {}); - var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - cells.reverse().forEach(function(cell) { cell.set('z', z--, opt); }); - } + joint.util.assign(processedAttrs.set, roProcessedAttrs.set); + joint.util.assign(processedAttrs.position, roProcessedAttrs.position); + joint.util.assign(processedAttrs.offset, roProcessedAttrs.offset); - this.set('z', z, opt).stopBatch('to-back'); + // Handle also the special transform property. + var transform = processedAttrs.normal && processedAttrs.normal.transform; + if (transform !== undefined && roProcessedAttrs.normal) { + roProcessedAttrs.normal.transform = transform; } + processedAttrs.normal = roProcessedAttrs.normal; + }, - return this; + onRemove: function() { + this.removeTools(); }, - embed: function(cell, opt) { + _toolsView: null, - if (this === cell || this.isEmbeddedIn(cell)) { + hasTools: function(name) { + var toolsView = this._toolsView; + if (!toolsView) return false; + if (!name) return true; + return (toolsView.getName() === name); + }, - throw new Error('Recursive embedding not allowed.'); + addTools: function(toolsView) { - } else { + this.removeTools(); - this.startBatch('embed'); + if (toolsView instanceof joint.dia.ToolsView) { + this._toolsView = toolsView; + toolsView.configure({ relatedView: this }); + toolsView.listenTo(this.paper, 'tools:event', this.onToolEvent.bind(this)); + toolsView.mount(); + } + return this; + }, - var embeds = joint.util.assign([], this.get('embeds')); + updateTools: function(opt) { - // We keep all element ids after link ids. - embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); + var toolsView = this._toolsView; + if (toolsView) toolsView.update(opt); + return this; + }, - cell.set('parent', this.id, opt); - this.set('embeds', joint.util.uniq(embeds), opt); + removeTools: function() { - this.stopBatch('embed'); + var toolsView = this._toolsView; + if (toolsView) { + toolsView.remove(); + this._toolsView = null; } - return this; }, - unembed: function(cell, opt) { - - this.startBatch('unembed'); - - cell.unset('parent', opt); - this.set('embeds', joint.util.without(this.get('embeds'), cell.id), opt); - - this.stopBatch('unembed'); + hideTools: function() { + var toolsView = this._toolsView; + if (toolsView) toolsView.hide(); return this; }, - // Return an array of ancestor cells. - // The array is ordered from the parent of the cell - // to the most distant ancestor. - getAncestors: function() { + showTools: function() { - var ancestors = []; - var parentId = this.get('parent'); - - if (!this.graph) { - return ancestors; - } + var toolsView = this._toolsView; + if (toolsView) toolsView.show(); + return this; + }, - while (parentId !== undefined) { - var parent = this.graph.getCell(parentId); - if (parent !== undefined) { - ancestors.push(parent); - parentId = parent.get('parent'); - } else { + onToolEvent: function(event) { + switch (event) { + case 'remove': + this.removeTools(); + break; + case 'hide': + this.hideTools(); + break; + case 'show': + this.showTools(); break; - } } - - return ancestors; }, - getEmbeddedCells: function(opt) { - - opt = opt || {}; - - // Cell models can only be retrieved when this element is part of a collection. - // There is no way this element knows about other cells otherwise. - // This also means that calling e.g. `translate()` on an element with embeds before - // adding it to a graph does not translate its embeds. - if (this.graph) { - - var cells; - - if (opt.deep) { - - if (opt.breadthFirst) { + // Interaction. The controller part. + // --------------------------------- - // breadthFirst algorithm - cells = []; - var queue = this.getEmbeddedCells(); + // Interaction is handled by the paper and delegated to the view in interest. + // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. + // If necessary, real coordinates can be obtained from the `evt` event object. - while (queue.length > 0) { + // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, + // i.e. `joint.dia.Element` and `joint.dia.Link`. - var parent = queue.shift(); - cells.push(parent); - queue.push.apply(queue, parent.getEmbeddedCells()); - } + pointerdblclick: function(evt, x, y) { - } else { + this.notify('cell:pointerdblclick', evt, x, y); + }, - // depthFirst algorithm - cells = this.getEmbeddedCells(); - cells.forEach(function(cell) { - cells.push.apply(cells, cell.getEmbeddedCells(opt)); - }); - } + pointerclick: function(evt, x, y) { - } else { + this.notify('cell:pointerclick', evt, x, y); + }, - cells = joint.util.toArray(this.get('embeds')).map(this.graph.getCell, this.graph); - } + contextmenu: function(evt, x, y) { - return cells; - } - return []; + this.notify('cell:contextmenu', evt, x, y); }, - isEmbeddedIn: function(cell, opt) { + pointerdown: function(evt, x, y) { - var cellId = joint.util.isString(cell) ? cell : cell.id; - var parentId = this.get('parent'); + if (this.model.graph) { + this.model.startBatch('pointer'); + this._graph = this.model.graph; + } - opt = joint.util.defaults({ deep: true }, opt); + this.notify('cell:pointerdown', evt, x, y); + }, - // See getEmbeddedCells(). - if (this.graph && opt.deep) { + pointermove: function(evt, x, y) { - while (parentId) { - if (parentId === cellId) { - return true; - } - parentId = this.graph.getCell(parentId).get('parent'); - } + this.notify('cell:pointermove', evt, x, y); + }, - return false; + pointerup: function(evt, x, y) { - } else { + this.notify('cell:pointerup', evt, x, y); - // When this cell is not part of a collection check - // at least whether it's a direct child of given cell. - return parentId === cellId; + if (this._graph) { + // we don't want to trigger event on model as model doesn't + // need to be member of collection anymore (remove) + this._graph.stopBatch('pointer', { cell: this.model }); + delete this._graph; } }, - // Whether or not the cell is embedded in any other cell. - isEmbedded: function() { + mouseover: function(evt) { - return !!this.get('parent'); + this.notify('cell:mouseover', evt); }, - // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). - // Shallow cloning simply clones the cell and returns a new cell with different ID. - // Deep cloning clones the cell and all its embedded cells recursively. - clone: function(opt) { - - opt = opt || {}; + mouseout: function(evt) { - if (!opt.deep) { - // Shallow cloning. + this.notify('cell:mouseout', evt); + }, - var clone = Backbone.Model.prototype.clone.apply(this, arguments); - // We don't want the clone to have the same ID as the original. - clone.set('id', joint.util.uuid()); - // A shallow cloned element does not carry over the original embeds. - clone.unset('embeds'); - // And can not be embedded in any cell - // as the clone is not part of the graph. - clone.unset('parent'); + mouseenter: function(evt) { - return clone; + this.notify('cell:mouseenter', evt); + }, - } else { - // Deep cloning. + mouseleave: function(evt) { - // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. - return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); - } + this.notify('cell:mouseleave', evt); }, - // A convenient way to set nested properties. - // This method merges the properties you'd like to set with the ones - // stored in the cell and makes sure change events are properly triggered. - // You can either set a nested property with one object - // or use a property path. - // The most simple use case is: - // `cell.prop('name/first', 'John')` or - // `cell.prop({ name: { first: 'John' } })`. - // Nested arrays are supported too: - // `cell.prop('series/0/data/0/degree', 50)` or - // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. - prop: function(props, value, opt) { + mousewheel: function(evt, x, y, delta) { - var delim = '/'; - var isString = joint.util.isString(props); + this.notify('cell:mousewheel', evt, x, y, delta); + }, + + onevent: function(evt, eventName, x, y) { - if (isString || Array.isArray(props)) { - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. + this.notify(eventName, evt, x, y); + }, - if (arguments.length > 1) { + onmagnet: function() { - var path; - var pathArray; + // noop + }, - if (isString) { - path = props; - pathArray = path.split('/') - } else { - path = props.join(delim); - pathArray = props.slice(); - } + setInteractivity: function(value) { - var property = pathArray[0]; - var pathArrayLength = pathArray.length; + this.options.interactive = value; + } +}, { - opt = opt || {}; - opt.propertyPath = path; - opt.propertyValue = value; - opt.propertyPathArray = pathArray; + dispatchToolsEvent: function(paper, event) { + if ((typeof event === 'string') && (paper instanceof joint.dia.Paper)) { + paper.trigger('tools:event', event); + } + } +}); - if (pathArrayLength === 1) { - // Property is not nested. We can simply use `set()`. - return this.set(property, value, opt); - } - var update = {}; - // Initialize the nested object. Subobjects are either arrays or objects. - // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. - // Note that this imposes a limitation on object keys one can use with Inspector. - // Pure integer keys will cause issues and are therefore not allowed. - var initializer = update; - var prevProperty = property; +// joint.dia.Element base model. +// ----------------------------- - for (var i = 1; i < pathArrayLength; i++) { - var pathItem = pathArray[i]; - var isArrayIndex = Number.isFinite(isString ? Number(pathItem) : pathItem); - initializer = initializer[prevProperty] = isArrayIndex ? [] : {}; - prevProperty = pathItem; - } +joint.dia.Element = joint.dia.Cell.extend({ - // Fill update with the `value` on `path`. - update = joint.util.setByPath(update, pathArray, value, '/'); + defaults: { + position: { x: 0, y: 0 }, + size: { width: 1, height: 1 }, + angle: 0 + }, - var baseAttributes = joint.util.merge({}, this.attributes); - // if rewrite mode enabled, we replace value referenced by path with - // the new one (we don't merge). - opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); + initialize: function() { - // Merge update with the model attributes. - var attributes = joint.util.merge(baseAttributes, update); - // Finally, set the property to the updated attributes. - return this.set(property, attributes[property], opt); + this._initializePorts(); + joint.dia.Cell.prototype.initialize.apply(this, arguments); + }, - } else { + /** + * @abstract + */ + _initializePorts: function() { + // implemented in ports.js + }, - return joint.util.getByPath(this.attributes, props, delim); - } - } + isElement: function() { - return this.set(joint.util.merge({}, this.attributes, props), value); + return true; }, - // A convient way to unset nested properties - removeProp: function(path, opt) { + position: function(x, y, opt) { - // Once a property is removed from the `attrs` attribute - // the cellView will recognize a `dirty` flag and rerender itself - // in order to remove the attribute from SVG element. - opt = opt || {}; - opt.dirty = true; + var isSetter = joint.util.isNumber(y); - var pathArray = Array.isArray(path) ? path : path.split('/'); + opt = (isSetter ? opt : x) || {}; - if (pathArray.length === 1) { - // A top level property - return this.unset(path, opt); + // option `parentRelative` for setting the position relative to the element's parent. + if (opt.parentRelative) { + + // Getting the parent's position requires the collection. + // Cell.parent() holds cell id only. + if (!this.graph) throw new Error('Element must be part of a graph.'); + + var parent = this.getParentCell(); + var parentPosition = parent && !parent.isLink() + ? parent.get('position') + : { x: 0, y: 0 }; } - // A nested property - var property = pathArray[0]; - var nestedPath = pathArray.slice(1); - var propertyValue = joint.util.cloneDeep(this.get(property)); + if (isSetter) { - joint.util.unsetByPath(propertyValue, nestedPath, '/'); + if (opt.parentRelative) { + x += parentPosition.x; + y += parentPosition.y; + } - return this.set(property, propertyValue, opt); - }, + if (opt.deep) { + var currentPosition = this.get('position'); + this.translate(x - currentPosition.x, y - currentPosition.y, opt); + } else { + this.set('position', { x: x, y: y }, opt); + } - // A convenient way to set nested attributes. - attr: function(attrs, value, opt) { + return this; - var args = Array.from(arguments); - if (args.length === 0) { - return this.get('attrs'); + } else { // Getter returns a geometry point. + + var elementPosition = g.point(this.get('position')); + + return opt.parentRelative + ? elementPosition.difference(parentPosition) + : elementPosition; } + }, - if (Array.isArray(attrs)) { - args[0] = ['attrs'].concat(attrs); - } else if (joint.util.isString(attrs)) { - // Get/set an attribute by a special path syntax that delimits - // nested objects by the colon character. - args[0] = 'attrs/' + attrs; + translate: function(tx, ty, opt) { - } else { + tx = tx || 0; + ty = ty || 0; - args[0] = { 'attrs' : attrs }; + if (tx === 0 && ty === 0) { + // Like nothing has happened. + return this; } - return this.prop.apply(this, args); - }, + opt = opt || {}; + // Pass the initiator of the translation. + opt.translateBy = opt.translateBy || this.id; - // A convenient way to unset nested attributes - removeAttr: function(path, opt) { + var position = this.get('position') || { x: 0, y: 0 }; - if (Array.isArray(path)) { + if (opt.restrictedArea && opt.translateBy === this.id) { - return this.removeProp(['attrs'].concat(path)); + // We are restricting the translation for the element itself only. We get + // the bounding box of the element including all its embeds. + // All embeds have to be translated the exact same way as the element. + var bbox = this.getBBox({ deep: true }); + var ra = opt.restrictedArea; + //- - - - - - - - - - - - -> ra.x + ra.width + // - - - -> position.x | + // -> bbox.x + // ▓▓▓▓▓▓▓ | + // ░░░░░░░▓▓▓▓▓▓▓ + // ░░░░░░░░░ | + // ▓▓▓▓▓▓▓▓░░░░░░░ + // ▓▓▓▓▓▓▓▓ | + // <-dx-> | restricted area right border + // <-width-> | ░ translated element + // <- - bbox.width - -> ▓ embedded element + var dx = position.x - bbox.x; + var dy = position.y - bbox.y; + // Find the maximal/minimal coordinates that the element can be translated + // while complies the restrictions. + var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); + var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); + // recalculate the translation taking the resctrictions into account. + tx = x - position.x; + ty = y - position.y; } - return this.removeProp('attrs/' + path, opt); - }, + var translatedPosition = { + x: position.x + tx, + y: position.y + ty + }; - transition: function(path, value, opt, delim) { + // To find out by how much an element was translated in event 'change:position' handlers. + opt.tx = tx; + opt.ty = ty; - delim = delim || '/'; + if (opt.transition) { - var defaults = { - duration: 100, - delay: 10, - timingFunction: joint.util.timing.linear, - valueFunction: joint.util.interpolate.number - }; + if (!joint.util.isObject(opt.transition)) opt.transition = {}; - opt = joint.util.assign(defaults, opt); + this.transition('position', translatedPosition, joint.util.assign({}, opt.transition, { + valueFunction: joint.util.interpolate.object + })); - var firstFrameTime = 0; - var interpolatingFunction; + } else { - var setter = function(runtime) { + this.set('position', translatedPosition, opt); + } - var id, progress, propertyValue; + // Recursively call `translate()` on all the embeds cells. + joint.util.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); - firstFrameTime = firstFrameTime || runtime; - runtime -= firstFrameTime; - progress = runtime / opt.duration; + return this; + }, - if (progress < 1) { - this._transitionIds[path] = id = joint.util.nextFrame(setter); - } else { - progress = 1; - delete this._transitionIds[path]; - } + size: function(width, height, opt) { + + var currentSize = this.get('size'); + // Getter + // () signature + if (width === undefined) { + return { + width: currentSize.width, + height: currentSize.height + }; + } + // Setter + // (size, opt) signature + if (joint.util.isObject(width)) { + opt = height; + height = joint.util.isNumber(width.height) ? width.height : currentSize.height; + width = joint.util.isNumber(width.width) ? width.width : currentSize.width; + } + + return this.resize(width, height, opt); + }, + + resize: function(width, height, opt) { + + opt = opt || {}; + + this.startBatch('resize', opt); - propertyValue = interpolatingFunction(opt.timingFunction(progress)); + if (opt.direction) { - opt.transitionId = id; + var currentSize = this.get('size'); - this.prop(path, propertyValue, opt); + switch (opt.direction) { - if (!id) this.trigger('transition:end', this, path); + case 'left': + case 'right': + // Don't change height when resizing horizontally. + height = currentSize.height; + break; - }.bind(this); + case 'top': + case 'bottom': + // Don't change width when resizing vertically. + width = currentSize.width; + break; + } - var initiator = function(callback) { + // Get the angle and clamp its value between 0 and 360 degrees. + var angle = g.normalizeAngle(this.get('angle') || 0); - this.stopTransitions(path); + var quadrant = { + 'top-right': 0, + 'right': 0, + 'top-left': 1, + 'top': 1, + 'bottom-left': 2, + 'left': 2, + 'bottom-right': 3, + 'bottom': 3 + }[opt.direction]; - interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); + if (opt.absolute) { - this._transitionIds[path] = joint.util.nextFrame(callback); + // We are taking the element's rotation into account + quadrant += Math.floor((angle + 45) / 90); + quadrant %= 4; + } - this.trigger('transition:start', this, path); + // This is a rectangle in size of the unrotated element. + var bbox = this.getBBox(); - }.bind(this); + // Pick the corner point on the element, which meant to stay on its place before and + // after the rotation. + var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); - return setTimeout(initiator, opt.delay, setter); - }, + // Find an image of the previous indent point. This is the position, where is the + // point actually located on the screen. + var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); - getTransitions: function() { - return Object.keys(this._transitionIds); - }, + // Every point on the element rotates around a circle with the centre of rotation + // in the middle of the element while the whole element is being rotated. That means + // that the distance from a point in the corner of the element (supposed its always rect) to + // the center of the element doesn't change during the rotation and therefore it equals + // to a distance on unrotated element. + // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. + var radius = Math.sqrt((width * width) + (height * height)) / 2; - stopTransitions: function(path, delim) { + // Now we are looking for an angle between x-axis and the line starting at image of fixed point + // and ending at the center of the element. We call this angle `alpha`. - delim = delim || '/'; + // The image of a fixed point is located in n-th quadrant. For each quadrant passed + // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. + // + // 3 | 2 + // --c-- Quadrant positions around the element's center `c` + // 0 | 1 + // + var alpha = quadrant * Math.PI / 2; - var pathArray = path && path.split(delim); + // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis + // going through the center of the element) and line crossing the indent of the fixed point and the center + // of the element. This is the angle we need but on the unrotated element. + alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); - Object.keys(this._transitionIds).filter(pathArray && function(key) { + // Lastly we have to deduct the original angle the element was rotated by and that's it. + alpha -= g.toRad(angle); - return joint.util.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); + // With this angle and distance we can easily calculate the centre of the unrotated element. + // Note that fromPolar constructor accepts an angle in radians. + var center = g.point.fromPolar(radius, alpha, imageFixedPoint); - }).forEach(function(key) { + // The top left corner on the unrotated element has to be half a width on the left + // and half a height to the top from the center. This will be the origin of rectangle + // we were looking for. + var origin = g.point(center).offset(width / -2, height / -2); - joint.util.cancelFrame(this._transitionIds[key]); + // Resize the element (before re-positioning it). + this.set('size', { width: width, height: height }, opt); - delete this._transitionIds[key]; + // Finally, re-position the element. + this.position(origin.x, origin.y, opt); - this.trigger('transition:end', this, key); + } else { - }, this); + // Resize the element. + this.set('size', { width: width, height: height }, opt); + } + + this.stopBatch('resize', opt); return this; }, - // A shorcut making it easy to create constructs like the following: - // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. - addTo: function(graph, opt) { + scale: function(sx, sy, origin, opt) { - graph.addCell(this, opt); + var scaledBBox = this.getBBox().scale(sx, sy, origin); + this.startBatch('scale', opt); + this.position(scaledBBox.x, scaledBBox.y, opt); + this.resize(scaledBBox.width, scaledBBox.height, opt); + this.stopBatch('scale'); return this; }, - // A shortcut for an equivalent call: `paper.findViewByModel(cell)` - // making it easy to create constructs like the following: - // `cell.findView(paper).highlight()` - findView: function(paper) { + fitEmbeds: function(opt) { - return paper.findViewByModel(this); - }, + opt = opt || {}; - isElement: function() { + // Getting the children's size and position requires the collection. + // Cell.get('embdes') helds an array of cell ids only. + if (!this.graph) throw new Error('Element must be part of a graph.'); - return false; - }, + var embeddedCells = this.getEmbeddedCells(); - isLink: function() { + if (embeddedCells.length > 0) { - return false; - }, + this.startBatch('fit-embeds', opt); - startBatch: function(name, opt) { - if (this.graph) { this.graph.startBatch(name, joint.util.assign({}, opt, { cell: this })); } - return this; - }, + if (opt.deep) { + // Recursively apply fitEmbeds on all embeds first. + joint.util.invoke(embeddedCells, 'fitEmbeds', opt); + } - stopBatch: function(name, opt) { - if (this.graph) { this.graph.stopBatch(name, joint.util.assign({}, opt, { cell: this })); } - return this; - } + // Compute cell's size and position based on the children bbox + // and given padding. + var bbox = this.graph.getCellsBBox(embeddedCells); + var padding = joint.util.normalizeSides(opt.padding); -}, { + // Apply padding computed above to the bbox. + bbox.moveAndExpand({ + x: -padding.left, + y: -padding.top, + width: padding.right + padding.left, + height: padding.bottom + padding.top + }); - getAttributeDefinition: function(attrName) { + // Set new element dimensions finally. + this.set({ + position: { x: bbox.x, y: bbox.y }, + size: { width: bbox.width, height: bbox.height } + }, opt); - var defNS = this.attributes; - var globalDefNS = joint.dia.attributes; - return (defNS && defNS[attrName]) || globalDefNS[attrName]; + this.stopBatch('fit-embeds'); + } + + return this; }, - define: function(type, defaults, protoProps, staticProps) { + // Rotate element by `angle` degrees, optionally around `origin` point. + // If `origin` is not provided, it is considered to be the center of the element. + // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not + // the difference from the previous angle. + rotate: function(angle, absolute, origin, opt) { - protoProps = joint.util.assign({ - defaults: joint.util.defaultsDeep({ type: type }, defaults, this.prototype.defaults) - }, protoProps); + if (origin) { - var Cell = this.extend(protoProps, staticProps); - joint.util.setByPath(joint.shapes, type, Cell, '.'); - return Cell; - } -}); + var center = this.getBBox().center(); + var size = this.get('size'); + var position = this.get('position'); + center.rotate(origin, this.get('angle') - angle); + var dx = center.x - size.width / 2 - position.x; + var dy = center.y - size.height / 2 - position.y; + this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); + this.position(position.x + dx, position.y + dy, opt); + this.rotate(angle, absolute, null, opt); + this.stopBatch('rotate'); -// joint.dia.CellView base view and controller. -// -------------------------------------------- + } else { -// This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. + this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360, opt); + } -joint.dia.CellView = joint.mvc.View.extend({ + return this; + }, - tagName: 'g', + angle: function() { + return g.normalizeAngle(this.get('angle') || 0); + }, - svgElement: true, + getBBox: function(opt) { - className: function() { + opt = opt || {}; - var classNames = ['cell']; - var type = this.model.get('type'); + if (opt.deep && this.graph) { - if (type) { + // Get all the embedded elements using breadth first algorithm, + // that doesn't use recursion. + var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); + // Add the model itself. + elements.push(this); - type.toLowerCase().split('.').forEach(function(value, index, list) { - classNames.push('type-' + list.slice(0, index + 1).join('-')); - }); + return this.graph.getCellsBBox(elements); } - return classNames.join(' '); - }, + var position = this.get('position'); + var size = this.get('size'); - attributes: function() { + return new g.Rect(position.x, position.y, size.width, size.height); + } +}); - return { 'model-id': this.model.id }; - }, +// joint.dia.Element base view and controller. +// ------------------------------------------- - constructor: function(options) { +joint.dia.ElementView = joint.dia.CellView.extend({ - // Make sure a global unique id is assigned to this view. Store this id also to the properties object. - // The global unique id makes sure that the same view can be rendered on e.g. different machines and - // still be associated to the same object among all those clients. This is necessary for real-time - // collaboration mechanism. - options.id = options.id || joint.util.guid(this); + /** + * @abstract + */ + _removePorts: function() { + // implemented in ports.js + }, - joint.mvc.View.call(this, options); + /** + * + * @abstract + */ + _renderPorts: function() { + // implemented in ports.js }, - init: function() { - - joint.util.bindAll(this, 'remove', 'update'); + className: function() { - // Store reference to this to the <g> DOM element so that the view is accessible through the DOM tree. - this.$el.data('view', this); + var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); - // Add the cell's type to the view's element as a data attribute. - this.$el.attr('data-type', this.model.get('type')); + classNames.push('element'); - this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); + return classNames.join(' '); }, - onChangeAttrs: function(cell, attrs, opt) { + metrics: null, - if (opt.dirty) { + initialize: function() { - // dirty flag could be set when a model attribute was removed and it needs to be cleared - // also from the DOM element. See cell.removeAttr(). - return this.render(); - } + joint.dia.CellView.prototype.initialize.apply(this, arguments); - return this.update(cell, attrs, opt); - }, + var model = this.model; - // Return `true` if cell link is allowed to perform a certain UI `feature`. - // Example: `can('vertexMove')`, `can('labelMove')`. - can: function(feature) { + this.listenTo(model, 'change:position', this.translate); + this.listenTo(model, 'change:size', this.resize); + this.listenTo(model, 'change:angle', this.rotate); + this.listenTo(model, 'change:markup', this.render); - var interactive = joint.util.isFunction(this.options.interactive) - ? this.options.interactive(this) - : this.options.interactive; + this._initializePorts(); - return (joint.util.isObject(interactive) && interactive[feature] !== false) || - (joint.util.isBoolean(interactive) && interactive !== false); + this.metrics = {}; }, - findBySelector: function(selector, root) { + /** + * @abstract + */ + _initializePorts: function() { - var $root = $(root || this.el); - // These are either descendants of `this.$el` of `this.$el` itself. - // `.` is a special selector used to select the wrapping `<g>` element. - return (selector === '.') ? $root : $root.find(selector); }, - notify: function(eventName) { + update: function(cell, renderingOnlyAttrs) { - if (this.paper) { + this.metrics = {}; - var args = Array.prototype.slice.call(arguments, 1); + this._removePorts(); - // Trigger the event on both the element itself and also on the paper. - this.trigger.apply(this, [eventName].concat(args)); + var model = this.model; + var modelAttrs = model.attr(); + this.updateDOMSubtreeAttributes(this.el, modelAttrs, { + rootBBox: new g.Rect(model.size()), + selectors: this.selectors, + scalableNode: this.scalableNode, + rotatableNode: this.rotatableNode, + // Use rendering only attributes if they differs from the model attributes + roAttributes: (renderingOnlyAttrs === modelAttrs) ? null : renderingOnlyAttrs + }); - // Paper event handlers receive the view object as the first argument. - this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); - } + this._renderPorts(); }, - getStrokeBBox: function(el) { - // Return a bounding box rectangle that takes into account stroke. - // Note that this is a naive and ad-hoc implementation that does not - // works only in certain cases and should be replaced as soon as browsers will - // start supporting the getStrokeBBox() SVG method. - // @TODO any better solution is very welcome! + rotatableSelector: 'rotatable', + scalableSelector: 'scalable', + scalableNode: null, + rotatableNode: null, - var isMagnet = !!el; + // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the + // default markup is not desirable. + renderMarkup: function() { - el = el || this.el; - var bbox = V(el).getBBox({ target: this.paper.viewport }); + var element = this.model; + var markup = element.get('markup') || element.markup; + if (!markup) throw new Error('dia.ElementView: markup required'); + if (Array.isArray(markup)) return this.renderJSONMarkup(markup); + if (typeof markup === 'string') return this.renderStringMarkup(markup); + throw new Error('dia.ElementView: invalid markup'); + }, - var strokeWidth; - if (isMagnet) { + renderJSONMarkup: function(markup) { - strokeWidth = V(el).attr('stroke-width'); + var doc = joint.util.parseDOMJSON(markup); + // Selectors + var selectors = this.selectors = doc.selectors; + var rootSelector = this.selector; + if (selectors[rootSelector]) throw new Error('dia.ElementView: ambiguous root selector.'); + selectors[rootSelector] = this.el; + // Cache transformation groups + this.rotatableNode = V(selectors[this.rotatableSelector]) || null; + this.scalableNode = V(selectors[this.scalableSelector]) || null; + // Fragment + this.vel.append(doc.fragment); + }, - } else { + renderStringMarkup: function(markup) { - strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); - } + var vel = this.vel; + vel.append(V(markup)); + // Cache transformation groups + this.rotatableNode = vel.findOne('.rotatable'); + this.scalableNode = vel.findOne('.scalable'); - strokeWidth = parseFloat(strokeWidth) || 0; + var selectors = this.selectors = {}; + selectors[this.selector] = this.el; + }, - return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); + render: function() { + + this.vel.empty(); + this.renderMarkup(); + if (this.scalableNode) { + // Double update is necessary for elements with the scalable group only + // Note the resize() triggers the other `update`. + this.update(); + } + this.resize(); + if (this.rotatableNode) { + // Translate transformation is applied on `this.el` while the rotation transformation + // on `this.rotatableNode` + this.rotate(); + this.translate(); + return this; + } + this.updateTransformation(); + return this; }, - getBBox: function() { + resize: function() { - return this.vel.getBBox({ target: this.paper.svg }); + if (this.scalableNode) return this.sgResize.apply(this, arguments); + if (this.model.attributes.angle) this.rotate(); + this.update(); }, - highlight: function(el, opt) { + translate: function() { - el = !el ? this.el : this.$(el)[0] || this.el; + if (this.rotatableNode) return this.rgTranslate(); + this.updateTransformation(); + }, - // set partial flag if the highlighted element is not the entire view. - opt = opt || {}; - opt.partial = (el !== this.el); + rotate: function() { - this.notify('cell:highlight', el, opt); - return this; + if (this.rotatableNode) return this.rgRotate(); + this.updateTransformation(); }, - unhighlight: function(el, opt) { + updateTransformation: function() { - el = !el ? this.el : this.$(el)[0] || this.el; + var transformation = this.getTranslateString(); + var rotateString = this.getRotateString(); + if (rotateString) transformation += ' ' + rotateString; + this.vel.attr('transform', transformation); + }, - opt = opt || {}; - opt.partial = el != this.el; + getTranslateString: function() { - this.notify('cell:unhighlight', el, opt); - return this; + var position = this.model.attributes.position; + return 'translate(' + position.x + ',' + position.y + ')'; }, - // Find the closest element that has the `magnet` attribute set to `true`. If there was not such - // an element found, return the root element of the cell view. - findMagnet: function(el) { + getRotateString: function() { + var attributes = this.model.attributes; + var angle = attributes.angle; + if (!angle) return null; + var size = attributes.size; + return 'rotate(' + angle + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'; + }, - var $el = this.$(el); - var $rootEl = this.$el; + getBBox: function(opt) { - if ($el.length === 0) { - $el = $rootEl; + var bbox; + if (opt && opt.useModelGeometry) { + var model = this.model; + bbox = model.getBBox().bbox(model.angle()); + } else { + bbox = this.getNodeBBox(this.el); } - do { + return this.paper.localToPaperRect(bbox); + }, - var magnet = $el.attr('magnet'); - if ((magnet || $el.is($rootEl)) && magnet !== 'false') { - return $el[0]; - } + nodeCache: function(magnet) { - $el = $el.parent(); + var id = V.ensureId(magnet); + var metrics = this.metrics[id]; + if (!metrics) metrics = this.metrics[id] = {}; + return metrics; + }, - } while ($el.length > 0); + getNodeData: function(magnet) { - // If the overall cell has set `magnet === false`, then return `undefined` to - // announce there is no magnet found for this cell. - // This is especially useful to set on cells that have 'ports'. In this case, - // only the ports have set `magnet === true` and the overall element has `magnet === false`. - return undefined; + var metrics = this.nodeCache(magnet); + if (!metrics.data) metrics.data = {}; + return metrics.data; }, - // Construct a unique selector for the `el` element within this view. - // `prevSelector` is being collected through the recursive call. - // No value for `prevSelector` is expected when using this method. - getSelector: function(el, prevSelector) { + getNodeBBox: function(magnet) { - if (el === this.el) { - return prevSelector; - } + var rect = this.getNodeBoundingRect(magnet); + var magnetMatrix = this.getNodeMatrix(magnet); + var translateMatrix = this.getRootTranslateMatrix(); + var rotateMatrix = this.getRootRotateMatrix(); + return V.transformRect(rect, translateMatrix.multiply(rotateMatrix).multiply(magnetMatrix)); + }, - var selector; + getNodeBoundingRect: function(magnet) { - if (el) { + var metrics = this.nodeCache(magnet); + if (metrics.boundingRect === undefined) metrics.boundingRect = V(magnet).getBBox(); + return new g.Rect(metrics.boundingRect); + }, - var nthChild = V(el).index() + 1; - selector = el.tagName + ':nth-child(' + nthChild + ')'; + getNodeUnrotatedBBox: function(magnet) { - if (prevSelector) { - selector += ' > ' + prevSelector; - } + var rect = this.getNodeBoundingRect(magnet); + var magnetMatrix = this.getNodeMatrix(magnet); + var translateMatrix = this.getRootTranslateMatrix(); + return V.transformRect(rect, translateMatrix.multiply(magnetMatrix)); + }, - selector = this.getSelector(el.parentNode, selector); - } + getNodeShape: function(magnet) { - return selector; + var metrics = this.nodeCache(magnet); + if (metrics.geometryShape === undefined) metrics.geometryShape = V(magnet).toGeometryShape(); + return metrics.geometryShape.clone(); }, - getAttributeDefinition: function(attrName) { + getNodeMatrix: function(magnet) { - return this.model.constructor.getAttributeDefinition(attrName); + var metrics = this.nodeCache(magnet); + if (metrics.magnetMatrix === undefined) { + var target = this.rotatableNode || this.el; + metrics.magnetMatrix = V(magnet).getTransformToElement(target); + } + return V.createSVGMatrix(metrics.magnetMatrix); }, - setNodeAttributes: function(node, attrs) { + getRootTranslateMatrix: function() { - if (!joint.util.isEmpty(attrs)) { - if (node instanceof SVGElement) { - V(node).attr(attrs); - } else { - $(node).attr(attrs); - } - } + var model = this.model; + var position = model.position(); + var mt = V.createSVGMatrix().translate(position.x, position.y); + return mt; }, - processNodeAttributes: function(node, attrs) { + getRootRotateMatrix: function() { - var attrName, attrVal, def, i, n; - var normalAttrs, setAttrs, positionAttrs, offsetAttrs; - var relatives = []; - // divide the attributes between normal and special - for (attrName in attrs) { - if (!attrs.hasOwnProperty(attrName)) continue; - attrVal = attrs[attrName]; - def = this.getAttributeDefinition(attrName); - if (def && (!joint.util.isFunction(def.qualify) || def.qualify.call(this, attrVal, node, attrs))) { - if (joint.util.isString(def.set)) { - normalAttrs || (normalAttrs = {}); - normalAttrs[def.set] = attrVal; - } - if (attrVal !== null) { - relatives.push(attrName, def); - } - } else { - normalAttrs || (normalAttrs = {}); - normalAttrs[joint.util.toKebabCase(attrName)] = attrVal; - } + var mr = V.createSVGMatrix(); + var model = this.model; + var angle = model.angle(); + if (angle) { + var bbox = model.getBBox(); + var cx = bbox.width / 2; + var cy = bbox.height / 2; + mr = mr.translate(cx, cy).rotate(angle).translate(-cx, -cy); } + return mr; + }, - // handle the rest of attributes via related method - // from the special attributes namespace. - for (i = 0, n = relatives.length; i < n; i+=2) { - attrName = relatives[i]; - def = relatives[i+1]; - attrVal = attrs[attrName]; - if (joint.util.isFunction(def.set)) { - setAttrs || (setAttrs = {}); - setAttrs[attrName] = attrVal; - } - if (joint.util.isFunction(def.position)) { - positionAttrs || (positionAttrs = {}); - positionAttrs[attrName] = attrVal; - } - if (joint.util.isFunction(def.offset)) { - offsetAttrs || (offsetAttrs = {}); - offsetAttrs[attrName] = attrVal; - } - } + // Rotatable & Scalable Group + // always slower, kept mainly for backwards compatibility - return { - raw: attrs, - normal: normalAttrs, - set: setAttrs, - position: positionAttrs, - offset: offsetAttrs - }; + rgRotate: function() { + + this.rotatableNode.attr('transform', this.getRotateString()); }, - updateRelativeAttributes: function(node, attrs, refBBox, opt) { + rgTranslate: function() { - opt || (opt = {}); + this.vel.attr('transform', this.getTranslateString()); + }, - var attrName, attrVal, def; - var rawAttrs = attrs.raw || {}; - var nodeAttrs = attrs.normal || {}; - var setAttrs = attrs.set; - var positionAttrs = attrs.position; - var offsetAttrs = attrs.offset; + sgResize: function(cell, changed, opt) { - for (attrName in setAttrs) { - attrVal = setAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // SET - set function should return attributes to be set on the node, - // which will affect the node dimensions based on the reference bounding - // box. e.g. `width`, `height`, `d`, `rx`, `ry`, `points - var setResult = def.set.call(this, attrVal, refBBox.clone(), node, rawAttrs); - if (joint.util.isObject(setResult)) { - joint.util.assign(nodeAttrs, setResult); - } else if (setResult !== undefined) { - nodeAttrs[attrName] = setResult; - } - } + var model = this.model; + var angle = model.get('angle') || 0; + var size = model.get('size') || { width: 1, height: 1 }; + var scalable = this.scalableNode; - if (node instanceof HTMLElement) { - // TODO: setting the `transform` attribute on HTMLElements - // via `node.style.transform = 'matrix(...)';` would introduce - // a breaking change (e.g. basic.TextBlock). - this.setNodeAttributes(node, nodeAttrs); - return; + // Getting scalable group's bbox. + // Due to a bug in webkit's native SVG .getBBox implementation, the bbox of groups with path children includes the paths' control points. + // To work around the issue, we need to check whether there are any path elements inside the scalable group. + var recursive = false; + if (scalable.node.getElementsByTagName('path').length > 0) { + // If scalable has at least one descendant that is a path, we need to switch to recursive bbox calculation. + // If there are no path descendants, group bbox calculation works and so we can use the (faster) native function directly. + recursive = true; } + var scalableBBox = scalable.getBBox({ recursive: recursive }); - // The final translation of the subelement. - var nodeTransform = nodeAttrs.transform; - var nodeMatrix = V.transformStringToMatrix(nodeTransform); - var nodePosition = g.Point(nodeMatrix.e, nodeMatrix.f); - if (nodeTransform) { - nodeAttrs = joint.util.omit(nodeAttrs, 'transform'); - nodeMatrix.e = nodeMatrix.f = 0; - } + // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making + // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. + var sx = (size.width / (scalableBBox.width || 1)); + var sy = (size.height / (scalableBBox.height || 1)); + scalable.attr('transform', 'scale(' + sx + ',' + sy + ')'); - // Calculate node scale determined by the scalable group - // only if later needed. - var sx, sy, translation; - if (positionAttrs || offsetAttrs) { - var nodeScale = this.getNodeScale(node, opt.scalableNode); - sx = nodeScale.sx; - sy = nodeScale.sy; - } + // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` + // Order of transformations is significant but we want to reconstruct the object always in the order: + // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, + // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the + // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation + // around the center of the resized object (which is a different origin then the origin of the previous rotation) + // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. - var positioned = false; - for (attrName in positionAttrs) { - attrVal = positionAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // POSITION - position function should return a point from the - // reference bounding box. The default position of the node is x:0, y:0 of - // the reference bounding box or could be further specify by some - // SVG attributes e.g. `x`, `y` - translation = def.position.call(this, attrVal, refBBox.clone(), node, rawAttrs); - if (translation) { - nodePosition.offset(g.Point(translation).scale(sx, sy)); - positioned || (positioned = true); - } - } + // Cancel the rotation but now around a different origin, which is the center of the scaled object. + var rotatable = this.rotatableNode; + var rotation = rotatable && rotatable.attr('transform'); + if (rotation && rotation !== null) { - // The node bounding box could depend on the `size` set from the previous loop. - // Here we know, that all the size attributes have been already set. - this.setNodeAttributes(node, nodeAttrs); + rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); + var rotatableBBox = scalable.getBBox({ target: this.paper.viewport }); - var offseted = false; - if (offsetAttrs) { - // Check if the node is visible - var nodeClientRect = node.getBoundingClientRect(); - if (nodeClientRect.width > 0 && nodeClientRect.height > 0) { - var nodeBBox = V.transformRect(node.getBBox(), nodeMatrix).scale(1 / sx, 1 / sy); - for (attrName in offsetAttrs) { - attrVal = offsetAttrs[attrName]; - def = this.getAttributeDefinition(attrName); - // OFFSET - offset function should return a point from the element - // bounding box. The default offset point is x:0, y:0 (origin) or could be further - // specify with some SVG attributes e.g. `text-anchor`, `cx`, `cy` - translation = def.offset.call(this, attrVal, nodeBBox, node, rawAttrs); - if (translation) { - nodePosition.offset(g.Point(translation).scale(sx, sy)); - offseted || (offseted = true); - } - } - } + // Store new x, y and perform rotate() again against the new rotation origin. + model.set('position', { x: rotatableBBox.x, y: rotatableBBox.y }, opt); + this.rotate(); } - // Do not touch node's transform attribute if there is no transformation applied. - if (nodeTransform !== undefined || positioned || offseted) { - // Round the coordinates to 1 decimal point. - nodePosition.round(1); - nodeMatrix.e = nodePosition.x; - nodeMatrix.f = nodePosition.y; - node.setAttribute('transform', V.matrixToTransformString(nodeMatrix)); - } + // Update must always be called on non-rotated element. Otherwise, relative positioning + // would work with wrong (rotated) bounding boxes. + this.update(); }, - getNodeScale: function(node, scalableNode) { + // Embedding mode methods. + // ----------------------- - // Check if the node is a descendant of the scalable group. - var sx, sy; - if (scalableNode && scalableNode.contains(node)) { - var scale = scalableNode.scale(); - sx = 1 / scale.sx; - sy = 1 / scale.sy; - } else { - sx = 1; - sy = 1; - } + prepareEmbedding: function(data) { - return { sx: sx, sy: sy }; - }, + data || (data = {}); - findNodesAttributes: function(attrs, root, selectorCache) { + var model = data.model || this.model; + var paper = data.paper || this.paper; + var graph = paper.model; - // TODO: merge attributes in order defined by `index` property + model.startBatch('to-front'); - var nodesAttrs = {}; + // Bring the model to the front with all his embeds. + model.toFront({ deep: true, ui: true }); - for (var selector in attrs) { - if (!attrs.hasOwnProperty(selector)) continue; - var $selected = selectorCache[selector] = this.findBySelector(selector, root); + // Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see + // the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z. + var maxZ = graph.get('cells').max('z').get('z'); + var connectedLinks = graph.getConnectedLinks(model, { deep: true }); - for (var i = 0, n = $selected.length; i < n; i++) { - var node = $selected[i]; - var nodeId = V.ensureId(node); - var nodeAttrs = attrs[selector]; - var prevNodeAttrs = nodesAttrs[nodeId]; - if (prevNodeAttrs) { - if (!prevNodeAttrs.merged) { - prevNodeAttrs.merged = true; - prevNodeAttrs.attributes = joint.util.cloneDeep(prevNodeAttrs.attributes); - } - joint.util.merge(prevNodeAttrs.attributes, nodeAttrs); - } else { - nodesAttrs[nodeId] = { - attributes: nodeAttrs, - node: node, - merged: false - }; - } - } - } + // Move to front also all the inbound and outbound links that are connected + // to any of the element descendant. If we bring to front only embedded elements, + // links connected to them would stay in the background. + joint.util.invoke(connectedLinks, 'set', 'z', maxZ + 1, { ui: true }); - return nodesAttrs; + model.stopBatch('to-front'); + + // Before we start looking for suitable parent we remove the current one. + var parentId = model.parent(); + parentId && graph.getCell(parentId).unembed(model, { ui: true }); }, - // Default is to process the `model.attributes.attrs` object and set attributes on subelements based on the selectors, - // unless `attrs` parameter was passed. - updateDOMSubtreeAttributes: function(rootNode, attrs, opt) { + processEmbedding: function(data) { - opt || (opt = {}); - opt.rootBBox || (opt.rootBBox = g.Rect()); + data || (data = {}); - // Cache table for query results and bounding box calculation. - // Note that `selectorCache` needs to be invalidated for all - // `updateAttributes` calls, as the selectors might pointing - // to nodes designated by an attribute or elements dynamically - // created. - var selectorCache = {}; - var bboxCache = {}; - var relativeItems = []; - var item, node, nodeAttrs, nodeData, processedAttrs; + var model = data.model || this.model; + var paper = data.paper || this.paper; + var paperOptions = paper.options; - var roAttrs = opt.roAttributes; - var nodesAttrs = this.findNodesAttributes(roAttrs || attrs, rootNode, selectorCache); - // `nodesAttrs` are different from all attributes, when - // rendering only attributes sent to this method. - var nodesAllAttrs = (roAttrs) - ? nodesAllAttrs = this.findNodesAttributes(attrs, rootNode, selectorCache) - : nodesAttrs; + var candidates = []; + if (joint.util.isFunction(paperOptions.findParentBy)) { + var parents = joint.util.toArray(paperOptions.findParentBy.call(paper.model, this)); + candidates = parents.filter(function(el) { + return el instanceof joint.dia.Cell && this.model.id !== el.id && !el.isEmbeddedIn(this.model); + }.bind(this)); + } else { + candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); + } - for (var nodeId in nodesAttrs) { - nodeData = nodesAttrs[nodeId]; - nodeAttrs = nodeData.attributes; - node = nodeData.node; - processedAttrs = this.processNodeAttributes(node, nodeAttrs); + if (paperOptions.frontParentOnly) { + // pick the element with the highest `z` index + candidates = candidates.slice(-1); + } - if (!processedAttrs.set && !processedAttrs.position && !processedAttrs.offset) { - // Set all the normal attributes right on the SVG/HTML element. - this.setNodeAttributes(node, processedAttrs.normal); + var newCandidateView = null; + var prevCandidateView = data.candidateEmbedView; - } else { + // iterate over all candidates starting from the last one (has the highest z-index). + for (var i = candidates.length - 1; i >= 0; i--) { - var nodeAllAttrs = nodesAllAttrs[nodeId] && nodesAllAttrs[nodeId].attributes; - var refSelector = (nodeAllAttrs && (nodeAttrs.ref === undefined)) - ? nodeAllAttrs.ref - : nodeAttrs.ref; + var candidate = candidates[i]; - var refNode; - if (refSelector) { - refNode = (selectorCache[refSelector] || this.findBySelector(refSelector, rootNode))[0]; - if (!refNode) { - throw new Error('dia.ElementView: "' + refSelector + '" reference does not exists.'); - } - } else { - refNode = null; - } + if (prevCandidateView && prevCandidateView.model.id == candidate.id) { - item = { - node: node, - refNode: refNode, - processedAttributes: processedAttrs, - allAttributes: nodeAllAttrs - }; + // candidate remains the same + newCandidateView = prevCandidateView; + break; - // If an element in the list is positioned relative to this one, then - // we want to insert this one before it in the list. - var itemIndex = relativeItems.findIndex(function(item) { - return item.refNode === node; - }); + } else { - if (itemIndex > -1) { - relativeItems.splice(itemIndex, 0, item); - } else { - relativeItems.push(item); + var view = candidate.findView(paper); + if (paperOptions.validateEmbedding.call(paper, this, view)) { + + // flip to the new candidate + newCandidateView = view; + break; } } } - for (var i = 0, n = relativeItems.length; i < n; i++) { - item = relativeItems[i]; - node = item.node; - refNode = item.refNode; + if (newCandidateView && newCandidateView != prevCandidateView) { + // A new candidate view found. Highlight the new one. + this.clearEmbedding(data); + data.candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); + } - // Find the reference element bounding box. If no reference was provided, we - // use the optional bounding box. - var refNodeId = refNode ? V.ensureId(refNode) : ''; - var refBBox = bboxCache[refNodeId]; - if (!refBBox) { - // Get the bounding box of the reference element relative to the `rotatable` `<g>` (without rotation) - // or to the root `<g>` element if no rotatable group present if reference node present. - // Uses the bounding box provided. - refBBox = bboxCache[refNodeId] = (refNode) - ? V(refNode).getBBox({ target: (opt.rotatableNode || rootNode) }) - : opt.rootBBox; - } + if (!newCandidateView && prevCandidateView) { + // No candidate view found. Unhighlight the previous candidate. + this.clearEmbedding(data); + } + }, - if (roAttrs) { - // if there was a special attribute affecting the position amongst passed-in attributes - // we have to merge it with the rest of the element's attributes as they are necessary - // to update the position relatively (i.e `ref-x` && 'ref-dx') - processedAttrs = this.processNodeAttributes(node, item.allAttributes); - this.mergeProcessedAttributes(processedAttrs, item.processedAttributes); + clearEmbedding: function(data) { - } else { - processedAttrs = item.processedAttributes; - } + data || (data = {}); - this.updateRelativeAttributes(node, processedAttrs, refBBox, opt); + var candidateView = data.candidateEmbedView; + if (candidateView) { + // No candidate view found. Unhighlight the previous candidate. + candidateView.unhighlight(null, { embedding: true }); + data.candidateEmbedView = null; } }, - mergeProcessedAttributes: function(processedAttrs, roProcessedAttrs) { + finalizeEmbedding: function(data) { + + data || (data = {}); + + var candidateView = data.candidateEmbedView; + var model = data.model || this.model; + var paper = data.paper || this.paper; - processedAttrs.set || (processedAttrs.set = {}); - processedAttrs.position || (processedAttrs.position = {}); - processedAttrs.offset || (processedAttrs.offset = {}); + if (candidateView) { - joint.util.assign(processedAttrs.set, roProcessedAttrs.set); - joint.util.assign(processedAttrs.position, roProcessedAttrs.position); - joint.util.assign(processedAttrs.offset, roProcessedAttrs.offset); + // We finished embedding. Candidate view is chosen to become the parent of the model. + candidateView.model.embed(model, { ui: true }); + candidateView.unhighlight(null, { embedding: true }); - // Handle also the special transform property. - var transform = processedAttrs.normal && processedAttrs.normal.transform; - if (transform !== undefined && roProcessedAttrs.normal) { - roProcessedAttrs.normal.transform = transform; + data.candidateEmbedView = null; } - processedAttrs.normal = roProcessedAttrs.normal; + + joint.util.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); }, // Interaction. The controller part. // --------------------------------- - // Interaction is handled by the paper and delegated to the view in interest. - // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. - // If necessary, real coordinates can be obtained from the `evt` event object. - - // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, - // i.e. `joint.dia.Element` and `joint.dia.Link`. - pointerdblclick: function(evt, x, y) { - this.notify('cell:pointerdblclick', evt, x, y); + joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments); + this.notify('element:pointerdblclick', evt, x, y); }, pointerclick: function(evt, x, y) { - this.notify('cell:pointerclick', evt, x, y); + joint.dia.CellView.prototype.pointerclick.apply(this, arguments); + this.notify('element:pointerclick', evt, x, y); + }, + + contextmenu: function(evt, x, y) { + + joint.dia.CellView.prototype.contextmenu.apply(this, arguments); + this.notify('element:contextmenu', evt, x, y); }, pointerdown: function(evt, x, y) { - if (this.model.graph) { - this.model.startBatch('pointer'); - this._graph = this.model.graph; - } + joint.dia.CellView.prototype.pointerdown.apply(this, arguments); + this.notify('element:pointerdown', evt, x, y); - this.notify('cell:pointerdown', evt, x, y); + this.dragStart(evt, x, y); }, pointermove: function(evt, x, y) { - this.notify('cell:pointermove', evt, x, y); + var data = this.eventData(evt); + switch (data.action) { + case 'move': + this.drag(evt, x, y); + break; + case 'magnet': + this.dragMagnet(evt, x, y); + break; + } + + if (!data.stopPropagation) { + joint.dia.CellView.prototype.pointermove.apply(this, arguments); + this.notify('element:pointermove', evt, x, y); + } + + // Make sure the element view data is passed along. + // It could have been wiped out in the handlers above. + this.eventData(evt, data); }, pointerup: function(evt, x, y) { - this.notify('cell:pointerup', evt, x, y); + var data = this.eventData(evt); + switch (data.action) { + case 'move': + this.dragEnd(evt, x, y); + break; + case 'magnet': + this.dragMagnetEnd(evt, x, y); + return; + } - if (this._graph) { - // we don't want to trigger event on model as model doesn't - // need to be member of collection anymore (remove) - this._graph.stopBatch('pointer', { cell: this.model }); - delete this._graph; + if (!data.stopPropagation) { + this.notify('element:pointerup', evt, x, y); + joint.dia.CellView.prototype.pointerup.apply(this, arguments); } }, mouseover: function(evt) { - this.notify('cell:mouseover', evt); + joint.dia.CellView.prototype.mouseover.apply(this, arguments); + this.notify('element:mouseover', evt); }, mouseout: function(evt) { - this.notify('cell:mouseout', evt); + joint.dia.CellView.prototype.mouseout.apply(this, arguments); + this.notify('element:mouseout', evt); }, mouseenter: function(evt) { - this.notify('cell:mouseenter', evt); + joint.dia.CellView.prototype.mouseenter.apply(this, arguments); + this.notify('element:mouseenter', evt); }, mouseleave: function(evt) { - this.notify('cell:mouseleave', evt); + joint.dia.CellView.prototype.mouseleave.apply(this, arguments); + this.notify('element:mouseleave', evt); }, mousewheel: function(evt, x, y, delta) { - this.notify('cell:mousewheel', evt, x, y, delta); + joint.dia.CellView.prototype.mousewheel.apply(this, arguments); + this.notify('element:mousewheel', evt, x, y, delta); }, - contextmenu: function(evt, x, y) { + onmagnet: function(evt, x, y) { - this.notify('cell:contextmenu', evt, x, y); + this.dragMagnetStart(evt, x, y); + + var stopPropagation = this.eventData(evt).stopPropagation; + if (stopPropagation) evt.stopPropagation(); }, - event: function(evt, eventName, x, y) { + // Drag Start Handlers - this.notify(eventName, evt, x, y); + dragStart: function(evt, x, y) { + + if (!this.can('elementMove')) return; + + this.eventData(evt, { + action: 'move', + x: x, + y: y, + restrictedArea: this.paper.getRestrictedArea(this) + }); }, - setInteractivity: function(value) { + dragMagnetStart: function(evt, x, y) { - this.options.interactive = value; - } -}); + if (!this.can('addLinkFromMagnet')) return; -// joint.dia.Element base model. -// ----------------------------- + this.model.startBatch('add-link'); -joint.dia.Element = joint.dia.Cell.extend({ + var paper = this.paper; + var graph = paper.model; + var magnet = evt.target; + var link = paper.getDefaultLink(this, magnet); + var sourceEnd = this.getLinkEnd(magnet, x, y, link, 'source'); + var targetEnd = { x: x, y: y }; + + link.set({ source: sourceEnd, target: targetEnd }); + link.addTo(graph, { async: false, ui: true }); + + var linkView = link.findView(paper); + joint.dia.CellView.prototype.pointerdown.apply(linkView, arguments); + linkView.notify('link:pointerdown', evt, x, y); + var data = linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); + linkView.eventData(evt, data); + + this.eventData(evt, { + action: 'magnet', + linkView: linkView, + stopPropagation: true + }); - defaults: { - position: { x: 0, y: 0 }, - size: { width: 1, height: 1 }, - angle: 0 + this.paper.delegateDragEvents(this, evt.data); }, - initialize: function() { + // Drag Handlers - this._initializePorts(); - joint.dia.Cell.prototype.initialize.apply(this, arguments); + drag: function(evt, x, y) { + + var paper = this.paper; + var grid = paper.options.gridSize; + var element = this.model; + var position = element.position(); + var data = this.eventData(evt); + + // Make sure the new element's position always snaps to the current grid after + // translate as the previous one could be calculated with a different grid size. + var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - data.x, grid); + var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - data.y, grid); + + element.translate(tx, ty, { restrictedArea: data.restrictedArea, ui: true }); + + var embedding = !!data.embedding; + if (paper.options.embeddingMode) { + if (!embedding) { + // Prepare the element for embedding only if the pointer moves. + // We don't want to do unnecessary action with the element + // if an user only clicks/dblclicks on it. + this.prepareEmbedding(data); + embedding = true; + } + this.processEmbedding(data); + } + + this.eventData(evt, { + x: g.snapToGrid(x, grid), + y: g.snapToGrid(y, grid), + embedding: embedding + }); }, - /** - * @abstract - */ - _initializePorts: function() { - // implemented in ports.js + dragMagnet: function(evt, x, y) { + + var data = this.eventData(evt); + var linkView = data.linkView; + if (linkView) linkView.pointermove(evt, x, y); }, - isElement: function() { + // Drag End Handlers - return true; + dragEnd: function(evt, x, y) { + + var data = this.eventData(evt); + if (data.embedding) this.finalizeEmbedding(data); }, - position: function(x, y, opt) { + dragMagnetEnd: function(evt, x, y) { - var isSetter = joint.util.isNumber(y); + var data = this.eventData(evt); + var linkView = data.linkView; + if (linkView) linkView.pointerup(evt, x, y); - opt = (isSetter ? opt : x) || {}; + this.model.stopBatch('add-link'); + } - // option `parentRelative` for setting the position relative to the element's parent. - if (opt.parentRelative) { +}); - // Getting the parent's position requires the collection. - // Cell.get('parent') helds cell id only. - if (!this.graph) throw new Error('Element must be part of a graph.'); - var parent = this.graph.getCell(this.get('parent')); - var parentPosition = parent && !parent.isLink() - ? parent.get('position') - : { x: 0, y: 0 }; - } +// joint.dia.Link base model. +// -------------------------- - if (isSetter) { +joint.dia.Link = joint.dia.Cell.extend({ - if (opt.parentRelative) { - x += parentPosition.x; - y += parentPosition.y; - } + // The default markup for links. + markup: [ + '<path class="connection" stroke="black" d="M 0 0 0 0"/>', + '<path class="marker-source" fill="black" stroke="black" d="M 0 0 0 0"/>', + '<path class="marker-target" fill="black" stroke="black" d="M 0 0 0 0"/>', + '<path class="connection-wrap" d="M 0 0 0 0"/>', + '<g class="labels"/>', + '<g class="marker-vertices"/>', + '<g class="marker-arrowheads"/>', + '<g class="link-tools"/>' + ].join(''), - if (opt.deep) { - var currentPosition = this.get('position'); - this.translate(x - currentPosition.x, y - currentPosition.y, opt); - } else { - this.set('position', { x: x, y: y }, opt); + toolMarkup: [ + '<g class="link-tool">', + '<g class="tool-remove" event="remove">', + '<circle r="11" />', + '<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z" />', + '<title>Remove link.', + '', + '', + '', + '', + 'Link options.', + '', + '' + ].join(''), + + doubleToolMarkup: undefined, + + // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). + // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for + // dragging vertices (changin their position). The latter is used for removing vertices. + vertexMarkup: [ + '', + '', + '', + '', + 'Remove vertex.', + '', + '' + ].join(''), + + arrowheadMarkup: [ + '', + '', + '' + ].join(''), + + // may be overwritten by user to change default label (its markup, attrs, position) + defaultLabel: undefined, + + // deprecated + // may be overwritten by user to change default label markup + // lower priority than defaultLabel.markup + labelMarkup: undefined, + + // private + _builtins: { + defaultLabel: { + // builtin default markup: + // used if neither defaultLabel.markup + // nor label.markup is set + markup: [ + { + tagName: 'rect', + selector: 'rect' // faster than tagName CSS selector + }, { + tagName: 'text', + selector: 'text' // faster than tagName CSS selector + } + ], + // builtin default attributes: + // applied only if builtin default markup is used + attrs: { + text: { + fill: '#000000', + fontSize: 14, + textAnchor: 'middle', + yAlignment: 'middle', + pointerEvents: 'none' + }, + rect: { + ref: 'text', + fill: '#ffffff', + rx: 3, + ry: 3, + refWidth: 1, + refHeight: 1, + refX: 0, + refY: 0 + } + }, + // builtin default position: + // used if neither defaultLabel.position + // nor label.position is set + position: { + distance: 0.5 } + } + }, - return this; + defaults: { + type: 'link', + source: {}, + target: {} + }, - } else { // Getter returns a geometry point. + isLink: function() { - var elementPosition = g.point(this.get('position')); + return true; + }, - return opt.parentRelative - ? elementPosition.difference(parentPosition) - : elementPosition; + disconnect: function(opt) { + + return this.set({ + source: { x: 0, y: 0 }, + target: { x: 0, y: 0 } + }, opt); + }, + + source: function(source, args, opt) { + + // getter + if (source === undefined) { + return joint.util.clone(this.get('source')); + } + + // setter + var localSource; + var localOpt; + + // `source` is a cell + // take only its `id` and combine with `args` + var isCellProvided = source instanceof joint.dia.Cell; + if (isCellProvided) { // three arguments + localSource = joint.util.clone(args) || {}; + localSource.id = source.id; + localOpt = opt; + return this.set('source', localSource, localOpt); + } + + // `source` is a g.Point + // take only its `x` and `y` and combine with `args` + var isPointProvided = source instanceof g.Point; + if (isPointProvided) { // three arguments + localSource = joint.util.clone(args) || {}; + localSource.x = source.x; + localSource.y = source.y; + localOpt = opt; + return this.set('source', localSource, localOpt); } + + // `source` is an object + // no checking + // two arguments + localSource = source; + localOpt = args; + return this.set('source', localSource, localOpt); }, - translate: function(tx, ty, opt) { + target: function(target, args, opt) { - tx = tx || 0; - ty = ty || 0; + // getter + if (target === undefined) { + return joint.util.clone(this.get('target')); + } - if (tx === 0 && ty === 0) { - // Like nothing has happened. - return this; + // setter + var localTarget; + var localOpt; + + // `target` is a cell + // take only its `id` argument and combine with `args` + var isCellProvided = target instanceof joint.dia.Cell; + if (isCellProvided) { // three arguments + localTarget = joint.util.clone(args) || {}; + localTarget.id = target.id; + localOpt = opt; + return this.set('target', localTarget, localOpt); } - opt = opt || {}; - // Pass the initiator of the translation. - opt.translateBy = opt.translateBy || this.id; + // `target` is a g.Point + // take only its `x` and `y` and combine with `args` + var isPointProvided = target instanceof g.Point; + if (isPointProvided) { // three arguments + localTarget = joint.util.clone(args) || {}; + localTarget.x = target.x; + localTarget.y = target.y; + localOpt = opt; + return this.set('target', localTarget, localOpt); + } - var position = this.get('position') || { x: 0, y: 0 }; + // `target` is an object + // no checking + // two arguments + localTarget = target; + localOpt = args; + return this.set('target', localTarget, localOpt); + }, - if (opt.restrictedArea && opt.translateBy === this.id) { + router: function(name, args, opt) { - // We are restricting the translation for the element itself only. We get - // the bounding box of the element including all its embeds. - // All embeds have to be translated the exact same way as the element. - var bbox = this.getBBox({ deep: true }); - var ra = opt.restrictedArea; - //- - - - - - - - - - - - -> ra.x + ra.width - // - - - -> position.x | - // -> bbox.x - // ▓▓▓▓▓▓▓ | - // ░░░░░░░▓▓▓▓▓▓▓ - // ░░░░░░░░░ | - // ▓▓▓▓▓▓▓▓░░░░░░░ - // ▓▓▓▓▓▓▓▓ | - // <-dx-> | restricted area right border - // <-width-> | ░ translated element - // <- - bbox.width - -> ▓ embedded element - var dx = position.x - bbox.x; - var dy = position.y - bbox.y; - // Find the maximal/minimal coordinates that the element can be translated - // while complies the restrictions. - var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); - var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); - // recalculate the translation taking the resctrictions into account. - tx = x - position.x; - ty = y - position.y; + // getter + if (name === undefined) { + router = this.get('router'); + if (!router) { + if (this.get('manhattan')) return { name: 'orthogonal' }; // backwards compatibility + return null; + } + if (typeof router === 'object') return joint.util.clone(router); + return router; // e.g. a function } - var translatedPosition = { - x: position.x + tx, - y: position.y + ty - }; + // setter + var isRouterProvided = ((typeof name === 'object') || (typeof name === 'function')); + var localRouter = isRouterProvided ? name : { name: name, args: args }; + var localOpt = isRouterProvided ? args : opt; + + return this.set('router', localRouter, localOpt); + }, + + connector: function(name, args, opt) { - // To find out by how much an element was translated in event 'change:position' handlers. - opt.tx = tx; - opt.ty = ty; + // getter + if (name === undefined) { + connector = this.get('connector'); + if (!connector) { + if (this.get('smooth')) return { name: 'smooth' }; // backwards compatibility + return null; + } + if (typeof connector === 'object') return joint.util.clone(connector); + return connector; // e.g. a function + } - if (opt.transition) { + // setter + var isConnectorProvided = ((typeof name === 'object' || typeof name === 'function')); + var localConnector = isConnectorProvided ? name : { name: name, args: args }; + var localOpt = isConnectorProvided ? args : opt; - if (!joint.util.isObject(opt.transition)) opt.transition = {}; + return this.set('connector', localConnector, localOpt); + }, - this.transition('position', translatedPosition, joint.util.assign({}, opt.transition, { - valueFunction: joint.util.interpolate.object - })); + // Labels API - } else { + // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. + label: function(idx, label, opt) { - this.set('position', translatedPosition, opt); - } + var labels = this.labels(); - // Recursively call `translate()` on all the embeds cells. - joint.util.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : 0; + if (idx < 0) idx = labels.length + idx; - return this; + // getter + if (arguments.length <= 1) return this.prop(['labels', idx]); + // setter + return this.prop(['labels', idx], label, opt); }, - size: function(width, height, opt) { + labels: function(labels, opt) { - var currentSize = this.get('size'); - // Getter - // () signature - if (width === undefined) { - return { - width: currentSize.width, - height: currentSize.height - }; - } - // Setter - // (size, opt) signature - if (joint.util.isObject(width)) { - opt = height; - height = joint.util.isNumber(width.height) ? width.height : currentSize.height; - width = joint.util.isNumber(width.width) ? width.width : currentSize.width; + // getter + if (arguments.length === 0) { + labels = this.get('labels'); + if (!Array.isArray(labels)) return []; + return labels.slice(); } + // setter + if (!Array.isArray(labels)) labels = []; + return this.set('labels', labels, opt); + }, - return this.resize(width, height, opt); + insertLabel: function(idx, label, opt) { + + if (!label) throw new Error('dia.Link: no label provided'); + + var labels = this.labels(); + var n = labels.length; + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : n; + if (idx < 0) idx = n + idx + 1; + + labels.splice(idx, 0, label); + return this.labels(labels, opt); }, - resize: function(width, height, opt) { + // convenience function + // add label to end of labels array + appendLabel: function(label, opt) { - opt = opt || {}; + return this.insertLabel(-1, label, opt); + }, - this.startBatch('resize', opt); + removeLabel: function(idx, opt) { - if (opt.direction) { + var labels = this.labels(); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : -1; - var currentSize = this.get('size'); + labels.splice(idx, 1); + return this.labels(labels, opt); + }, - switch (opt.direction) { + // Vertices API - case 'left': - case 'right': - // Don't change height when resizing horizontally. - height = currentSize.height; - break; + vertex: function(idx, vertex, opt) { - case 'top': - case 'bottom': - // Don't change width when resizing vertically. - width = currentSize.width; - break; - } + var vertices = this.vertices(); - // Get the angle and clamp its value between 0 and 360 degrees. - var angle = g.normalizeAngle(this.get('angle') || 0); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : 0; + if (idx < 0) idx = vertices.length + idx; - var quadrant = { - 'top-right': 0, - 'right': 0, - 'top-left': 1, - 'top': 1, - 'bottom-left': 2, - 'left': 2, - 'bottom-right': 3, - 'bottom': 3 - }[opt.direction]; + // getter + if (arguments.length <= 1) return this.prop(['vertices', idx]); + // setter + return this.prop(['vertices', idx], vertex, opt); + }, - if (opt.absolute) { + vertices: function(vertices, opt) { - // We are taking the element's rotation into account - quadrant += Math.floor((angle + 45) / 90); - quadrant %= 4; - } + // getter + if (arguments.length === 0) { + vertices = this.get('vertices'); + if (!Array.isArray(vertices)) return []; + return vertices.slice(); + } + // setter + if (!Array.isArray(vertices)) vertices = []; + return this.set('vertices', vertices, opt); + }, - // This is a rectangle in size of the unrotated element. - var bbox = this.getBBox(); + insertVertex: function(idx, vertex, opt) { - // Pick the corner point on the element, which meant to stay on its place before and - // after the rotation. - var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); + if (!vertex) throw new Error('dia.Link: no vertex provided'); - // Find an image of the previous indent point. This is the position, where is the - // point actually located on the screen. - var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); + var vertices = this.vertices(); + var n = vertices.length; + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : n; + if (idx < 0) idx = n + idx + 1; - // Every point on the element rotates around a circle with the centre of rotation - // in the middle of the element while the whole element is being rotated. That means - // that the distance from a point in the corner of the element (supposed its always rect) to - // the center of the element doesn't change during the rotation and therefore it equals - // to a distance on unrotated element. - // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. - var radius = Math.sqrt((width * width) + (height * height)) / 2; + vertices.splice(idx, 0, vertex); + return this.vertices(vertices, opt); + }, - // Now we are looking for an angle between x-axis and the line starting at image of fixed point - // and ending at the center of the element. We call this angle `alpha`. + removeVertex: function(idx, opt) { - // The image of a fixed point is located in n-th quadrant. For each quadrant passed - // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. - // - // 3 | 2 - // --c-- Quadrant positions around the element's center `c` - // 0 | 1 - // - var alpha = quadrant * Math.PI / 2; + var vertices = this.vertices(); + idx = (isFinite(idx) && idx !== null) ? (idx | 0) : -1; - // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis - // going through the center of the element) and line crossing the indent of the fixed point and the center - // of the element. This is the angle we need but on the unrotated element. - alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); + vertices.splice(idx, 1); + return this.vertices(vertices, opt); + }, - // Lastly we have to deduct the original angle the element was rotated by and that's it. - alpha -= g.toRad(angle); + // Transformations - // With this angle and distance we can easily calculate the centre of the unrotated element. - // Note that fromPolar constructor accepts an angle in radians. - var center = g.point.fromPolar(radius, alpha, imageFixedPoint); + translate: function(tx, ty, opt) { - // The top left corner on the unrotated element has to be half a width on the left - // and half a height to the top from the center. This will be the origin of rectangle - // we were looking for. - var origin = g.point(center).offset(width / -2, height / -2); + // enrich the option object + opt = opt || {}; + opt.translateBy = opt.translateBy || this.id; + opt.tx = tx; + opt.ty = ty; - // Resize the element (before re-positioning it). - this.set('size', { width: width, height: height }, opt); + return this.applyToPoints(function(p) { + return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; + }, opt); + }, - // Finally, re-position the element. - this.position(origin.x, origin.y, opt); + scale: function(sx, sy, origin, opt) { - } else { + return this.applyToPoints(function(p) { + return g.point(p).scale(sx, sy, origin).toJSON(); + }, opt); + }, - // Resize the element. - this.set('size', { width: width, height: height }, opt); + applyToPoints: function(fn, opt) { + + if (!joint.util.isFunction(fn)) { + throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); } - this.stopBatch('resize', opt); + var attrs = {}; - return this; - }, + var source = this.source(); + if (!source.id) { + attrs.source = fn(source); + } - scale: function(sx, sy, origin, opt) { + var target = this.target(); + if (!target.id) { + attrs.target = fn(target); + } - var scaledBBox = this.getBBox().scale(sx, sy, origin); - this.startBatch('scale', opt); - this.position(scaledBBox.x, scaledBBox.y, opt); - this.resize(scaledBBox.width, scaledBBox.height, opt); - this.stopBatch('scale'); - return this; + var vertices = this.vertices(); + if (vertices.length > 0) { + attrs.vertices = vertices.map(fn); + } + + return this.set(attrs, opt); }, - fitEmbeds: function(opt) { + reparent: function(opt) { - opt = opt || {}; + var newParent; - // Getting the children's size and position requires the collection. - // Cell.get('embdes') helds an array of cell ids only. - if (!this.graph) throw new Error('Element must be part of a graph.'); + if (this.graph) { - var embeddedCells = this.getEmbeddedCells(); + var source = this.getSourceElement(); + var target = this.getTargetElement(); + var prevParent = this.getParentCell(); - if (embeddedCells.length > 0) { + if (source && target) { + newParent = this.graph.getCommonAncestor(source, target); + } - this.startBatch('fit-embeds', opt); + if (prevParent && (!newParent || newParent.id !== prevParent.id)) { + // Unembed the link if source and target has no common ancestor + // or common ancestor changed + prevParent.unembed(this, opt); + } - if (opt.deep) { - // Recursively apply fitEmbeds on all embeds first. - joint.util.invoke(embeddedCells, 'fitEmbeds', opt); + if (newParent) { + newParent.embed(this, opt); } + } - // Compute cell's size and position based on the children bbox - // and given padding. - var bbox = this.graph.getCellsBBox(embeddedCells); - var padding = joint.util.normalizeSides(opt.padding); + return newParent; + }, - // Apply padding computed above to the bbox. - bbox.moveAndExpand({ - x: -padding.left, - y: -padding.top, - width: padding.right + padding.left, - height: padding.bottom + padding.top - }); + hasLoop: function(opt) { - // Set new element dimensions finally. - this.set({ - position: { x: bbox.x, y: bbox.y }, - size: { width: bbox.width, height: bbox.height } - }, opt); + opt = opt || {}; - this.stopBatch('fit-embeds'); + var sourceId = this.source().id; + var targetId = this.target().id; + + if (!sourceId || !targetId) { + // Link "pinned" to the paper does not have a loop. + return false; } - return this; + var loop = sourceId === targetId; + + // Note that there in the deep mode a link can have a loop, + // even if it connects only a parent and its embed. + // A loop "target equals source" is valid in both shallow and deep mode. + if (!loop && opt.deep && this.graph) { + + var sourceElement = this.getSourceElement(); + var targetElement = this.getTargetElement(); + + loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); + } + + return loop; + }, + + // unlike source(), this method returns null if source is a point + getSourceElement: function() { + + var source = this.source(); + var graph = this.graph; + + return (source && source.id && graph && graph.getCell(source.id)) || null; + }, + + // unlike target(), this method returns null if target is a point + getTargetElement: function() { + + var target = this.target(); + var graph = this.graph; + + return (target && target.id && graph && graph.getCell(target.id)) || null; }, - // Rotate element by `angle` degrees, optionally around `origin` point. - // If `origin` is not provided, it is considered to be the center of the element. - // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not - // the difference from the previous angle. - rotate: function(angle, absolute, origin, opt) { + // Returns the common ancestor for the source element, + // target element and the link itself. + getRelationshipAncestor: function() { - if (origin) { + var connectionAncestor; - var center = this.getBBox().center(); - var size = this.get('size'); - var position = this.get('position'); - center.rotate(origin, this.get('angle') - angle); - var dx = center.x - size.width / 2 - position.x; - var dy = center.y - size.height / 2 - position.y; - this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); - this.position(position.x + dx, position.y + dy, opt); - this.rotate(angle, absolute, null, opt); - this.stopBatch('rotate'); + if (this.graph) { - } else { + var cells = [ + this, + this.getSourceElement(), // null if source is a point + this.getTargetElement() // null if target is a point + ].filter(function(item) { + return !!item; + }); - this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360, opt); + connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); } - return this; + return connectionAncestor || null; }, - getBBox: function(opt) { + // Is source, target and the link itself embedded in a given cell? + isRelationshipEmbeddedIn: function(cell) { - opt = opt || {}; + var cellId = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : cell.id; + var ancestor = this.getRelationshipAncestor(); - if (opt.deep && this.graph) { + return !!ancestor && (ancestor.id === cellId || ancestor.isEmbeddedIn(cellId)); + }, - // Get all the embedded elements using breadth first algorithm, - // that doesn't use recursion. - var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); - // Add the model itself. - elements.push(this); + // Get resolved default label. + _getDefaultLabel: function() { - return this.graph.getCellsBBox(elements); - } + var defaultLabel = this.get('defaultLabel') || this.defaultLabel || {}; - var position = this.get('position'); - var size = this.get('size'); + var label = {}; + label.markup = defaultLabel.markup || this.get('labelMarkup') || this.labelMarkup; + label.position = defaultLabel.position; + label.attrs = defaultLabel.attrs; + label.size = defaultLabel.size; - return g.rect(position.x, position.y, size.width, size.height); + return label; } -}); - -// joint.dia.Element base view and controller. -// ------------------------------------------- +}, + { + endsEqual: function(a, b) { + var portsEqual = a.port === b.port || !a.port && !b.port; + return a.id === b.id && portsEqual; + } + }); -joint.dia.ElementView = joint.dia.CellView.extend({ - /** - * @abstract - */ - _removePorts: function() { - // implemented in ports.js - }, +// joint.dia.Link base view and controller. +// ---------------------------------------- - /** - * - * @abstract - */ - _renderPorts: function() { - // implemented in ports.js - }, +joint.dia.LinkView = joint.dia.CellView.extend({ className: function() { var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); - classNames.push('element'); + classNames.push('link'); return classNames.join(' '); }, - initialize: function() { + options: { + + shortLinkLength: 105, + doubleLinkTools: false, + longLinkLength: 155, + linkToolsOffset: 40, + doubleLinkToolsOffset: 65, + sampleInterval: 50, + }, + + _labelCache: null, + _labelSelectors: null, + _markerCache: null, + _V: null, + _dragData: null, // deprecated + + metrics: null, + decimalsRounding: 2, + + initialize: function(options) { joint.dia.CellView.prototype.initialize.apply(this, arguments); - var model = this.model; + // create methods in prototype, so they can be accessed from any instance and + // don't need to be create over and over + if (typeof this.constructor.prototype.watchSource !== 'function') { + this.constructor.prototype.watchSource = this.createWatcher('source'); + this.constructor.prototype.watchTarget = this.createWatcher('target'); + } - this.listenTo(model, 'change:position', this.translate); - this.listenTo(model, 'change:size', this.resize); - this.listenTo(model, 'change:angle', this.rotate); - this.listenTo(model, 'change:markup', this.render); + // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to + // `` nodes wrapped by Vectorizer. This allows for quick access to the + // nodes in `updateLabelPosition()` in order to update the label positions. + this._labelCache = {}; - this._initializePorts(); + // a cache of label selectors + this._labelSelectors = {}; + + // keeps markers bboxes and positions again for quicker access + this._markerCache = {}; + + // cache of default markup nodes + this._V = {}, + + // connection path metrics + this.metrics = {}, + + // bind events + this.startListening(); }, - /** - * @abstract - */ - _initializePorts: function() { + startListening: function() { + var model = this.model; + + this.listenTo(model, 'change:markup', this.render); + this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); + this.listenTo(model, 'change:toolMarkup', this.onToolsChange); + this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); + this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); + this.listenTo(model, 'change:source', this.onSourceChange); + this.listenTo(model, 'change:target', this.onTargetChange); }, - update: function(cell, renderingOnlyAttrs) { + onSourceChange: function(cell, source, opt) { - this._removePorts(); + // Start watching the new source model. + this.watchSource(cell, source); + // This handler is called when the source attribute is changed. + // This can happen either when someone reconnects the link (or moves arrowhead), + // or when an embedded link is translated by its ancestor. + // 1. Always do update. + // 2. Do update only if the opposite end ('target') is also a point. + var model = this.model; + if (!opt.translateBy || !model.get('target').id || !source.id) { + this.update(model, null, opt); + } + }, + + onTargetChange: function(cell, target, opt) { + // Start watching the new target model. + this.watchTarget(cell, target); + // See `onSourceChange` method. var model = this.model; - var modelAttrs = model.attr(); - this.updateDOMSubtreeAttributes(this.el, modelAttrs, { - rootBBox: g.Rect(model.size()), - scalableNode: this.scalableNode, - rotatableNode: this.rotatableNode, - // Use rendering only attributes if they differs from the model attributes - roAttributes: (renderingOnlyAttrs === modelAttrs) ? null : renderingOnlyAttrs - }); + if (!opt.translateBy || (model.get('source').id && !target.id && joint.util.isEmpty(model.get('vertices')))) { + this.update(model, null, opt); + } + }, - this._renderPorts(); + onVerticesChange: function(cell, changed, opt) { + + this.renderVertexMarkers(); + + // If the vertices have been changed by a translation we do update only if the link was + // the only link that was translated. If the link was translated via another element which the link + // is embedded in, this element will be translated as well and that triggers an update. + // Note that all embeds in a model are sorted - first comes links, then elements. + if (!opt.translateBy || opt.translateBy === this.model.id) { + // Vertices were changed (not as a reaction on translate) + // or link.translate() was called or + this.update(cell, null, opt); + } }, - // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the - // default markup is not desirable. - renderMarkup: function() { + onToolsChange: function() { - var markup = this.model.get('markup') || this.model.markup; + this.renderTools().updateToolsPosition(); + }, - if (markup) { + onLabelsChange: function(link, labels, opt) { - var svg = joint.util.template(markup)(); - var nodes = V(svg); + var requireRender = true; - this.vel.append(nodes); + var previousLabels = this.model.previous('labels'); - } else { + if (previousLabels) { + // Here is an optimalization for cases when we know, that change does + // not require rerendering of all labels. + if (('propertyPathArray' in opt) && ('propertyValue' in opt)) { + // The label is setting by `prop()` method + var pathArray = opt.propertyPathArray || []; + var pathLength = pathArray.length; + if (pathLength > 1) { + // We are changing a single label here e.g. 'labels/0/position' + var labelExists = !!previousLabels[pathArray[1]]; + if (labelExists) { + if (pathLength === 2) { + // We are changing the entire label. Need to check if the + // markup is also being changed. + requireRender = ('markup' in Object(opt.propertyValue)); + } else if (pathArray[2] !== 'markup') { + // We are changing a label property but not the markup + requireRender = false; + } + } + } + } + } - throw new Error('properties.markup is missing while the default render() implementation is used.'); + if (requireRender) { + this.renderLabels(); + } else { + this.updateLabels(); } + + this.updateLabelPositions(); }, - render: function() { + // Rendering. + // ---------- - this.$el.empty(); + render: function() { + this.vel.empty(); + this._V = {}; this.renderMarkup(); - this.rotatableNode = this.vel.findOne('.rotatable'); - var scalable = this.scalableNode = this.vel.findOne('.scalable'); - if (scalable) { - // Double update is necessary for elements with the scalable group only - // Note the resize() triggers the other `update`. - this.update(); - } - this.resize(); - this.rotate(); - this.translate(); + // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox + // returns zero values) + this.renderLabels(); + // start watching the ends of the link for changes + var model = this.model; + this.watchSource(model, model.source()) + .watchTarget(model, model.target()) + .update(); return this; }, - resize: function(cell, changed, opt) { + renderMarkup: function() { + + var link = this.model; + var markup = link.get('markup') || link.markup; + if (!markup) throw new Error('dia.LinkView: markup required'); + if (Array.isArray(markup)) return this.renderJSONMarkup(markup); + if (typeof markup === 'string') return this.renderStringMarkup(markup); + throw new Error('dia.LinkView: invalid markup'); + }, + + renderJSONMarkup: function(markup) { + + var doc = joint.util.parseDOMJSON(markup); + // Selectors + var selectors = this.selectors = doc.selectors; + var rootSelector = this.selector; + if (selectors[rootSelector]) throw new Error('dia.LinkView: ambiguous root selector.'); + selectors[rootSelector] = this.el; + // Fragment + this.vel.append(doc.fragment); + }, + + renderStringMarkup: function(markup) { + + // A special markup can be given in the `properties.markup` property. This might be handy + // if e.g. arrowhead markers should be `` elements or any other element than ``s. + // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors + // of elements with special meaning though. Therefore, those classes should be preserved in any + // special markup passed in `properties.markup`. + var children = V(markup); + // custom markup may contain only one children + if (!Array.isArray(children)) children = [children]; + // Cache all children elements for quicker access. + var cache = this._V; // vectorized markup; + for (var i = 0, n = children.length; i < n; i++) { + var child = children[i]; + var className = child.attr('class'); + if (className) { + // Strip the joint class name prefix, if there is one. + className = joint.util.removeClassNamePrefix(className); + cache[$.camelCase(className)] = child; + } + } + // partial rendering + this.renderTools(); + this.renderVertexMarkers(); + this.renderArrowheadMarkers(); + this.vel.append(children); + }, + + _getLabelMarkup: function(labelMarkup) { - var model = this.model; - var size = model.get('size') || { width: 1, height: 1 }; - var angle = model.get('angle') || 0; - - var scalable = this.scalableNode; - if (!scalable) { - - if (angle !== 0) { - // update the origin of the rotation - this.rotate(); - } - // update the ref attributes - this.update(); + if (!labelMarkup) return undefined; - // If there is no scalable elements, than there is nothing to scale. - return; - } + if (Array.isArray(labelMarkup)) return this._getLabelJSONMarkup(labelMarkup); + if (typeof labelMarkup === 'string') return this._getLabelStringMarkup(labelMarkup); + throw new Error('dia.linkView: invalid label markup'); + }, - // Getting scalable group's bbox. - // Due to a bug in webkit's native SVG .getBBox implementation, the bbox of groups with path children includes the paths' control points. - // To work around the issue, we need to check whether there are any path elements inside the scalable group. - var recursive = false; - if (scalable.node.getElementsByTagName('path').length > 0) { - // If scalable has at least one descendant that is a path, we need to switch to recursive bbox calculation. - // If there are no path descendants, group bbox calculation works and so we can use the (faster) native function directly. - recursive = true; - } - var scalableBBox = scalable.getBBox({ recursive: recursive }); + _getLabelJSONMarkup: function(labelMarkup) { - // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making - // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. - var sx = (size.width / (scalableBBox.width || 1)); - var sy = (size.height / (scalableBBox.height || 1)); - scalable.attr('transform', 'scale(' + sx + ',' + sy + ')'); + return joint.util.parseDOMJSON(labelMarkup); // fragment and selectors + }, - // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` - // Order of transformations is significant but we want to reconstruct the object always in the order: - // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, - // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the - // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation - // around the center of the resized object (which is a different origin then the origin of the previous rotation) - // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. + _getLabelStringMarkup: function(labelMarkup) { - // Cancel the rotation but now around a different origin, which is the center of the scaled object. - var rotatable = this.rotatableNode; - var rotation = rotatable && rotatable.attr('transform'); - if (rotation && rotation !== 'null') { + var children = V(labelMarkup); + var fragment = document.createDocumentFragment(); - rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); - var rotatableBBox = scalable.getBBox({ target: this.paper.viewport }); + if (!Array.isArray(children)) { + fragment.append(children.node); - // Store new x, y and perform rotate() again against the new rotation origin. - model.set('position', { x: rotatableBBox.x, y: rotatableBBox.y }, opt); - this.rotate(); + } else { + for (var i = 0, n = children.length; i < n; i++) { + var currentChild = children[i].node; + fragment.appendChild(currentChild); + } } - // Update must always be called on non-rotated element. Otherwise, relative positioning - // would work with wrong (rotated) bounding boxes. - this.update(); + return { fragment: fragment, selectors: {} }; // no selectors }, - translate: function(model, changes, opt) { - - var position = this.model.get('position') || { x: 0, y: 0 }; + // Label markup fragment may come wrapped in , or not. + // If it doesn't, add the container here. + _normalizeLabelMarkup: function(markup) { - this.vel.attr('transform', 'translate(' + position.x + ',' + position.y + ')'); - }, + if (!markup) return undefined; - rotate: function() { + var fragment = markup.fragment; + if (!(markup.fragment instanceof DocumentFragment) || !markup.fragment.hasChildNodes()) throw new Error('dia.LinkView: invalid label markup.'); - var rotatable = this.rotatableNode; - if (!rotatable) { - // If there is no rotatable elements, then there is nothing to rotate. - return; - } + var vNode; + var childNodes = fragment.childNodes; - var angle = this.model.get('angle') || 0; - var size = this.model.get('size') || { width: 1, height: 1 }; + if ((childNodes.length > 1) || childNodes[0].nodeName.toUpperCase() !== 'G') { + // default markup fragment is not wrapped in + // add a container - var ox = size.width / 2; - var oy = size.height / 2; + vNode = V('g'); + vNode.append(fragment); + vNode.addClass('label'); - if (angle !== 0) { - rotatable.attr('transform', 'rotate(' + angle + ',' + ox + ',' + oy + ')'); } else { - rotatable.removeAttr('transform'); + vNode = V(childNodes[0]); + vNode.addClass('label'); } + + return { node: vNode.node, selectors: markup.selectors }; }, - getBBox: function(opt) { + renderLabels: function() { - if (opt && opt.useModelGeometry) { - var bbox = this.model.getBBox().bbox(this.model.get('angle')); - return this.paper.localToPaperRect(bbox); + var cache = this._V; + var vLabels = cache.labels; + var labelCache = this._labelCache = {}; + var labelSelectors = this._labelSelectors = {}; + + if (vLabels) vLabels.empty(); + + var model = this.model; + var labels = model.get('labels') || []; + var labelsCount = labels.length; + if (labelsCount === 0) return this; + + if (!vLabels) { + // there is no label container in the markup but some labels are defined + // add a container + vLabels = cache.labels = V('g').addClass('labels').appendTo(this.el); } - return joint.dia.CellView.prototype.getBBox.apply(this, arguments); - }, + for (var i = 0; i < labelsCount; i++) { - // Embedding mode methods - // ---------------------- + var label = labels[i]; + var labelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(label.markup)); - prepareEmbedding: function(opt) { + var node; + var selectors; + if (labelMarkup) { + node = labelMarkup.node; + selectors = labelMarkup.selectors; - opt = opt || {}; + } else { + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(builtinDefaultLabel.markup)); - var model = opt.model || this.model; - var paper = opt.paper || this.paper; - var graph = paper.model; + var defaultLabel = model._getDefaultLabel(); + var defaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(defaultLabel.markup)); - model.startBatch('to-front', opt); + var defaultMarkup = defaultLabelMarkup || builtinDefaultLabelMarkup; - // Bring the model to the front with all his embeds. - model.toFront({ deep: true, ui: true }); + node = defaultMarkup.node; + selectors = defaultMarkup.selectors; + } - // Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see - // the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z. - var maxZ = graph.get('cells').max('z').get('z'); - var connectedLinks = graph.getConnectedLinks(model, { deep: true }); + var vLabel = V(node); + vLabel.attr('label-idx', i); // assign label-idx + vLabel.appendTo(vLabels); + labelCache[i] = vLabel; // cache node for `updateLabels()` so it can just update label node positions - // Move to front also all the inbound and outbound links that are connected - // to any of the element descendant. If we bring to front only embedded elements, - // links connected to them would stay in the background. - joint.util.invoke(connectedLinks, 'set', 'z', maxZ + 1, { ui: true }); + selectors[this.selector] = vLabel.node; + labelSelectors[i] = selectors; // cache label selectors for `updateLabels()` + } - model.stopBatch('to-front'); + this.updateLabels(); - // Before we start looking for suitable parent we remove the current one. - var parentId = model.get('parent'); - parentId && graph.getCell(parentId).unembed(model, { ui: true }); + return this; }, - processEmbedding: function(opt) { + // merge default label attrs into label attrs + // keep `undefined` or `null` because `{}` means something else + _mergeLabelAttrs: function(hasCustomMarkup, labelAttrs, defaultLabelAttrs, builtinDefaultLabelAttrs) { - opt = opt || {}; + if (labelAttrs === null) return null; + if (labelAttrs === undefined) { - var model = opt.model || this.model; - var paper = opt.paper || this.paper; + if (defaultLabelAttrs === null) return null; + if (defaultLabelAttrs === undefined) { - var paperOptions = paper.options; - var candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); + if (hasCustomMarkup) return undefined; + return builtinDefaultLabelAttrs; + } - if (paperOptions.frontParentOnly) { - // pick the element with the highest `z` index - candidates = candidates.slice(-1); + if (hasCustomMarkup) return defaultLabelAttrs; + return joint.util.merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs); } - var newCandidateView = null; - var prevCandidateView = this._candidateEmbedView; + if (hasCustomMarkup) return joint.util.merge({}, defaultLabelAttrs, labelAttrs); + return joint.util.merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs, labelAttrs); + }, - // iterate over all candidates starting from the last one (has the highest z-index). - for (var i = candidates.length - 1; i >= 0; i--) { + updateLabels: function() { - var candidate = candidates[i]; + if (!this._V.labels) return this; - if (prevCandidateView && prevCandidateView.model.id == candidate.id) { + var model = this.model; + var labels = model.get('labels') || []; + var canLabelMove = this.can('labelMove'); - // candidate remains the same - newCandidateView = prevCandidateView; - break; + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelAttrs = builtinDefaultLabel.attrs; - } else { + var defaultLabel = model._getDefaultLabel(); + var defaultLabelMarkup = defaultLabel.markup; + var defaultLabelAttrs = defaultLabel.attrs; - var view = candidate.findView(paper); - if (paperOptions.validateEmbedding.call(paper, this, view)) { + for (var i = 0, n = labels.length; i < n; i++) { - // flip to the new candidate - newCandidateView = view; - break; - } - } - } + var vLabel = this._labelCache[i]; + vLabel.attr('cursor', (canLabelMove ? 'move' : 'default')); - if (newCandidateView && newCandidateView != prevCandidateView) { - // A new candidate view found. Highlight the new one. - this.clearEmbedding(); - this._candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); - } + var selectors = this._labelSelectors[i]; - if (!newCandidateView && prevCandidateView) { - // No candidate view found. Unhighlight the previous candidate. - this.clearEmbedding(); - } - }, + var label = labels[i]; + var labelMarkup = label.markup; + var labelAttrs = label.attrs; - clearEmbedding: function() { + var attrs = this._mergeLabelAttrs( + (labelMarkup || defaultLabelMarkup), + labelAttrs, + defaultLabelAttrs, + builtinDefaultLabelAttrs + ); - var candidateView = this._candidateEmbedView; - if (candidateView) { - // No candidate view found. Unhighlight the previous candidate. - candidateView.unhighlight(null, { embedding: true }); - this._candidateEmbedView = null; + this.updateDOMSubtreeAttributes(vLabel.node, attrs, { + rootBBox: new g.Rect(label.size), + selectors: selectors + }); } + + return this; }, - finalizeEmbedding: function(opt) { + renderTools: function() { - opt = opt || {}; + if (!this._V.linkTools) return this; - var candidateView = this._candidateEmbedView; - var model = opt.model || this.model; - var paper = opt.paper || this.paper; + // Tools are a group of clickable elements that manipulate the whole link. + // A good example of this is the remove tool that removes the whole link. + // Tools appear after hovering the link close to the `source` element/point of the link + // but are offset a bit so that they don't cover the `marker-arrowhead`. - if (candidateView) { + var $tools = $(this._V.linkTools.node).empty(); + var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); + var tool = V(toolTemplate()); - // We finished embedding. Candidate view is chosen to become the parent of the model. - candidateView.model.embed(model, { ui: true }); - candidateView.unhighlight(null, { embedding: true }); + $tools.append(tool.node); - delete this._candidateEmbedView; - } + // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. + this._toolCache = tool; - joint.util.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); - }, + // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the + // link as well but only if the link is longer than `longLinkLength`. + if (this.options.doubleLinkTools) { - // Interaction. The controller part. - // --------------------------------- + var tool2; + if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { + toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); + tool2 = V(toolTemplate()); + } else { + tool2 = tool.clone(); + } - pointerdown: function(evt, x, y) { + $tools.append(tool2.node); + this._tool2Cache = tool2; + } - var paper = this.paper; + return this; + }, - if ( - evt.target.getAttribute('magnet') && - this.can('addLinkFromMagnet') && - paper.options.validateMagnet.call(paper, this, evt.target) - ) { + renderVertexMarkers: function() { - this.model.startBatch('add-link'); + if (!this._V.markerVertices) return this; - var link = paper.getDefaultLink(this, evt.target); + var $markerVertices = $(this._V.markerVertices.node).empty(); - link.set({ - source: { - id: this.model.id, - selector: this.getSelector(evt.target), - port: evt.target.getAttribute('port') - }, - target: { x: x, y: y } - }); + // A special markup can be given in the `properties.vertexMarkup` property. This might be handy + // if default styling (elements) are not desired. This makes it possible to use any + // SVG elements for .marker-vertex and .marker-vertex-remove tools. + var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); - paper.model.addCell(link); + this.model.vertices().forEach(function(vertex, idx) { - var linkView = this._linkView = paper.findViewByModel(link); + $markerVertices.append(V(markupTemplate(joint.util.assign({ idx: idx }, vertex))).node); + }); - linkView.pointerdown(evt, x, y); - linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); + return this; + }, - } else { + renderArrowheadMarkers: function() { - this._dx = x; - this._dy = y; + // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. + if (!this._V.markerArrowheads) return this; - this.restrictedArea = paper.getRestrictedArea(this); + var $markerArrowheads = $(this._V.markerArrowheads.node); - joint.dia.CellView.prototype.pointerdown.apply(this, arguments); - this.notify('element:pointerdown', evt, x, y); - } - }, + $markerArrowheads.empty(); - pointermove: function(evt, x, y) { + // A special markup can be given in the `properties.vertexMarkup` property. This might be handy + // if default styling (elements) are not desired. This makes it possible to use any + // SVG elements for .marker-vertex and .marker-vertex-remove tools. + var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); - if (this._linkView) { + this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); + this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); - // let the linkview deal with this event - this._linkView.pointermove(evt, x, y); + $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); - } else { + return this; + }, - var grid = this.paper.options.gridSize; + // Updating. + // --------- - if (this.can('elementMove')) { + // Default is to process the `attrs` object and set attributes on subelements based on the selectors. + update: function(model, attributes, opt) { - var position = this.model.get('position'); + opt || (opt = {}); - // Make sure the new element's position always snaps to the current grid after - // translate as the previous one could be calculated with a different grid size. - var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - this._dx, grid); - var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - this._dy, grid); + // update the link path + this.updateConnection(opt); - this.model.translate(tx, ty, { restrictedArea: this.restrictedArea, ui: true }); + // update SVG attributes defined by 'attrs/'. + this.updateDOMSubtreeAttributes(this.el, this.model.attr(), { selectors: this.selectors }); - if (this.paper.options.embeddingMode) { + this.updateDefaultConnectionPath(); - if (!this._inProcessOfEmbedding) { - // Prepare the element for embedding only if the pointer moves. - // We don't want to do unnecessary action with the element - // if an user only clicks/dblclicks on it. - this.prepareEmbedding(); - this._inProcessOfEmbedding = true; - } + // update the label position etc. + this.updateLabelPositions(); + this.updateToolsPosition(); + this.updateArrowheadMarkers(); - this.processEmbedding(); - } - } + this.updateTools(opt); + // Local perpendicular flag (as opposed to one defined on paper). + // Could be enabled inside a connector/router. It's valid only + // during the update execution. + this.options.perpendicular = null; + // Mark that postponed update has been already executed. + this.updatePostponed = false; - this._dx = g.snapToGrid(x, grid); - this._dy = g.snapToGrid(y, grid); + return this; + }, - joint.dia.CellView.prototype.pointermove.apply(this, arguments); - this.notify('element:pointermove', evt, x, y); + removeRedundantLinearVertices: function(opt) { + var link = this.model; + var vertices = link.vertices(); + var conciseVertices = []; + var n = vertices.length; + var m = 0; + for (var i = 0; i < n; i++) { + var current = new g.Point(vertices[i]).round(); + var prev = new g.Point(conciseVertices[m - 1] || this.sourceAnchor).round(); + if (prev.equals(current)) continue; + var next = new g.Point(vertices[i + 1] || this.targetAnchor).round(); + if (prev.equals(next)) continue; + var line = new g.Line(prev, next); + if (line.pointOffset(current) === 0) continue; + conciseVertices.push(vertices[i]); + m++; } + if (n === m) return 0; + link.vertices(conciseVertices, opt); + return (n - m); }, - pointerup: function(evt, x, y) { + updateDefaultConnectionPath: function() { - if (this._linkView) { + var cache = this._V; - // Let the linkview deal with this event. - this._linkView.pointerup(evt, x, y); - this._linkView = null; - this.model.stopBatch('add-link'); + if (cache.connection) { + cache.connection.attr('d', this.getSerializedConnection()); + } - } else { + if (cache.connectionWrap) { + cache.connectionWrap.attr('d', this.getSerializedConnection()); + } - if (this._inProcessOfEmbedding) { - this.finalizeEmbedding(); - this._inProcessOfEmbedding = false; - } + if (cache.markerSource && cache.markerTarget) { + this._translateAndAutoOrientArrows(cache.markerSource, cache.markerTarget); + } + }, - this.notify('element:pointerup', evt, x, y); - joint.dia.CellView.prototype.pointerup.apply(this, arguments); + getEndView: function(type) { + switch (type) { + case 'source': + return this.sourceView || null; + case 'target': + return this.targetView || null; + default: + throw new Error('dia.LinkView: type parameter required.'); } }, - mouseenter: function(evt) { + getEndAnchor: function(type) { + switch (type) { + case 'source': + return new g.Point(this.sourceAnchor); + case 'target': + return new g.Point(this.targetAnchor); + default: + throw new Error('dia.LinkView: type parameter required.'); + } + }, - joint.dia.CellView.prototype.mouseenter.apply(this, arguments); - this.notify('element:mouseenter', evt); + getEndMagnet: function(type) { + switch (type) { + case 'source': + var sourceView = this.sourceView; + if (!sourceView) break; + return this.sourceMagnet || sourceView.el; + case 'target': + var targetView = this.targetView; + if (!targetView) break; + return this.targetMagnet || targetView.el; + default: + throw new Error('dia.LinkView: type parameter required.'); + } + return null; }, - mouseleave: function(evt) { + updateConnection: function(opt) { - joint.dia.CellView.prototype.mouseleave.apply(this, arguments); - this.notify('element:mouseleave', evt); - } -}); + opt = opt || {}; + + var model = this.model; + var route, path; + if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { + // The link is being translated by an ancestor that will + // shift source point, target point and all vertices + // by an equal distance. + var tx = opt.tx || 0; + var ty = opt.ty || 0; -// joint.dia.Link base model. -// -------------------------- + route = (new g.Polyline(this.route)).translate(tx, ty).points; -joint.dia.Link = joint.dia.Cell.extend({ + // translate source and target connection and marker points. + this._translateConnectionPoints(tx, ty); - // The default markup for links. - markup: [ - '', - '', - '', - '', - '', - '', - '', - '' - ].join(''), + // translate the path itself + path = this.path; + path.translate(tx, ty); - labelMarkup: [ - '', - '', - '', - '' - ].join(''), + } else { - toolMarkup: [ - '', - '', - '', - '', - 'Remove link.', - '', - '', - '', - '', - 'Link options.', - '', - '' - ].join(''), + var vertices = model.vertices(); + // 1. Find Anchors - // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). - // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for - // dragging vertices (changin their position). The latter is used for removing vertices. - vertexMarkup: [ - '', - '', - '', - '', - 'Remove vertex.', - '', - '' - ].join(''), + var anchors = this.findAnchors(vertices); + var sourceAnchor = this.sourceAnchor = anchors.source; + var targetAnchor = this.targetAnchor = anchors.target; - arrowheadMarkup: [ - '', - '', - '' - ].join(''), + // 2. Find Route + route = this.findRoute(vertices, opt); - defaults: { + // 3. Find Connection Points + var connectionPoints = this.findConnectionPoints(route, sourceAnchor, targetAnchor); + var sourcePoint = this.sourcePoint = connectionPoints.source; + var targetPoint = this.targetPoint = connectionPoints.target; - type: 'link', - source: {}, - target: {} - }, + // 3b. Find Marker Connection Point - Backwards Compatibility + var markerPoints = this.findMarkerPoints(route, sourcePoint, targetPoint); - isLink: function() { + // 4. Find Connection + path = this.findPath(route, markerPoints.source || sourcePoint, markerPoints.target || targetPoint); + } - return true; + this.route = route; + this.path = path; + this.metrics = {}; }, - disconnect: function() { + findMarkerPoints: function(route, sourcePoint, targetPoint) { - return this.set({ source: g.point(0, 0), target: g.point(0, 0) }); - }, + var firstWaypoint = route[0]; + var lastWaypoint = route[route.length - 1]; - // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. - label: function(idx, value, opt) { + // Move the source point by the width of the marker taking into account + // its scale around x-axis. Note that scale is the only transform that + // makes sense to be set in `.marker-source` attributes object + // as all other transforms (translate/rotate) will be replaced + // by the `translateAndAutoOrient()` function. + var cache = this._markerCache; + // cache source and target points + var sourceMarkerPoint, targetMarkerPoint; - idx = idx || 0; + if (this._V.markerSource) { - // Is it a getter? - if (arguments.length <= 1) { - return this.prop(['labels', idx]); + cache.sourceBBox = cache.sourceBBox || this._V.markerSource.getBBox(); + sourceMarkerPoint = g.point(sourcePoint).move( + firstWaypoint || targetPoint, + cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 + ).round(); } - return this.prop(['labels', idx], value, opt); - }, + if (this._V.markerTarget) { - translate: function(tx, ty, opt) { + cache.targetBBox = cache.targetBBox || this._V.markerTarget.getBBox(); + targetMarkerPoint = g.point(targetPoint).move( + lastWaypoint || sourcePoint, + cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 + ).round(); + } - // enrich the option object - opt = opt || {}; - opt.translateBy = opt.translateBy || this.id; - opt.tx = tx; - opt.ty = ty; + // if there was no markup for the marker, use the connection point. + cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); + cache.targetPoint = targetMarkerPoint || targetPoint.clone(); - return this.applyToPoints(function(p) { - return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; - }, opt); + return { + source: sourceMarkerPoint, + target: targetMarkerPoint + } }, - scale: function(sx, sy, origin, opt) { + findAnchors: function(vertices) { - return this.applyToPoints(function(p) { - return g.point(p).scale(sx, sy, origin).toJSON(); - }, opt); - }, + var model = this.model; + var firstVertex = vertices[0]; + var lastVertex = vertices[vertices.length - 1]; + var sourceDef = model.get('source'); + var targetDef = model.get('target'); + var sourceView = this.sourceView; + var targetView = this.targetView; + var sourceMagnet, targetMagnet; + + // Anchor Source + var sourceAnchor; + if (sourceView) { + sourceMagnet = (this.sourceMagnet || sourceView.el); + var sourceAnchorRef; + if (firstVertex) { + sourceAnchorRef = new g.Point(firstVertex); + } else if (targetView) { + // TODO: the source anchor reference is not a point, how to deal with this? + sourceAnchorRef = this.targetMagnet || targetView.el; + } else { + sourceAnchorRef = new g.Point(targetDef); + } + sourceAnchor = this.getAnchor(sourceDef.anchor, sourceView, sourceMagnet, sourceAnchorRef, 'source'); + } else { + sourceAnchor = new g.Point(sourceDef); + } - applyToPoints: function(fn, opt) { + // Anchor Target + var targetAnchor; + if (targetView) { + targetMagnet = (this.targetMagnet || targetView.el); + var targetAnchorRef = new g.Point(lastVertex || sourceAnchor); + targetAnchor = this.getAnchor(targetDef.anchor, targetView, targetMagnet, targetAnchorRef, 'target'); + } else { + targetAnchor = new g.Point(targetDef); + } - if (!joint.util.isFunction(fn)) { - throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); + // Con + return { + source: sourceAnchor, + target: targetAnchor } + }, - var attrs = {}; + findConnectionPoints: function(route, sourceAnchor, targetAnchor) { - var source = this.get('source'); - if (!source.id) { - attrs.source = fn(source); + var firstWaypoint = route[0]; + var lastWaypoint = route[route.length - 1]; + var model = this.model; + var sourceDef = model.get('source'); + var targetDef = model.get('target'); + var sourceView = this.sourceView; + var targetView = this.targetView; + var paperOptions = this.paper.options; + var sourceMagnet, targetMagnet; + + // Connection Point Source + var sourcePoint; + if (sourceView) { + sourceMagnet = (this.sourceMagnet || sourceView.el); + var sourceConnectionPointDef = sourceDef.connectionPoint || paperOptions.defaultConnectionPoint; + var sourcePointRef = firstWaypoint || targetAnchor; + var sourceLine = new g.Line(sourcePointRef, sourceAnchor); + sourcePoint = this.getConnectionPoint(sourceConnectionPointDef, sourceView, sourceMagnet, sourceLine, 'source'); + } else { + sourcePoint = sourceAnchor; + } + // Connection Point Target + var targetPoint; + if (targetView) { + targetMagnet = (this.targetMagnet || targetView.el); + var targetConnectionPointDef = targetDef.connectionPoint || paperOptions.defaultConnectionPoint; + var targetPointRef = lastWaypoint || sourceAnchor; + var targetLine = new g.Line(targetPointRef, targetAnchor); + targetPoint = this.getConnectionPoint(targetConnectionPointDef, targetView, targetMagnet, targetLine, 'target'); + } else { + targetPoint = targetAnchor; } - var target = this.get('target'); - if (!target.id) { - attrs.target = fn(target); + return { + source: sourcePoint, + target: targetPoint } + }, - var vertices = this.get('vertices'); - if (vertices && vertices.length > 0) { - attrs.vertices = vertices.map(fn); + getAnchor: function(anchorDef, cellView, magnet, ref, endType) { + + if (!anchorDef) { + var paperOptions = this.paper.options; + if (paperOptions.perpendicularLinks || this.options.perpendicular) { + // Backwards compatibility + // If `perpendicularLinks` flag is set on the paper and there are vertices + // on the link, then try to find a connection point that makes the link perpendicular + // even though the link won't point to the center of the targeted object. + anchorDef = { name: 'perpendicular' }; + } else { + anchorDef = paperOptions.defaultAnchor; + } } - return this.set(attrs, opt); + if (!anchorDef) throw new Error('Anchor required.'); + var anchorFn; + if (typeof anchorDef === 'function') { + anchorFn = anchorDef; + } else { + var anchorName = anchorDef.name; + anchorFn = joint.anchors[anchorName]; + if (typeof anchorFn !== 'function') throw new Error('Unknown anchor: ' + anchorName); + } + var anchor = anchorFn.call(this, cellView, magnet, ref, anchorDef.args || {}, endType, this); + if (anchor) return anchor.round(this.decimalsRounding); + return new g.Point() }, - reparent: function(opt) { - var newParent; + getConnectionPoint: function(connectionPointDef, view, magnet, line, endType) { - if (this.graph) { + var connectionPoint; + var anchor = line.end; + // Backwards compatibility + var paperOptions = this.paper.options; + if (typeof paperOptions.linkConnectionPoint === 'function') { + connectionPoint = paperOptions.linkConnectionPoint(this, view, magnet, line.start, endType); + if (connectionPoint) return connectionPoint; + } - var source = this.graph.getCell(this.get('source').id); - var target = this.graph.getCell(this.get('target').id); - var prevParent = this.graph.getCell(this.get('parent')); + if (!connectionPointDef) return anchor; + var connectionPointFn; + if (typeof connectionPointDef === 'function') { + connectionPointFn = connectionPointDef; + } else { + var connectionPointName = connectionPointDef.name; + connectionPointFn = joint.connectionPoints[connectionPointName]; + if (typeof connectionPointFn !== 'function') throw new Error('Unknown connection point: ' + connectionPointName); + } + connectionPoint = connectionPointFn.call(this, line, view, magnet, connectionPointDef.args || {}, endType, this); + if (connectionPoint) return connectionPoint.round(this.decimalsRounding); + return anchor; + }, - if (source && target) { - newParent = this.graph.getCommonAncestor(source, target); - } + _translateConnectionPoints: function(tx, ty) { - if (prevParent && (!newParent || newParent.id !== prevParent.id)) { - // Unembed the link if source and target has no common ancestor - // or common ancestor changed - prevParent.unembed(this, opt); - } + var cache = this._markerCache; - if (newParent) { - newParent.embed(this, opt); - } - } + cache.sourcePoint.offset(tx, ty); + cache.targetPoint.offset(tx, ty); + this.sourcePoint.offset(tx, ty); + this.targetPoint.offset(tx, ty); + this.sourceAnchor.offset(tx, ty); + this.targetAnchor.offset(tx, ty); + }, - return newParent; + // if label position is a number, normalize it to a position object + // this makes sure that label positions can be merged properly + _normalizeLabelPosition: function(labelPosition) { + + if (typeof labelPosition === 'number') return { distance: labelPosition, offset: null, args: null }; + return labelPosition; }, - hasLoop: function(opt) { + updateLabelPositions: function() { - opt = opt || {}; + if (!this._V.labels) return this; - var sourceId = this.get('source').id; - var targetId = this.get('target').id; + var path = this.path; + if (!path) return this; - if (!sourceId || !targetId) { - // Link "pinned" to the paper does not have a loop. - return false; - } + // This method assumes all the label nodes are stored in the `this._labelCache` hash table + // by their indices in the `this.get('labels')` array. This is done in the `renderLabels()` method. - var loop = sourceId === targetId; + var model = this.model; + var labels = model.get('labels') || []; + if (!labels.length) return this; - // Note that there in the deep mode a link can have a loop, - // even if it connects only a parent and its embed. - // A loop "target equals source" is valid in both shallow and deep mode. - if (!loop && opt.deep && this.graph) { + var builtinDefaultLabel = model._builtins.defaultLabel; + var builtinDefaultLabelPosition = builtinDefaultLabel.position; - var sourceElement = this.graph.getCell(sourceId); - var targetElement = this.graph.getCell(targetId); + var defaultLabel = model._getDefaultLabel(); + var defaultLabelPosition = this._normalizeLabelPosition(defaultLabel.position); - loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); - } + var defaultPosition = joint.util.merge({}, builtinDefaultLabelPosition, defaultLabelPosition); - return loop; - }, + for (var idx = 0, n = labels.length; idx < n; idx++) { - getSourceElement: function() { + var label = labels[idx]; + var labelPosition = this._normalizeLabelPosition(label.position); + + var position = joint.util.merge({}, defaultPosition, labelPosition); - var source = this.get('source'); + var labelPoint = this.getLabelCoordinates(position); + this._labelCache[idx].attr('transform', 'translate(' + labelPoint.x + ', ' + labelPoint.y + ')'); + } - return (source && source.id && this.graph && this.graph.getCell(source.id)) || null; + return this; }, - getTargetElement: function() { + updateToolsPosition: function() { - var target = this.get('target'); + if (!this._V.linkTools) return this; - return (target && target.id && this.graph && this.graph.getCell(target.id)) || null; - }, + // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. + // Note that the offset is hardcoded here. The offset should be always + // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking + // this up all the time would be slow. - // Returns the common ancestor for the source element, - // target element and the link itself. - getRelationshipAncestor: function() { + var scale = ''; + var offset = this.options.linkToolsOffset; + var connectionLength = this.getConnectionLength(); - var connectionAncestor; + // Firefox returns connectionLength=NaN in odd cases (for bezier curves). + // In that case we won't update tools position at all. + if (!Number.isNaN(connectionLength)) { - if (this.graph) { + // If the link is too short, make the tools half the size and the offset twice as low. + if (connectionLength < this.options.shortLinkLength) { + scale = 'scale(.5)'; + offset /= 2; + } - var cells = [ - this, - this.getSourceElement(), // null if source is a point - this.getTargetElement() // null if target is a point - ].filter(function(item) { - return !!item; - }); + var toolPosition = this.getPointAtLength(offset); - connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); - } + this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); - return connectionAncestor || null; - }, + if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { - // Is source, target and the link itself embedded in a given cell? - isRelationshipEmbeddedIn: function(cell) { + var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; - var cellId = (joint.util.isString(cell) || joint.util.isNumber(cell)) ? cell : cell.id; - var ancestor = this.getRelationshipAncestor(); + toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); + this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + this._tool2Cache.attr('visibility', 'visible'); - return !!ancestor && (ancestor.id === cellId || ancestor.isEmbeddedIn(cellId)); - } -}, - { - endsEqual: function(a, b) { + } else if (this.options.doubleLinkTools) { - var portsEqual = a.port === b.port || !a.port && !b.port; - return a.id === b.id && portsEqual; + this._tool2Cache.attr('visibility', 'hidden'); + } } - }); + return this; + }, -// joint.dia.Link base view and controller. -// ---------------------------------------- + updateArrowheadMarkers: function() { -joint.dia.LinkView = joint.dia.CellView.extend({ + if (!this._V.markerArrowheads) return this; - className: function() { + // getting bbox of an element with `display="none"` in IE9 ends up with access violation + if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; - var classNames = joint.dia.CellView.prototype.className.apply(this).split(' '); + var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; + this._V.sourceArrowhead.scale(sx); + this._V.targetArrowhead.scale(sx); - classNames.push('link'); + this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); - return classNames.join(' '); + return this; }, - options: { + // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, + // it stops listening on the previous one and starts listening to the new one. + createWatcher: function(endType) { - shortLinkLength: 100, - doubleLinkTools: false, - longLinkLength: 160, - linkToolsOffset: 40, - doubleLinkToolsOffset: 60, - sampleInterval: 50 - }, + // create handler for specific end type (source|target). + var onModelChange = function(endModel, opt) { + this.onEndModelChange(endType, endModel, opt); + }; - _z: null, + function watchEndModel(link, end) { - initialize: function(options) { + end = end || {}; - joint.dia.CellView.prototype.initialize.apply(this, arguments); + var endModel = null; + var previousEnd = link.previous(endType) || {}; - // create methods in prototype, so they can be accessed from any instance and - // don't need to be create over and over - if (typeof this.constructor.prototype.watchSource !== 'function') { - this.constructor.prototype.watchSource = this.createWatcher('source'); - this.constructor.prototype.watchTarget = this.createWatcher('target'); - } + if (previousEnd.id) { + this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); + } - // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to - // `` nodes wrapped by Vectorizer. This allows for quick access to the - // nodes in `updateLabelPosition()` in order to update the label positions. - this._labelCache = {}; + if (end.id) { + // If the observed model changes, it caches a new bbox and do the link update. + endModel = this.paper.getModelById(end.id); + this.listenTo(endModel, 'change', onModelChange); + } - // keeps markers bboxes and positions again for quicker access - this._markerCache = {}; + onModelChange.call(this, endModel, { cacheOnly: true }); - // bind events - this.startListening(); + return this; + } + + return watchEndModel; }, - startListening: function() { + onEndModelChange: function(endType, endModel, opt) { + var doUpdate = !opt.cacheOnly; var model = this.model; + var end = model.get(endType) || {}; - this.listenTo(model, 'change:markup', this.render); - this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); - this.listenTo(model, 'change:toolMarkup', this.onToolsChange); - this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); - this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); - this.listenTo(model, 'change:source', this.onSourceChange); - this.listenTo(model, 'change:target', this.onTargetChange); - }, + if (endModel) { - onSourceChange: function(cell, source, opt) { + var selector = this.constructor.makeSelector(end); + var oppositeEndType = endType == 'source' ? 'target' : 'source'; + var oppositeEnd = model.get(oppositeEndType) || {}; + var endId = end.id; + var oppositeEndId = oppositeEnd.id; + var oppositeSelector = oppositeEndId && this.constructor.makeSelector(oppositeEnd); - // Start watching the new source model. - this.watchSource(cell, source); - // This handler is called when the source attribute is changed. - // This can happen either when someone reconnects the link (or moves arrowhead), - // or when an embedded link is translated by its ancestor. - // 1. Always do update. - // 2. Do update only if the opposite end ('target') is also a point. - if (!opt.translateBy || !this.model.get('target').id) { - opt.updateConnectionOnly = true; - this.update(this.model, null, opt); - } - }, + // Caching end models bounding boxes. + // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached + // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed + // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). + if (opt.handleBy === this.cid && (endId === oppositeEndId) && selector == oppositeSelector) { - onTargetChange: function(cell, target, opt) { + // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the + // second time now. There is no need to calculate bbox and find magnet element again. + // It was calculated already for opposite link end. + this[endType + 'View'] = this[oppositeEndType + 'View']; + this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; - // Start watching the new target model. - this.watchTarget(cell, target); - // See `onSourceChange` method. - if (!opt.translateBy) { - opt.updateConnectionOnly = true; - this.update(this.model, null, opt); - } - }, + } else if (opt.translateBy) { + // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. + // If `opt.translateBy` is an ID of the element that was originally translated. - onVerticesChange: function(cell, changed, opt) { + // Noop - this.renderVertexMarkers(); + } else { + // The slowest path, source/target could have been rotated or resized or any attribute + // that affects the bounding box of the view might have been changed. - // If the vertices have been changed by a translation we do update only if the link was - // the only link that was translated. If the link was translated via another element which the link - // is embedded in, this element will be translated as well and that triggers an update. - // Note that all embeds in a model are sorted - first comes links, then elements. - if (!opt.translateBy || opt.translateBy === this.model.id) { - // Vertices were changed (not as a reaction on translate) - // or link.translate() was called or - opt.updateConnectionOnly = true; - this.update(cell, null, opt); - } - }, + var connectedModel = this.paper.model.getCell(endId); + if (!connectedModel) throw new Error('LinkView: invalid ' + endType + ' cell.'); + var connectedView = connectedModel.findView(this.paper); + if (connectedView) { + var connectedMagnet = connectedView.getMagnetFromLinkEnd(end); + if (connectedMagnet === connectedView.el) connectedMagnet = null; + this[endType + 'View'] = connectedView; + this[endType + 'Magnet'] = connectedMagnet; + } else { + // the view is not rendered yet + this[endType + 'View'] = this[endType + 'Magnet'] = null; + } + } - onToolsChange: function() { + if (opt.handleBy === this.cid && opt.translateBy && + model.isEmbeddedIn(endModel) && + !joint.util.isEmpty(model.get('vertices'))) { + // Loop link whose element was translated and that has vertices (that need to be translated with + // the parent in which my element is embedded). + // If the link is embedded, has a loop and vertices and the end model + // has been translated, do not update yet. There are vertices still to be updated (change:vertices + // event will come in the next turn). + doUpdate = false; + } - this.renderTools().updateToolsPosition(); - }, + if (!this.updatePostponed && oppositeEndId) { + // The update was not postponed (that can happen e.g. on the first change event) and the opposite + // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if + // we're reacting on change:source event, the oppositeEnd is the target model). - onLabelsChange: function(link, labels, opt) { + var oppositeEndModel = this.paper.getModelById(oppositeEndId); - var requireRender = true; + // Passing `handleBy` flag via event option. + // Note that if we are listening to the same model for event 'change' twice. + // The same event will be handled by this method also twice. + if (end.id === oppositeEnd.id) { + // We're dealing with a loop link. Tell the handlers in the next turn that they should update + // the link instead of me. (We know for sure there will be a next turn because + // loop links react on at least two events: change on the source model followed by a change on + // the target model). + opt.handleBy = this.cid; + } - var previousLabels = this.model.previous('labels'); + if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { - if (previousLabels) { - // Here is an optimalization for cases when we know, that change does - // not require rerendering of all labels. - if (('propertyPathArray' in opt) && ('propertyValue' in opt)) { - // The label is setting by `prop()` method - var pathArray = opt.propertyPathArray || []; - var pathLength = pathArray.length; - if (pathLength > 1) { - // We are changing a single label here e.g. 'labels/0/position' - var labelExists = !!previousLabels[pathArray[1]]; - if (labelExists) { - if (pathLength === 2) { - // We are changing the entire label. Need to check if the - // markup is also being changed. - requireRender = ('markup' in Object(opt.propertyValue)); - } else if (pathArray[2] !== 'markup') { - // We are changing a label property but not the markup - requireRender = false; - } - } + // Here are two options: + // - Source and target are connected to the same model (not necessarily the same port). + // - Both end models are translated by the same ancestor. We know that opposite end + // model will be translated in the next turn as well. + // In both situations there will be more changes on the model that trigger an + // update. So there is no need to update the linkView yet. + this.updatePostponed = true; + doUpdate = false; } } - } - if (requireRender) { - this.renderLabels(); } else { - this.updateLabels(); + + // the link end is a point ~ rect 1x1 + this[endType + 'View'] = this[endType + 'Magnet'] = null; } - this.updateLabelPositions(); + if (doUpdate) { + this.update(model, null, opt); + } }, - // Rendering - //---------- + _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { - render: function() { + // Make the markers "point" to their sticky points being auto-oriented towards + // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. + var route = joint.util.toArray(this.route); + if (sourceArrow) { + sourceArrow.translateAndAutoOrient( + this.sourcePoint, + route[0] || this.targetPoint, + this.paper.viewport + ); + } - this.$el.empty(); + if (targetArrow) { + targetArrow.translateAndAutoOrient( + this.targetPoint, + route[route.length - 1] || this.sourcePoint, + this.paper.viewport + ); + } + }, - // A special markup can be given in the `properties.markup` property. This might be handy - // if e.g. arrowhead markers should be `` elements or any other element than ``s. - // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors - // of elements with special meaning though. Therefore, those classes should be preserved in any - // special markup passed in `properties.markup`. - var model = this.model; - var markup = model.get('markup') || model.markup; - var children = V(markup); + _getDefaultLabelPositionArgs: function() { - // custom markup may contain only one children - if (!Array.isArray(children)) children = [children]; + var defaultLabel = this.model._getDefaultLabel(); + var defaultLabelPosition = defaultLabel.position || {}; + return defaultLabelPosition.args; + }, - // Cache all children elements for quicker access. - this._V = {}; // vectorized markup; - children.forEach(function(child) { + _getLabelPositionArgs: function(idx) { - var className = child.attr('class'); + var labelPosition = this.model.label(idx).position || {}; + return labelPosition.args; + }, - if (className) { - // Strip the joint class name prefix, if there is one. - className = joint.util.removeClassNamePrefix(className); - this._V[$.camelCase(className)] = child; - } + // merge default label position args into label position args + // keep `undefined` or `null` because `{}` means something else + _mergeLabelPositionArgs: function(labelPositionArgs, defaultLabelPositionArgs) { - }, this); + if (labelPositionArgs === null) return null; + if (labelPositionArgs === undefined) { - // Only the connection path is mandatory - if (!this._V.connection) throw new Error('link: no connection path in the markup'); + if (defaultLabelPositionArgs === null) return null; + return defaultLabelPositionArgs; + } - // partial rendering - this.renderTools(); - this.renderVertexMarkers(); - this.renderArrowheadMarkers(); + return joint.util.merge({}, defaultLabelPositionArgs, labelPositionArgs); + }, - this.vel.append(children); + // Add default label at given position at end of `labels` array. + // Assigns relative coordinates by default. + // `opt.absoluteDistance` forces absolute coordinates. + // `opt.reverseDistance` forces reverse absolute coordinates (if absoluteDistance = true). + // `opt.absoluteOffset` forces absolute coordinates for offset. + addLabel: function(x, y, opt) { - // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox - // returns zero values) - this.renderLabels(); + // accept input in form `{ x, y }, opt` or `x, y, opt` + var isPointProvided = (typeof x !== 'number'); + var localX = isPointProvided ? x.x : x; + var localY = isPointProvided ? x.y : y; + var localOpt = isPointProvided ? y : opt; - // start watching the ends of the link for changes - this.watchSource(model, model.get('source')) - .watchTarget(model, model.get('target')) - .update(); + var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs(); + var labelPositionArgs = localOpt; + var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); - return this; + var label = { position: this.getLabelPosition(localX, localY, positionArgs) }; + var idx = -1; + this.model.insertLabel(idx, label, localOpt); + return idx; }, - renderLabels: function() { + // Add a new vertex at calculated index to the `vertices` array. + addVertex: function(x, y, opt) { - var vLabels = this._V.labels; - if (!vLabels) { - return this; + // accept input in form `{ x, y }, opt` or `x, y, opt` + var isPointProvided = (typeof x !== 'number'); + var localX = isPointProvided ? x.x : x; + var localY = isPointProvided ? x.y : y; + var localOpt = isPointProvided ? y : opt; + + var vertex = { x: localX, y: localY }; + var idx = this.getVertexIndex(localX, localY); + this.model.insertVertex(idx, vertex, localOpt); + return idx; + }, + + // Send a token (an SVG element, usually a circle) along the connection path. + // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)` + // `opt.duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. + // `opt.directon` is optional and it determines whether the token goes from source to target or other way round (`reverse`) + // `opt.connection` is an optional selector to the connection path. + // `callback` is optional and is a function to be called once the token reaches the target. + sendToken: function(token, opt, callback) { + + function onAnimationEnd(vToken, callback) { + return function() { + vToken.remove(); + if (typeof callback === 'function') { + callback(); + } + }; } - vLabels.empty(); - - var model = this.model; - var labels = model.get('labels') || []; - var labelCache = this._labelCache = {}; - var labelsCount = labels.length; - if (labelsCount === 0) { - return this; + var duration, isReversed, selector; + if (joint.util.isObject(opt)) { + duration = opt.duration; + isReversed = (opt.direction === 'reverse'); + selector = opt.connection; + } else { + // Backwards compatibility + duration = opt; + isReversed = false; + selector = null; } - var labelTemplate = joint.util.template(model.get('labelMarkup') || model.labelMarkup); - // This is a prepared instance of a vectorized SVGDOM node for the label element resulting from - // compilation of the labelTemplate. The purpose is that all labels will just `clone()` this - // node to create a duplicate. - var labelNodeInstance = V(labelTemplate()); - - for (var i = 0; i < labelsCount; i++) { + duration = duration || 1000; - var label = labels[i]; - var labelMarkup = label.markup; - // Cache label nodes so that the `updateLabels()` can just update the label node positions. - var vLabelNode = labelCache[i] = (labelMarkup) - ? V('g').append(V(labelMarkup)) - : labelNodeInstance.clone(); + var animationAttributes = { + dur: duration + 'ms', + repeatCount: 1, + calcMode: 'linear', + fill: 'freeze' + }; - vLabelNode - .addClass('label') - .attr('label-idx', i) - .appendTo(vLabels); + if (isReversed) { + animationAttributes.keyPoints = '1;0'; + animationAttributes.keyTimes = '0;1'; } - this.updateLabels(); - - return this; - }, - - updateLabels: function() { + var vToken = V(token); + var connection; + if (typeof selector === 'string') { + // Use custom connection path. + connection = this.findBySelector(selector, this.el, this.selectors)[0]; + } else { + // Select connection path automatically. + var cache = this._V; + connection = (cache.connection) ? cache.connection.node : this.el.querySelector('path'); + } - if (!this._V.labels) { - return this; + if (!(connection instanceof SVGPathElement)) { + throw new Error('dia.LinkView: token animation requires a valid connection path.'); } - var labels = this.model.get('labels') || []; - var canLabelMove = this.can('labelMove'); + vToken + .appendTo(this.paper.viewport) + .animateAlongPath(animationAttributes, connection); - for (var i = 0, n = labels.length; i < n; i++) { + setTimeout(onAnimationEnd(vToken, callback), duration); + }, - var vLabel = this._labelCache[i]; - var label = labels[i]; + findRoute: function(vertices) { - vLabel.attr('cursor', (canLabelMove ? 'move' : 'default')); + vertices || (vertices = []); - var labelAttrs = label.attrs; - if (!label.markup) { - // Default attributes to maintain backwards compatibility - labelAttrs = joint.util.merge({ - text: { - textAnchor: 'middle', - fontSize: 14, - fill: '#000000', - pointerEvents: 'none', - yAlignment: 'middle' - }, - rect: { - ref: 'text', - fill: '#ffffff', - rx: 3, - ry: 3, - refWidth: 1, - refHeight: 1, - refX: 0, - refY: 0 - } - }, labelAttrs); - } + var namespace = joint.routers; + var router = this.model.router(); + var defaultRouter = this.paper.options.defaultRouter; - this.updateDOMSubtreeAttributes(vLabel.node, labelAttrs, { - rootBBox: g.Rect(label.size) - }); + if (!router) { + if (defaultRouter) router = defaultRouter; + else return vertices.map(g.Point, g); // no router specified } - return this; - }, + var routerFn = joint.util.isFunction(router) ? router : namespace[router.name]; + if (!joint.util.isFunction(routerFn)) { + throw new Error('dia.LinkView: unknown router: "' + router.name + '".'); + } - renderTools: function() { + var args = router.args || {}; - if (!this._V.linkTools) return this; + var route = routerFn.call( + this, // context + vertices, // vertices + args, // options + this // linkView + ); - // Tools are a group of clickable elements that manipulate the whole link. - // A good example of this is the remove tool that removes the whole link. - // Tools appear after hovering the link close to the `source` element/point of the link - // but are offset a bit so that they don't cover the `marker-arrowhead`. + if (!route) return vertices.map(g.Point, g); + return route; + }, - var $tools = $(this._V.linkTools.node).empty(); - var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); - var tool = V(toolTemplate()); + // Return the `d` attribute value of the `` element representing the link + // between `source` and `target`. + findPath: function(route, sourcePoint, targetPoint) { - $tools.append(tool.node); + var namespace = joint.connectors; + var connector = this.model.connector(); + var defaultConnector = this.paper.options.defaultConnector; - // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. - this._toolCache = tool; + if (!connector) { + connector = defaultConnector || {}; + } - // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the - // link as well but only if the link is longer than `longLinkLength`. - if (this.options.doubleLinkTools) { + var connectorFn = joint.util.isFunction(connector) ? connector : namespace[connector.name]; + if (!joint.util.isFunction(connectorFn)) { + throw new Error('dia.LinkView: unknown connector: "' + connector.name + '".'); + } - var tool2; - if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { - toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); - tool2 = V(toolTemplate()); - } else { - tool2 = tool.clone(); - } + var args = joint.util.clone(connector.args || {}); + args.raw = true; // Request raw g.Path as the result. - $tools.append(tool2.node); - this._tool2Cache = tool2; + var path = connectorFn.call( + this, // context + sourcePoint, // start point + targetPoint, // end point + route, // vertices + args, // options + this // linkView + ); + + if (typeof path === 'string') { + // Backwards compatibility for connectors not supporting `raw` option. + path = new g.Path(V.normalizePathData(path)); } - return this; + return path; }, - renderVertexMarkers: function() { + // Public API. + // ----------- - if (!this._V.markerVertices) return this; + getConnection: function() { - var $markerVertices = $(this._V.markerVertices.node).empty(); + var path = this.path; + if (!path) return null; - // A special markup can be given in the `properties.vertexMarkup` property. This might be handy - // if default styling (elements) are not desired. This makes it possible to use any - // SVG elements for .marker-vertex and .marker-vertex-remove tools. - var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); + return path.clone(); + }, - joint.util.toArray(this.model.get('vertices')).forEach(function(vertex, idx) { + getSerializedConnection: function() { - $markerVertices.append(V(markupTemplate(joint.util.assign({ idx: idx }, vertex))).node); - }); + var path = this.path; + if (!path) return null; - return this; + var metrics = this.metrics; + if (metrics.hasOwnProperty('data')) return metrics.data; + var data = path.serialize(); + metrics.data = data; + return data; }, - renderArrowheadMarkers: function() { + getConnectionSubdivisions: function() { - // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. - if (!this._V.markerArrowheads) return this; + var path = this.path; + if (!path) return null; - var $markerArrowheads = $(this._V.markerArrowheads.node); + var metrics = this.metrics; + if (metrics.hasOwnProperty('segmentSubdivisions')) return metrics.segmentSubdivisions; + var subdivisions = path.getSegmentSubdivisions(); + metrics.segmentSubdivisions = subdivisions; + return subdivisions; + }, - $markerArrowheads.empty(); + getConnectionLength: function() { - // A special markup can be given in the `properties.vertexMarkup` property. This might be handy - // if default styling (elements) are not desired. This makes it possible to use any - // SVG elements for .marker-vertex and .marker-vertex-remove tools. - var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); + var path = this.path; + if (!path) return 0; - this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); - this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); + var metrics = this.metrics; + if (metrics.hasOwnProperty('length')) return metrics.length; + var length = path.length({ segmentSubdivisions: this.getConnectionSubdivisions() }); + metrics.length = length; + return length; + }, - $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); + getPointAtLength: function(length) { - return this; - }, + var path = this.path; + if (!path) return null; - // Updating - //--------- + return path.pointAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - // Default is to process the `attrs` object and set attributes on subelements based on the selectors. - update: function(model, attributes, opt) { + getPointAtRatio: function(ratio) { - opt = opt || {}; + var path = this.path; + if (!path) return null; - if (!opt.updateConnectionOnly) { - // update SVG attributes defined by 'attrs/'. - this.updateDOMSubtreeAttributes(this.el, this.model.attr()); - } + return path.pointAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - // update the link path, label position etc. - this.updateConnection(opt); - this.updateLabelPositions(); - this.updateToolsPosition(); - this.updateArrowheadMarkers(); + getTangentAtLength: function(length) { - // Local perpendicular flag (as opposed to one defined on paper). - // Could be enabled inside a connector/router. It's valid only - // during the update execution. - this.options.perpendicular = null; - // Mark that postponed update has been already executed. - this.updatePostponed = false; + var path = this.path; + if (!path) return null; - return this; + return path.tangentAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() }); }, - updateConnection: function(opt) { + getTangentAtRatio: function(ratio) { - opt = opt || {}; + var path = this.path; + if (!path) return null; - var model = this.model; - var route; + return path.tangentAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { - // The link is being translated by an ancestor that will - // shift source point, target point and all vertices - // by an equal distance. - var tx = opt.tx || 0; - var ty = opt.ty || 0; + getClosestPoint: function(point) { - route = this.route = joint.util.toArray(this.route).map(function(point) { - // translate point by point by delta translation - return g.point(point).offset(tx, ty); - }); + var path = this.path; + if (!path) return null; - // translate source and target connection and marker points. - this._translateConnectionPoints(tx, ty); + return path.closestPoint(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, - } else { - // Necessary path finding - route = this.route = this.findRoute(model.get('vertices') || [], opt); - // finds all the connection points taking new vertices into account - this._findConnectionPoints(route); - } + getClosestPointLength: function(point) { + + var path = this.path; + if (!path) return null; - var pathData = this.getPathData(route); + return path.closestPointLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); + }, + + getClosestPointRatio: function(point) { - // The markup needs to contain a `.connection` - this._V.connection.attr('d', pathData); - this._V.connectionWrap && this._V.connectionWrap.attr('d', pathData); + var path = this.path; + if (!path) return null; - this._translateAndAutoOrientArrows(this._V.markerSource, this._V.markerTarget); + return path.closestPointNormalizedLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() }); }, - _findConnectionPoints: function(vertices) { + // accepts options `absoluteDistance: boolean`, `reverseDistance: boolean`, `absoluteOffset: boolean` + // to move beyond connection endpoints, absoluteOffset has to be set + getLabelPosition: function(x, y, opt) { - // cache source and target points - var sourcePoint, targetPoint, sourceMarkerPoint, targetMarkerPoint; - var verticesArr = joint.util.toArray(vertices); + var position = {}; - var firstVertex = verticesArr[0]; + var localOpt = opt || {}; + if (opt) position.args = opt; - sourcePoint = this.getConnectionPoint( - 'source', this.model.get('source'), firstVertex || this.model.get('target') - ).round(); + var isDistanceRelative = !localOpt.absoluteDistance; // relative by default + var isDistanceAbsoluteReverse = (localOpt.absoluteDistance && localOpt.reverseDistance); // non-reverse by default + var isOffsetAbsolute = localOpt.absoluteOffset; // offset is non-absolute by default - var lastVertex = verticesArr[verticesArr.length - 1]; + var path = this.path; + var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }; - targetPoint = this.getConnectionPoint( - 'target', this.model.get('target'), lastVertex || sourcePoint - ).round(); + var labelPoint = new g.Point(x, y); + var t = path.closestPointT(labelPoint, pathOpt); - // Move the source point by the width of the marker taking into account - // its scale around x-axis. Note that scale is the only transform that - // makes sense to be set in `.marker-source` attributes object - // as all other transforms (translate/rotate) will be replaced - // by the `translateAndAutoOrient()` function. - var cache = this._markerCache; + // GET DISTANCE: - if (this._V.markerSource) { + var labelDistance = path.lengthAtT(t, pathOpt); + if (isDistanceRelative) labelDistance = (labelDistance / this.getConnectionLength()) || 0; // fix to prevent NaN for 0 length + if (isDistanceAbsoluteReverse) labelDistance = (-1 * (this.getConnectionLength() - labelDistance)) || 1; // fix for end point (-0 => 1) - cache.sourceBBox = cache.sourceBBox || this._V.markerSource.getBBox(); + position.distance = labelDistance; - sourceMarkerPoint = g.point(sourcePoint).move( - firstVertex || targetPoint, - cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 - ).round(); - } + // GET OFFSET: + // use absolute offset if: + // - opt.absoluteOffset is true, + // - opt.absoluteOffset is not true but there is no tangent - if (this._V.markerTarget) { + var tangent; + if (!isOffsetAbsolute) tangent = path.tangentAtT(t); - cache.targetBBox = cache.targetBBox || this._V.markerTarget.getBBox(); + var labelOffset; + if (tangent) { + labelOffset = tangent.pointOffset(labelPoint); - targetMarkerPoint = g.point(targetPoint).move( - lastVertex || sourcePoint, - cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 - ).round(); + } else { + var closestPoint = path.pointAtT(t); + var labelOffsetDiff = labelPoint.difference(closestPoint); + labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y }; } - // if there was no markup for the marker, use the connection point. - cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); - cache.targetPoint = targetMarkerPoint || targetPoint.clone(); + position.offset = labelOffset; - // make connection points public - this.sourcePoint = sourcePoint; - this.targetPoint = targetPoint; + return position; }, - _translateConnectionPoints: function(tx, ty) { - - var cache = this._markerCache; + getLabelCoordinates: function(labelPosition) { - cache.sourcePoint.offset(tx, ty); - cache.targetPoint.offset(tx, ty); - this.sourcePoint.offset(tx, ty); - this.targetPoint.offset(tx, ty); - }, + var labelDistance; + if (typeof labelPosition === 'number') labelDistance = labelPosition; + else if (typeof labelPosition.distance === 'number') labelDistance = labelPosition.distance; + else throw new Error('dia.LinkView: invalid label position distance.'); - updateLabelPositions: function() { + var isDistanceRelative = ((labelDistance > 0) && (labelDistance <= 1)); - if (!this._V.labels) return this; + var labelOffset = 0; + var labelOffsetCoordinates = { x: 0, y: 0 }; + if (labelPosition.offset) { + var positionOffset = labelPosition.offset; + if (typeof positionOffset === 'number') labelOffset = positionOffset; + if (positionOffset.x) labelOffsetCoordinates.x = positionOffset.x; + if (positionOffset.y) labelOffsetCoordinates.y = positionOffset.y; + } - // This method assumes all the label nodes are stored in the `this._labelCache` hash table - // by their indexes in the `this.get('labels')` array. This is done in the `renderLabels()` method. + var isOffsetAbsolute = ((labelOffsetCoordinates.x !== 0) || (labelOffsetCoordinates.y !== 0) || labelOffset === 0); - var labels = this.model.get('labels') || []; - if (!labels.length) return this; + var path = this.path; + var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() }; - var samples; - var connectionElement = this._V.connection.node; - var connectionLength = connectionElement.getTotalLength(); + var distance = isDistanceRelative ? (labelDistance * this.getConnectionLength()) : labelDistance; - // Firefox returns connectionLength=NaN in odd cases (for bezier curves). - // In that case we won't update labels at all. - if (Number.isNaN(connectionLength)) { - return this; - } + var point; - for (var idx = 0, n = labels.length; idx < n; idx++) { + if (isOffsetAbsolute) { + point = path.pointAtLength(distance, pathOpt); + point.offset(labelOffsetCoordinates); - var label = labels[idx]; - var position = label.position; - var isPositionObject = joint.util.isObject(position); - var labelCoordinates; + } else { + var tangent = path.tangentAtLength(distance, pathOpt); - var distance = isPositionObject ? position.distance : position; - var offset = isPositionObject ? position.offset : { x: 0, y: 0 }; + if (tangent) { + tangent.rotate(tangent.start, -90); + tangent.setLength(labelOffset); + point = tangent.end; - if (Number.isFinite(distance)) { - distance = (distance > connectionLength) ? connectionLength : distance; // sanity check - distance = (distance < 0) ? connectionLength + distance : distance; - distance = (distance > 1) ? distance : connectionLength * distance; } else { - distance = connectionLength / 2; + // fallback - the connection has zero length + point = path.start; } + } - labelCoordinates = connectionElement.getPointAtLength(distance); - - if (joint.util.isObject(offset)) { - - // Just offset the label by the x,y provided in the offset object. - labelCoordinates = g.point(labelCoordinates).offset(offset); + return point; + }, - } else if (Number.isFinite(offset)) { + getVertexIndex: function(x, y) { - if (!samples) { - samples = this._samples || this._V.connection.sample(this.options.sampleInterval); - } + var model = this.model; + var vertices = model.vertices(); - // Offset the label by the amount provided in `offset` to an either - // side of the link. - - // 1. Find the closest sample & its left and right neighbours. - var minSqDistance = Infinity; - var closestSampleIndex, sample, sqDistance; - for (var i = 0, m = samples.length; i < m; i++) { - sample = samples[i]; - sqDistance = g.line(sample, labelCoordinates).squaredLength(); - if (sqDistance < minSqDistance) { - minSqDistance = sqDistance; - closestSampleIndex = i; - } - } - var prevSample = samples[closestSampleIndex - 1]; - var nextSample = samples[closestSampleIndex + 1]; - - // 2. Offset the label on the perpendicular line between - // the current label coordinate ("at `distance`") and - // the next sample. - var angle = 0; - if (nextSample) { - angle = g.point(labelCoordinates).theta(nextSample); - } else if (prevSample) { - angle = g.point(prevSample).theta(labelCoordinates); - } - labelCoordinates = g.point(labelCoordinates).offset(offset).rotate(labelCoordinates, angle - 90); - } + var vertexLength = this.getClosestPointLength(new g.Point(x, y)); - this._labelCache[idx].attr('transform', 'translate(' + labelCoordinates.x + ', ' + labelCoordinates.y + ')'); + var idx = 0; + for (var n = vertices.length; idx < n; idx++) { + var currentVertex = vertices[idx]; + var currentVertexLength = this.getClosestPointLength(currentVertex); + if (vertexLength < currentVertexLength) break; } - return this; + return idx; }, + // Interaction. The controller part. + // --------------------------------- + + pointerdblclick: function(evt, x, y) { - updateToolsPosition: function() { + joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments); + this.notify('link:pointerdblclick', evt, x, y); + }, - if (!this._V.linkTools) return this; + pointerclick: function(evt, x, y) { - // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. - // Note that the offset is hardcoded here. The offset should be always - // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking - // this up all the time would be slow. + joint.dia.CellView.prototype.pointerclick.apply(this, arguments); + this.notify('link:pointerclick', evt, x, y); + }, - var scale = ''; - var offset = this.options.linkToolsOffset; - var connectionLength = this.getConnectionLength(); + contextmenu: function(evt, x, y) { - // Firefox returns connectionLength=NaN in odd cases (for bezier curves). - // In that case we won't update tools position at all. - if (!Number.isNaN(connectionLength)) { + joint.dia.CellView.prototype.contextmenu.apply(this, arguments); + this.notify('link:contextmenu', evt, x, y); + }, - // If the link is too short, make the tools half the size and the offset twice as low. - if (connectionLength < this.options.shortLinkLength) { - scale = 'scale(.5)'; - offset /= 2; - } + pointerdown: function(evt, x, y) { - var toolPosition = this.getPointAtLength(offset); + joint.dia.CellView.prototype.pointerdown.apply(this, arguments); + this.notify('link:pointerdown', evt, x, y); - this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); + // Backwards compatibility for the default markup + var className = evt.target.getAttribute('class'); + switch (className) { - if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { + case 'marker-vertex': + this.dragVertexStart(evt, x, y); + return; - var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; + case 'marker-vertex-remove': + case 'marker-vertex-remove-area': + this.dragVertexRemoveStart(evt, x, y); + return; - toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); - this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); - this._tool2Cache.attr('visibility', 'visible'); + case 'marker-arrowhead': + this.dragArrowheadStart(evt, x, y); + return; - } else if (this.options.doubleLinkTools) { + case 'connection': + case 'connection-wrap': + this.dragConnectionStart(evt, x, y); + return; - this._tool2Cache.attr('visibility', 'hidden'); - } + case 'marker-source': + case 'marker-target': + return; } - return this; + this.dragStart(evt, x, y); }, + pointermove: function(evt, x, y) { - updateArrowheadMarkers: function() { + // Backwards compatibility + var dragData = this._dragData; + if (dragData) this.eventData(evt, dragData); - if (!this._V.markerArrowheads) return this; + var data = this.eventData(evt); + switch (data.action) { - // getting bbox of an element with `display="none"` in IE9 ends up with access violation - if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; + case 'vertex-move': + this.dragVertex(evt, x, y); + break; - var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; - this._V.sourceArrowhead.scale(sx); - this._V.targetArrowhead.scale(sx); + case 'label-move': + this.dragLabel(evt, x, y); + break; - this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); + case 'arrowhead-move': + this.dragArrowhead(evt, x, y); + break; - return this; - }, + case 'move': + this.drag(evt, x, y); + break; + } - // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, - // it stops listening on the previous one and starts listening to the new one. - createWatcher: function(endType) { + // Backwards compatibility + if (dragData) joint.util.assign(dragData, this.eventData(evt)); - // create handler for specific end type (source|target). - var onModelChange = function(endModel, opt) { - this.onEndModelChange(endType, endModel, opt); - }; + joint.dia.CellView.prototype.pointermove.apply(this, arguments); + this.notify('link:pointermove', evt, x, y); + }, - function watchEndModel(link, end) { + pointerup: function(evt, x, y) { - end = end || {}; + // Backwards compatibility + var dragData = this._dragData; + if (dragData) { + this.eventData(evt, dragData); + this._dragData = null; + } - var endModel = null; - var previousEnd = link.previous(endType) || {}; + var data = this.eventData(evt); + switch (data.action) { - if (previousEnd.id) { - this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); - } + case 'vertex-move': + this.dragVertexEnd(evt, x, y); + break; - if (end.id) { - // If the observed model changes, it caches a new bbox and do the link update. - endModel = this.paper.getModelById(end.id); - this.listenTo(endModel, 'change', onModelChange); - } + case 'label-move': + this.dragLabelEnd(evt, x, y); + break; - onModelChange.call(this, endModel, { cacheOnly: true }); + case 'arrowhead-move': + this.dragArrowheadEnd(evt, x, y); + break; - return this; + case 'move': + this.dragEnd(evt, x, y); } - return watchEndModel; + this.notify('link:pointerup', evt, x, y); + joint.dia.CellView.prototype.pointerup.apply(this, arguments); }, - onEndModelChange: function(endType, endModel, opt) { + mouseover: function(evt) { - var doUpdate = !opt.cacheOnly; - var model = this.model; - var end = model.get(endType) || {}; + joint.dia.CellView.prototype.mouseover.apply(this, arguments); + this.notify('link:mouseover', evt); + }, - if (endModel) { + mouseout: function(evt) { - var selector = this.constructor.makeSelector(end); - var oppositeEndType = endType == 'source' ? 'target' : 'source'; - var oppositeEnd = model.get(oppositeEndType) || {}; - var oppositeSelector = oppositeEnd.id && this.constructor.makeSelector(oppositeEnd); + joint.dia.CellView.prototype.mouseout.apply(this, arguments); + this.notify('link:mouseout', evt); + }, - // Caching end models bounding boxes. - // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached - // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed - // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). - if (opt.handleBy === this.cid && selector == oppositeSelector) { + mouseenter: function(evt) { - // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the - // second time now. There is no need to calculate bbox and find magnet element again. - // It was calculated already for opposite link end. - this[endType + 'BBox'] = this[oppositeEndType + 'BBox']; - this[endType + 'View'] = this[oppositeEndType + 'View']; - this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; + joint.dia.CellView.prototype.mouseenter.apply(this, arguments); + this.notify('link:mouseenter', evt); + }, - } else if (opt.translateBy) { - // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. - // If `opt.translateBy` is an ID of the element that was originally translated. This allows us - // to just offset the cached bounding box by the translation instead of calculating the bounding - // box from scratch on every translate. + mouseleave: function(evt) { - var bbox = this[endType + 'BBox']; - bbox.x += opt.tx; - bbox.y += opt.ty; + joint.dia.CellView.prototype.mouseleave.apply(this, arguments); + this.notify('link:mouseleave', evt); + }, - } else { - // The slowest path, source/target could have been rotated or resized or any attribute - // that affects the bounding box of the view might have been changed. + mousewheel: function(evt, x, y, delta) { - var view = this.paper.findViewByModel(end.id); - var magnetElement = view.el.querySelector(selector); + joint.dia.CellView.prototype.mousewheel.apply(this, arguments); + this.notify('link:mousewheel', evt, x, y, delta); + }, - this[endType + 'BBox'] = view.getStrokeBBox(magnetElement); - this[endType + 'View'] = view; - this[endType + 'Magnet'] = magnetElement; - } + onevent: function(evt, eventName, x, y) { - if (opt.handleBy === this.cid && opt.translateBy && - model.isEmbeddedIn(endModel) && - !joint.util.isEmpty(model.get('vertices'))) { - // Loop link whose element was translated and that has vertices (that need to be translated with - // the parent in which my element is embedded). - // If the link is embedded, has a loop and vertices and the end model - // has been translated, do not update yet. There are vertices still to be updated (change:vertices - // event will come in the next turn). - doUpdate = false; + // Backwards compatibility + var linkTool = V(evt.target).findParentByClass('link-tool', this.el); + if (linkTool) { + // No further action to be executed + evt.stopPropagation(); + + // Allow `interactive.useLinkTools=false` + if (this.can('useLinkTools')) { + if (eventName === 'remove') { + // Built-in remove event + this.model.remove({ ui: true }); + + } else { + // link:options and other custom events inside the link tools + this.notify(eventName, evt, x, y); + } } - if (!this.updatePostponed && oppositeEnd.id) { - // The update was not postponed (that can happen e.g. on the first change event) and the opposite - // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if - // we're reacting on change:source event, the oppositeEnd is the target model). + } else { + joint.dia.CellView.prototype.onevent.apply(this, arguments); + } + }, - var oppositeEndModel = this.paper.getModelById(oppositeEnd.id); + onlabel: function(evt, x, y) { - // Passing `handleBy` flag via event option. - // Note that if we are listening to the same model for event 'change' twice. - // The same event will be handled by this method also twice. - if (end.id === oppositeEnd.id) { - // We're dealing with a loop link. Tell the handlers in the next turn that they should update - // the link instead of me. (We know for sure there will be a next turn because - // loop links react on at least two events: change on the source model followed by a change on - // the target model). - opt.handleBy = this.cid; - } + this.dragLabelStart(evt, x, y); - if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { + var stopPropagation = this.eventData(evt).stopPropagation; + if (stopPropagation) evt.stopPropagation(); + }, - // Here are two options: - // - Source and target are connected to the same model (not necessarily the same port). - // - Both end models are translated by the same ancestor. We know that opposite end - // model will be translated in the next turn as well. - // In both situations there will be more changes on the model that trigger an - // update. So there is no need to update the linkView yet. - this.updatePostponed = true; - doUpdate = false; - } - } + // Drag Start Handlers - } else { + dragConnectionStart: function(evt, x, y) { - // the link end is a point ~ rect 1x1 - this[endType + 'BBox'] = g.rect(end.x || 0, end.y || 0, 1, 1); - this[endType + 'View'] = this[endType + 'Magnet'] = null; - } + if (!this.can('vertexAdd')) return; - if (doUpdate) { - opt.updateConnectionOnly = true; - this.update(model, null, opt); - } + // Store the index at which the new vertex has just been placed. + // We'll be update the very same vertex position in `pointermove()`. + var vertexIdx = this.addVertex({ x: x, y: y }, { ui: true }); + this.eventData(evt, { + action: 'vertex-move', + vertexIdx: vertexIdx + }); }, - _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { + dragLabelStart: function(evt, x, y) { - // Make the markers "point" to their sticky points being auto-oriented towards - // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. - var route = joint.util.toArray(this.route); - if (sourceArrow) { - sourceArrow.translateAndAutoOrient( - this.sourcePoint, - route[0] || this.targetPoint, - this.paper.viewport - ); + if (!this.can('labelMove')) { + // Backwards compatibility: + // If labels can't be dragged no default action is triggered. + this.eventData(evt, { stopPropagation: true }); + return; } - if (targetArrow) { - targetArrow.translateAndAutoOrient( - this.targetPoint, - route[route.length - 1] || this.sourcePoint, - this.paper.viewport - ); - } - }, + var labelNode = evt.currentTarget; + var labelIdx = parseInt(labelNode.getAttribute('label-idx'), 10); - removeVertex: function(idx) { + var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs(); + var labelPositionArgs = this._getLabelPositionArgs(labelIdx); + var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); + + this.eventData(evt, { + action: 'label-move', + labelIdx: labelIdx, + positionArgs: positionArgs, + stopPropagation: true + }); - var vertices = joint.util.assign([], this.model.get('vertices')); + this.paper.delegateDragEvents(this, evt.data); + }, - if (vertices && vertices.length) { + dragVertexStart: function(evt, x, y) { - vertices.splice(idx, 1); - this.model.set('vertices', vertices, { ui: true }); - } + if (!this.can('vertexMove')) return; - return this; + var vertexNode = evt.target; + var vertexIdx = parseInt(vertexNode.getAttribute('idx'), 10); + this.eventData(evt, { + action: 'vertex-move', + vertexIdx: vertexIdx + }); }, - // This method ads a new vertex to the `vertices` array of `.connection`. This method - // uses a heuristic to find the index at which the new `vertex` should be placed at assuming - // the new vertex is somewhere on the path. - addVertex: function(vertex) { + dragVertexRemoveStart: function(evt, x, y) { - // As it is very hard to find a correct index of the newly created vertex, - // a little heuristics is taking place here. - // The heuristics checks if length of the newly created - // path is lot more than length of the old path. If this is the case, - // new vertex was probably put into a wrong index. - // Try to put it into another index and repeat the heuristics again. + if (!this.can('vertexRemove')) return; - var vertices = (this.model.get('vertices') || []).slice(); - // Store the original vertices for a later revert if needed. - var originalVertices = vertices.slice(); + var removeNode = evt.target; + var vertexIdx = parseInt(removeNode.getAttribute('idx'), 10); + this.model.removeVertex(vertexIdx); + }, - // A `` element used to compute the length of the path during heuristics. - var path = this._V.connection.node.cloneNode(false); + dragArrowheadStart: function(evt, x, y) { - // Length of the original path. - var originalPathLength = path.getTotalLength(); - // Current path length. - var pathLength; - // Tolerance determines the highest possible difference between the length - // of the old and new path. The number has been chosen heuristically. - var pathLengthTolerance = 20; - // Total number of vertices including source and target points. - var idx = vertices.length + 1; + if (!this.can('arrowheadMove')) return; - // Loop through all possible indexes and check if the difference between - // path lengths changes significantly. If not, the found index is - // most probably the right one. - while (idx--) { + var arrowheadNode = evt.target; + var arrowheadType = arrowheadNode.getAttribute('end'); + var data = this.startArrowheadMove(arrowheadType, { ignoreBackwardsCompatibility: true }); - vertices.splice(idx, 0, vertex); - V(path).attr('d', this.getPathData(this.findRoute(vertices))); + this.eventData(evt, data); + }, - pathLength = path.getTotalLength(); + dragStart: function(evt, x, y) { - // Check if the path lengths changed significantly. - if (pathLength - originalPathLength > pathLengthTolerance) { + if (!this.can('linkMove')) return; - // Revert vertices to the original array. The path length has changed too much - // so that the index was not found yet. - vertices = originalVertices.slice(); + this.eventData(evt, { + action: 'move', + dx: x, + dy: y + }); + }, - } else { + // Drag Handlers - break; - } - } + dragLabel: function(evt, x, y) { - if (idx === -1) { - // If no suitable index was found for such a vertex, make the vertex the first one. - idx = 0; - vertices.splice(idx, 0, vertex); - } + var data = this.eventData(evt); + var label = { position: this.getLabelPosition(x, y, data.positionArgs) }; + this.model.label(data.labelIdx, label); + }, - this.model.set('vertices', vertices, { ui: true }); + dragVertex: function(evt, x, y) { - return idx; + var data = this.eventData(evt); + this.model.vertex(data.vertexIdx, { x: x, y: y }, { ui: true }); }, - // Send a token (an SVG element, usually a circle) along the connection path. - // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)` - // `opt.duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. - // `opt.directon` is optional and it determines whether the token goes from source to target or other way round (`reverse`) - // `callback` is optional and is a function to be called once the token reaches the target. - sendToken: function(token, opt, callback) { + dragArrowhead: function(evt, x, y) { - function onAnimationEnd(vToken, callback) { - return function() { - vToken.remove(); - if (typeof callback === 'function') { - callback(); - } - }; - } + var data = this.eventData(evt); + + if (this.paper.options.snapLinks) { + + this._snapArrowhead(x, y, data); - var duration, isReversed; - if (joint.util.isObject(opt)) { - duration = opt.duration; - isReversed = (opt.direction === 'reverse'); } else { - // Backwards compatibility - duration = opt; - isReversed = false; + // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. + // It holds the element when a touchstart triggered. + var target = (evt.type === 'mousemove') + ? evt.target + : document.elementFromPoint(evt.clientX, evt.clientY); + + this._connectArrowhead(target, x, y, data); } + }, - duration = duration || 1000; + drag: function(evt, x, y) { - var animationAttributes = { - dur: duration + 'ms', - repeatCount: 1, - calcMode: 'linear', - fill: 'freeze' - }; + var data = this.eventData(evt); + this.model.translate(x - data.dx, y - data.dy, { ui: true }); + this.eventData(evt, { + dx: x, + dy: y + }); + }, - if (isReversed) { - animationAttributes.keyPoints = '1;0'; - animationAttributes.keyTimes = '0;1'; + // Drag End Handlers + + dragLabelEnd: function() { + // noop + }, + + dragVertexEnd: function() { + // noop + }, + + dragArrowheadEnd: function(evt, x, y) { + + var data = this.eventData(evt); + var paper = this.paper; + + if (paper.options.snapLinks) { + this._snapArrowheadEnd(data); + } else { + this._connectArrowheadEnd(data, x, y); } - var vToken = V(token); - var vPath = this._V.connection; + if (!paper.linkAllowed(this)) { + // If the changed link is not allowed, revert to its previous state. + this._disallow(data); + } else { + this._finishEmbedding(data); + this._notifyConnectEvent(data, evt); + } - vToken - .appendTo(this.paper.viewport) - .animateAlongPath(animationAttributes, vPath); + this._afterArrowheadMove(data); + + // mouseleave event is not triggered due to changing pointer-events to `none`. + if (!this.vel.contains(evt.target)) { + this.mouseleave(evt); + } + }, - setTimeout(onAnimationEnd(vToken, callback), duration); + dragEnd: function() { + // noop }, - findRoute: function(oldVertices) { + _disallow: function(data) { - var namespace = joint.routers; - var router = this.model.get('router'); - var defaultRouter = this.paper.options.defaultRouter; + switch (data.whenNotAllowed) { - if (!router) { + case 'remove': + this.model.remove({ ui: true }); + break; - if (this.model.get('manhattan')) { - // backwards compability - router = { name: 'orthogonal' }; - } else if (defaultRouter) { - router = defaultRouter; - } else { - return oldVertices; - } + case 'revert': + default: + this.model.set(data.arrowhead, data.initialEnd, { ui: true }); + break; } + }, - var args = router.args || {}; - var routerFn = joint.util.isFunction(router) ? router : namespace[router.name]; + _finishEmbedding: function(data) { - if (!joint.util.isFunction(routerFn)) { - throw new Error('unknown router: "' + router.name + '"'); + // Reparent the link if embedding is enabled + if (this.paper.options.embeddingMode && this.model.reparent()) { + // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). + data.z = null; } + }, - var newVertices = routerFn.call(this, oldVertices || [], args, this); + _notifyConnectEvent: function(data, evt) { - return newVertices; + var arrowhead = data.arrowhead; + var initialEnd = data.initialEnd; + var currentEnd = this.model.prop(arrowhead); + var endChanged = currentEnd && !joint.dia.Link.endsEqual(initialEnd, currentEnd); + if (endChanged) { + var paper = this.paper; + if (initialEnd.id) { + this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), data.initialMagnet, arrowhead); + } + if (currentEnd.id) { + this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), data.magnetUnderPointer, arrowhead); + } + } }, - // Return the `d` attribute value of the `` element representing the link - // between `source` and `target`. - getPathData: function(vertices) { + _snapArrowhead: function(x, y, data) { - var namespace = joint.connectors; - var connector = this.model.get('connector'); - var defaultConnector = this.paper.options.defaultConnector; + // checking view in close area of the pointer - if (!connector) { + var r = this.paper.options.snapLinks.radius || 50; + var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); - // backwards compability - if (this.model.get('smooth')) { - connector = { name: 'smooth' }; - } else { - connector = defaultConnector || {}; - } + if (data.closestView) { + data.closestView.unhighlight(data.closestMagnet, { + connecting: true, + snapping: true + }); } + data.closestView = data.closestMagnet = null; - var connectorFn = joint.util.isFunction(connector) ? connector : namespace[connector.name]; - var args = connector.args || {}; + var distance; + var minDistance = Number.MAX_VALUE; + var pointer = g.point(x, y); + var paper = this.paper; - if (!joint.util.isFunction(connectorFn)) { - throw new Error('unknown connector: "' + connector.name + '"'); - } + viewsInArea.forEach(function(view) { - var pathData = connectorFn.call( - this, - this._markerCache.sourcePoint, // Note that the value is translated by the size - this._markerCache.targetPoint, // of the marker. (We'r not using this.sourcePoint) - vertices || (this.model.get('vertices') || {}), - args, // options - this - ); + // skip connecting to the element in case '.': { magnet: false } attribute present + if (view.el.getAttribute('magnet') !== 'false') { - return pathData; - }, + // find distance from the center of the model to pointer coordinates + distance = view.model.getBBox().center().distance(pointer); - // Find a point that is the start of the connection. - // If `selectorOrPoint` is a point, then we're done and that point is the start of the connection. - // If the `selectorOrPoint` is an element however, we need to know a reference point (or element) - // that the link leads to in order to determine the start of the connection on the original element. - getConnectionPoint: function(end, selectorOrPoint, referenceSelectorOrPoint) { + // the connection is looked up in a circle area by `distance < r` + if (distance < r && distance < minDistance) { - var spot; + if (paper.options.validateConnection.apply( + paper, data.validateConnectionArgs(view, null) + )) { + minDistance = distance; + data.closestView = view; + data.closestMagnet = view.el; + } + } + } - // If the `selectorOrPoint` (or `referenceSelectorOrPoint`) is `undefined`, the `source`/`target` of the link model is `undefined`. - // We want to allow this however so that one can create links such as `var link = new joint.dia.Link` and - // set the `source`/`target` later. - joint.util.isEmpty(selectorOrPoint) && (selectorOrPoint = { x: 0, y: 0 }); - joint.util.isEmpty(referenceSelectorOrPoint) && (referenceSelectorOrPoint = { x: 0, y: 0 }); + view.$('[magnet]').each(function(index, magnet) { - if (!selectorOrPoint.id) { + var bbox = view.getNodeBBox(magnet); - // If the source is a point, we don't need a reference point to find the sticky point of connection. - spot = g.Point(selectorOrPoint); + distance = pointer.distance({ + x: bbox.x + bbox.width / 2, + y: bbox.y + bbox.height / 2 + }); - } else { + if (distance < r && distance < minDistance) { - // If the source is an element, we need to find a point on the element boundary that is closest - // to the reference point (or reference element). - // Get the bounding box of the spot relative to the paper viewport. This is necessary - // in order to follow paper viewport transformations (scale/rotate). - // `_sourceBbox` (`_targetBbox`) comes from `_sourceBboxUpdate` (`_sourceBboxUpdate`) - // method, it exists since first render and are automatically updated - var spotBBox = g.Rect(end === 'source' ? this.sourceBBox : this.targetBBox); + if (paper.options.validateConnection.apply( + paper, data.validateConnectionArgs(view, magnet) + )) { + minDistance = distance; + data.closestView = view; + data.closestMagnet = magnet; + } + } - var reference; + }.bind(this)); - if (!referenceSelectorOrPoint.id) { + }, this); - // Reference was passed as a point, therefore, we're ready to find the sticky point of connection on the source element. - reference = g.Point(referenceSelectorOrPoint); + var end; + var closestView = data.closestView; + var closestMagnet = data.closestMagnet; + var endType = data.arrowhead; + if (closestView) { + closestView.highlight(closestMagnet, { + connecting: true, + snapping: true + }); + end = closestView.getLinkEnd(closestMagnet, x, y, this.model, endType); + } else { + end = { x: x, y: y }; + } - } else { + this.model.set(endType, end || { x: x, y: y }, { ui: true }); + }, - // Reference was passed as an element, therefore we need to find a point on the reference - // element boundary closest to the source element. - // Get the bounding box of the spot relative to the paper viewport. This is necessary - // in order to follow paper viewport transformations (scale/rotate). - var referenceBBox = g.Rect(end === 'source' ? this.targetBBox : this.sourceBBox); + _snapArrowheadEnd: function(data) { - reference = referenceBBox.intersectionWithLineFromCenterToPoint(spotBBox.center()); - reference = reference || referenceBBox.center(); - } + // Finish off link snapping. + // Everything except view unhighlighting was already done on pointermove. + var closestView = data.closestView; + var closestMagnet = data.closestMagnet; + if (closestView && closestMagnet) { - var paperOptions = this.paper.options; - // If `perpendicularLinks` flag is set on the paper and there are vertices - // on the link, then try to find a connection point that makes the link perpendicular - // even though the link won't point to the center of the targeted object. - if (paperOptions.perpendicularLinks || this.options.perpendicular) { + closestView.unhighlight(closestMagnet, { connecting: true, snapping: true }); + data.magnetUnderPointer = closestView.findMagnet(closestMagnet); + } - var nearestSide; - var spotOrigin = spotBBox.origin(); - var spotCorner = spotBBox.corner(); + data.closestView = data.closestMagnet = null; + }, - if (spotOrigin.y <= reference.y && reference.y <= spotCorner.y) { + _connectArrowhead: function(target, x, y, data) { - nearestSide = spotBBox.sideNearestToPoint(reference); - switch (nearestSide) { - case 'left': - spot = g.Point(spotOrigin.x, reference.y); - break; - case 'right': - spot = g.Point(spotCorner.x, reference.y); - break; - default: - spot = spotBBox.center(); - break; - } + // checking views right under the pointer - } else if (spotOrigin.x <= reference.x && reference.x <= spotCorner.x) { + if (data.eventTarget !== target) { + // Unhighlight the previous view under pointer if there was one. + if (data.magnetUnderPointer) { + data.viewUnderPointer.unhighlight(data.magnetUnderPointer, { + connecting: true + }); + } - nearestSide = spotBBox.sideNearestToPoint(reference); - switch (nearestSide) { - case 'top': - spot = g.Point(reference.x, spotOrigin.y); - break; - case 'bottom': - spot = g.Point(reference.x, spotCorner.y); - break; - default: - spot = spotBBox.center(); - break; + data.viewUnderPointer = this.paper.findView(target); + if (data.viewUnderPointer) { + // If we found a view that is under the pointer, we need to find the closest + // magnet based on the real target element of the event. + data.magnetUnderPointer = data.viewUnderPointer.findMagnet(target); + + if (data.magnetUnderPointer && this.paper.options.validateConnection.apply( + this.paper, + data.validateConnectionArgs(data.viewUnderPointer, data.magnetUnderPointer) + )) { + // If there was no magnet found, do not highlight anything and assume there + // is no view under pointer we're interested in reconnecting to. + // This can only happen if the overall element has the attribute `'.': { magnet: false }`. + if (data.magnetUnderPointer) { + data.viewUnderPointer.highlight(data.magnetUnderPointer, { + connecting: true + }); } - } else { - - // If there is no intersection horizontally or vertically with the object bounding box, - // then we fall back to the regular situation finding straight line (not perpendicular) - // between the object and the reference point. - spot = spotBBox.intersectionWithLineFromCenterToPoint(reference); - spot = spot || spotBBox.center(); + // This type of connection is not valid. Disregard this magnet. + data.magnetUnderPointer = null; } - - } else if (paperOptions.linkConnectionPoint) { - - var view = (end === 'target') ? this.targetView : this.sourceView; - var magnet = (end === 'target') ? this.targetMagnet : this.sourceMagnet; - - spot = paperOptions.linkConnectionPoint(this, view, magnet, reference, end); - } else { - - spot = spotBBox.intersectionWithLineFromCenterToPoint(reference); - spot = spot || spotBBox.center(); + // Make sure we'll unset previous magnet. + data.magnetUnderPointer = null; } } - return spot; - }, + data.eventTarget = target; - // Public API - // ---------- + this.model.set(data.arrowhead, { x: x, y: y }, { ui: true }); + }, - getConnectionLength: function() { + _connectArrowheadEnd: function(data, x, y) { - return this._V.connection.node.getTotalLength(); - }, + var view = data.viewUnderPointer; + var magnet = data.magnetUnderPointer; + if (!magnet || !view) return; - getPointAtLength: function(length) { + view.unhighlight(magnet, { connecting: true }); - return this._V.connection.node.getPointAtLength(length); + var endType = data.arrowhead; + var end = view.getLinkEnd(magnet, x, y, this.model, endType); + this.model.set(endType, end, { ui: true }); }, - // Interaction. The controller part. - // --------------------------------- - - _beforeArrowheadMove: function() { + _beforeArrowheadMove: function(data) { - this._z = this.model.get('z'); + data.z = this.model.get('z'); this.model.toFront(); // Let the pointer propagate throught the link view elements so that @@ -10447,15 +16749,15 @@ joint.dia.LinkView = joint.dia.CellView.extend({ this.el.style.pointerEvents = 'none'; if (this.paper.options.markAvailable) { - this._markAvailableMagnets(); + this._markAvailableMagnets(data); } }, - _afterArrowheadMove: function() { + _afterArrowheadMove: function(data) { - if (this._z !== null) { - this.model.set('z', this._z, { ui: true }); - this._z = null; + if (data.z !== null) { + this.model.set('z', data.z, { ui: true }); + data.z = null; } // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation. @@ -10464,7 +16766,7 @@ joint.dia.LinkView = joint.dia.CellView.extend({ this.el.style.pointerEvents = 'visiblePainted'; if (this.paper.options.markAvailable) { - this._unmarkAvailableMagnets(); + this._unmarkAvailableMagnets(data); } }, @@ -10504,17 +16806,17 @@ joint.dia.LinkView = joint.dia.CellView.extend({ return validateConnectionArgs; }, - _markAvailableMagnets: function() { + _markAvailableMagnets: function(data) { function isMagnetAvailable(view, magnet) { var paper = view.paper; var validate = paper.options.validateConnection; - return validate.apply(paper, this._validateConnectionArgs(view, magnet)); + return validate.apply(paper, this.validateConnectionArgs(view, magnet)); } var paper = this.paper; var elements = paper.model.getElements(); - this._marked = {}; + data.marked = {}; for (var i = 0, n = elements.length; i < n; i++) { var view = elements[i].findView(paper); @@ -10525,465 +16827,81 @@ joint.dia.LinkView = joint.dia.CellView.extend({ var magnets = Array.prototype.slice.call(view.el.querySelectorAll('[magnet]')); if (view.el.getAttribute('magnet') !== 'false') { - // Element wrapping group is also a magnet - magnets.push(view.el); - } - - var availableMagnets = magnets.filter(isMagnetAvailable.bind(this, view)); - - if (availableMagnets.length > 0) { - // highlight all available magnets - for (var j = 0, m = availableMagnets.length; j < m; j++) { - view.highlight(availableMagnets[j], { magnetAvailability: true }); - } - // highlight the entire view - view.highlight(null, { elementAvailability: true }); - - this._marked[view.model.id] = availableMagnets; - } - } - }, - - _unmarkAvailableMagnets: function() { - - var markedKeys = Object.keys(this._marked); - var id; - var markedMagnets; - - for (var i = 0, n = markedKeys.length; i < n; i++) { - id = markedKeys[i]; - markedMagnets = this._marked[id]; - - var view = this.paper.findViewByModel(id); - if (view) { - for (var j = 0, m = markedMagnets.length; j < m; j++) { - view.unhighlight(markedMagnets[j], { magnetAvailability: true }); - } - view.unhighlight(null, { elementAvailability: true }); - } - } - - this._marked = null; - }, - - startArrowheadMove: function(end, opt) { - - opt = joint.util.defaults(opt || {}, { whenNotAllowed: 'revert' }); - // Allow to delegate events from an another view to this linkView in order to trigger arrowhead - // move without need to click on the actual arrowhead dom element. - this._action = 'arrowhead-move'; - this._whenNotAllowed = opt.whenNotAllowed; - this._arrowhead = end; - this._initialMagnet = this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null); - this._initialEnd = joint.util.assign({}, this.model.get(end)) || { x: 0, y: 0 }; - this._validateConnectionArgs = this._createValidateConnectionArgs(this._arrowhead); - this._beforeArrowheadMove(); - }, - - pointerdown: function(evt, x, y) { - - joint.dia.CellView.prototype.pointerdown.apply(this, arguments); - this.notify('link:pointerdown', evt, x, y); - - this._dx = x; - this._dy = y; - - // if are simulating pointerdown on a link during a magnet click, skip link interactions - if (evt.target.getAttribute('magnet') != null) return; - - var className = joint.util.removeClassNamePrefix(evt.target.getAttribute('class')); - var parentClassName = joint.util.removeClassNamePrefix(evt.target.parentNode.getAttribute('class')); - var labelNode; - if (parentClassName === 'label') { - className = parentClassName; - labelNode = evt.target.parentNode; - } else { - labelNode = evt.target; - } - - switch (className) { - - case 'marker-vertex': - if (this.can('vertexMove')) { - this._action = 'vertex-move'; - this._vertexIdx = evt.target.getAttribute('idx'); - } - break; - - case 'marker-vertex-remove': - case 'marker-vertex-remove-area': - if (this.can('vertexRemove')) { - this.removeVertex(evt.target.getAttribute('idx')); - } - break; - - case 'marker-arrowhead': - if (this.can('arrowheadMove')) { - this.startArrowheadMove(evt.target.getAttribute('end')); - } - break; - - case 'label': - if (this.can('labelMove')) { - this._action = 'label-move'; - this._labelIdx = parseInt(V(labelNode).attr('label-idx'), 10); - // Precalculate samples so that we don't have to do that - // over and over again while dragging the label. - this._samples = this._V.connection.sample(1); - this._linkLength = this._V.connection.node.getTotalLength(); - } - break; - - default: - - if (this.can('vertexAdd')) { - - // Store the index at which the new vertex has just been placed. - // We'll be update the very same vertex position in `pointermove()`. - this._vertexIdx = this.addVertex({ x: x, y: y }); - this._action = 'vertex-move'; - } - } - }, - - pointermove: function(evt, x, y) { - - switch (this._action) { - - case 'vertex-move': - - var vertices = joint.util.assign([], this.model.get('vertices')); - vertices[this._vertexIdx] = { x: x, y: y }; - this.model.set('vertices', vertices, { ui: true }); - break; - - case 'label-move': - - var dragPoint = { x: x, y: y }; - var samples = this._samples; - var minSqDistance = Infinity; - var closestSample; - var closestSampleIndex; - var p; - var sqDistance; - for (var i = 0, n = samples.length; i < n; i++) { - p = samples[i]; - sqDistance = g.line(p, dragPoint).squaredLength(); - if (sqDistance < minSqDistance) { - minSqDistance = sqDistance; - closestSample = p; - closestSampleIndex = i; - } - } - var prevSample = samples[closestSampleIndex - 1]; - var nextSample = samples[closestSampleIndex + 1]; - var offset = 0; - if (prevSample && nextSample) { - offset = g.line(prevSample, nextSample).pointOffset(dragPoint); - } else if (prevSample) { - offset = g.line(prevSample, closestSample).pointOffset(dragPoint); - } else if (nextSample) { - offset = g.line(closestSample, nextSample).pointOffset(dragPoint); - } - - this.model.label(this._labelIdx, { - position: { - distance: closestSample.distance / this._linkLength, - offset: offset - } - }); - break; - - case 'arrowhead-move': - - if (this.paper.options.snapLinks) { - - // checking view in close area of the pointer - - var r = this.paper.options.snapLinks.radius || 50; - var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); - - if (this._closestView) { - this._closestView.unhighlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - } - this._closestView = this._closestEnd = null; - - var distance; - var minDistance = Number.MAX_VALUE; - var pointer = g.point(x, y); - - viewsInArea.forEach(function(view) { - - // skip connecting to the element in case '.': { magnet: false } attribute present - if (view.el.getAttribute('magnet') !== 'false') { - - // find distance from the center of the model to pointer coordinates - distance = view.model.getBBox().center().distance(pointer); - - // the connection is looked up in a circle area by `distance < r` - if (distance < r && distance < minDistance) { - - if (this.paper.options.validateConnection.apply( - this.paper, this._validateConnectionArgs(view, null) - )) { - minDistance = distance; - this._closestView = view; - this._closestEnd = { id: view.model.id }; - } - } - } - - view.$('[magnet]').each(function(index, magnet) { - - var bbox = V(magnet).getBBox({ target: this.paper.viewport }); - - distance = pointer.distance({ - x: bbox.x + bbox.width / 2, - y: bbox.y + bbox.height / 2 - }); - - if (distance < r && distance < minDistance) { - - if (this.paper.options.validateConnection.apply( - this.paper, this._validateConnectionArgs(view, magnet) - )) { - minDistance = distance; - this._closestView = view; - this._closestEnd = { - id: view.model.id, - selector: view.getSelector(magnet), - port: magnet.getAttribute('port') - }; - } - } - - }.bind(this)); - - }, this); - - if (this._closestView) { - this._closestView.highlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - } - - this.model.set(this._arrowhead, this._closestEnd || { x: x, y: y }, { ui: true }); - - } else { - - // checking views right under the pointer - - // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. - // It holds the element when a touchstart triggered. - var target = (evt.type === 'mousemove') - ? evt.target - : document.elementFromPoint(evt.clientX, evt.clientY); - - if (this._eventTarget !== target) { - // Unhighlight the previous view under pointer if there was one. - if (this._magnetUnderPointer) { - this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { - connecting: true - }); - } - - this._viewUnderPointer = this.paper.findView(target); - if (this._viewUnderPointer) { - // If we found a view that is under the pointer, we need to find the closest - // magnet based on the real target element of the event. - this._magnetUnderPointer = this._viewUnderPointer.findMagnet(target); - - if (this._magnetUnderPointer && this.paper.options.validateConnection.apply( - this.paper, - this._validateConnectionArgs(this._viewUnderPointer, this._magnetUnderPointer) - )) { - // If there was no magnet found, do not highlight anything and assume there - // is no view under pointer we're interested in reconnecting to. - // This can only happen if the overall element has the attribute `'.': { magnet: false }`. - if (this._magnetUnderPointer) { - this._viewUnderPointer.highlight(this._magnetUnderPointer, { - connecting: true - }); - } - } else { - // This type of connection is not valid. Disregard this magnet. - this._magnetUnderPointer = null; - } - } else { - // Make sure we'll unset previous magnet. - this._magnetUnderPointer = null; - } - } - - this._eventTarget = target; - - this.model.set(this._arrowhead, { x: x, y: y }, { ui: true }); - } - break; - } - - this._dx = x; - this._dy = y; - - joint.dia.CellView.prototype.pointermove.apply(this, arguments); - this.notify('link:pointermove', evt, x, y); - }, - - pointerup: function(evt, x, y) { - - if (this._action === 'label-move') { - - this._samples = null; - - } else if (this._action === 'arrowhead-move') { - - var model = this.model; - var paper = this.paper; - var paperOptions = paper.options; - var arrowhead = this._arrowhead; - var initialEnd = this._initialEnd; - var magnetUnderPointer; - - if (paperOptions.snapLinks) { - - // Finish off link snapping. - // Everything except view unhighlighting was already done on pointermove. - if (this._closestView) { - this._closestView.unhighlight(this._closestEnd.selector, { - connecting: true, - snapping: true - }); - - magnetUnderPointer = this._closestView.findMagnet(this._closestEnd.selector); - } - - this._closestView = this._closestEnd = null; - - } else { - - var viewUnderPointer = this._viewUnderPointer; - magnetUnderPointer = this._magnetUnderPointer; - - this._viewUnderPointer = null; - this._magnetUnderPointer = null; - - if (magnetUnderPointer) { - - viewUnderPointer.unhighlight(magnetUnderPointer, { connecting: true }); - // Find a unique `selector` of the element under pointer that is a magnet. If the - // `this._magnetUnderPointer` is the root element of the `this._viewUnderPointer` itself, - // the returned `selector` will be `undefined`. That means we can directly pass it to the - // `source`/`target` attribute of the link model below. - var selector = viewUnderPointer.getSelector(magnetUnderPointer); - var port = magnetUnderPointer.getAttribute('port'); - var arrowheadValue = { id: viewUnderPointer.model.id }; - if (port != null) arrowheadValue.port = port; - if (selector != null) arrowheadValue.selector = selector; - model.set(arrowhead, arrowheadValue, { ui: true }); - } - } - - // If the changed link is not allowed, revert to its previous state. - if (!paper.linkAllowed(this)) { - - switch (this._whenNotAllowed) { - - case 'remove': - model.remove({ ui: true }); - break; - - case 'revert': - default: - model.set(arrowhead, initialEnd, { ui: true }); - break; - } + // Element wrapping group is also a magnet + magnets.push(view.el); + } - } else { + var availableMagnets = magnets.filter(isMagnetAvailable.bind(data, view)); - // Reparent the link if embedding is enabled - if (paperOptions.embeddingMode && model.reparent()) { - // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). - this._z = null; + if (availableMagnets.length > 0) { + // highlight all available magnets + for (var j = 0, m = availableMagnets.length; j < m; j++) { + view.highlight(availableMagnets[j], { magnetAvailability: true }); } + // highlight the entire view + view.highlight(null, { elementAvailability: true }); - var currentEnd = model.prop(arrowhead); - var endChanged = currentEnd && !joint.dia.Link.endsEqual(initialEnd, currentEnd); - if (endChanged) { - - if (initialEnd.id) { - this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), this._initialMagnet, arrowhead); - } - if (currentEnd.id) { - this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), magnetUnderPointer, arrowhead); - } - } + data.marked[view.model.id] = availableMagnets; } - - this._afterArrowheadMove(); } - - this._action = null; - this._whenNotAllowed = null; - this._initialMagnet = null; - this._initialEnd = null; - this._validateConnectionArgs = null; - this._eventTarget = null; - - this.notify('link:pointerup', evt, x, y); - joint.dia.CellView.prototype.pointerup.apply(this, arguments); }, - mouseenter: function(evt) { + _unmarkAvailableMagnets: function(data) { - joint.dia.CellView.prototype.mouseenter.apply(this, arguments); - this.notify('link:mouseenter', evt); - }, + var markedKeys = Object.keys(data.marked); + var id; + var markedMagnets; - mouseleave: function(evt) { + for (var i = 0, n = markedKeys.length; i < n; i++) { + id = markedKeys[i]; + markedMagnets = data.marked[id]; - joint.dia.CellView.prototype.mouseleave.apply(this, arguments); - this.notify('link:mouseleave', evt); + var view = this.paper.findViewByModel(id); + if (view) { + for (var j = 0, m = markedMagnets.length; j < m; j++) { + view.unhighlight(markedMagnets[j], { magnetAvailability: true }); + } + view.unhighlight(null, { elementAvailability: true }); + } + } + + data.marked = null; }, - event: function(evt, eventName, x, y) { + startArrowheadMove: function(end, opt) { - // Backwards compatibility - var linkTool = V(evt.target).findParentByClass('link-tool', this.el); - if (linkTool) { - // No further action to be executed - evt.stopPropagation(); - // Allow `interactive.useLinkTools=false` - if (this.can('useLinkTools')) { - if (eventName === 'remove') { - // Built-in remove event - this.model.remove({ ui: true }); - } else { - // link:options and other custom events inside the link tools - this.notify(eventName, evt, x, y); - } - } + opt || (opt = {}); - } else { + // Allow to delegate events from an another view to this linkView in order to trigger arrowhead + // move without need to click on the actual arrowhead dom element. + var data = { + action: 'arrowhead-move', + arrowhead: end, + whenNotAllowed: opt.whenNotAllowed || 'revert', + initialMagnet: this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null), + initialEnd: joint.util.clone(this.model.get(end)), + validateConnectionArgs: this._createValidateConnectionArgs(end) + }; - joint.dia.CellView.prototype.event.apply(this, arguments); + this._beforeArrowheadMove(data); + + if (opt.ignoreBackwardsCompatibility !== true) { + this._dragData = data; } - } + return data; + } }, { makeSelector: function(end) { - var selector = '[model-id="' + end.id + '"]'; + var selector = ''; // `port` has a higher precendence over `selector`. This is because the selector to the magnet // might change while the name of the port can stay the same. if (end.port) { - selector += ' [port="' + end.port + '"]'; + selector += '[port="' + end.port + '"]'; } else if (end.selector) { - selector += ' ' + end.selector; + selector += end.selector; } return selector; @@ -10992,6 +16910,40 @@ joint.dia.LinkView = joint.dia.CellView.extend({ }); +Object.defineProperty(joint.dia.LinkView.prototype, 'sourceBBox', { + + enumerable: true, + + get: function() { + var sourceView = this.sourceView; + var sourceMagnet = this.sourceMagnet; + if (sourceView) { + if (!sourceMagnet) sourceMagnet = sourceView.el; + return sourceView.getNodeBBox(sourceMagnet); + } + var sourceDef = this.model.source(); + return new g.Rect(sourceDef.x, sourceDef.y, 1, 1); + } + +}); + +Object.defineProperty(joint.dia.LinkView.prototype, 'targetBBox', { + + enumerable: true, + + get: function() { + var targetView = this.targetView; + var targetMagnet = this.targetMagnet; + if (targetView) { + if (!targetMagnet) targetMagnet = targetView.el; + return targetView.getNodeBBox(targetMagnet); + } + var targetDef = this.model.target(); + return new g.Rect(targetDef.x, targetDef.y, 1, 1); + } +}); + + joint.dia.Paper = joint.mvc.View.extend({ className: 'paper', @@ -11049,8 +17001,10 @@ joint.dia.Paper = joint.mvc.View.extend({ // Prevent the default context menu from being displayed. preventContextMenu: true, + // Prevent the default action for blank:pointer. preventDefaultBlankAction: true, + // Restrict the translation of elements by given bounding box. // Option accepts a boolean: // true - the translation is restricted to the paper area @@ -11063,6 +17017,7 @@ joint.dia.Paper = joint.mvc.View.extend({ // Or a bounding box: // restrictTranslate: { x: 10, y: 10, width: 790, height: 590 } restrictTranslate: false, + // Marks all available magnets with 'available-magnet' class name and all available cells with // 'available-cell' class name. Marks them when dragging a link is started and unmark // when the dragging is stopped. @@ -11081,8 +17036,14 @@ joint.dia.Paper = joint.mvc.View.extend({ // e.g. { name: 'oneSide', args: { padding: 10 }} or a function defaultRouter: { name: 'normal' }, + defaultAnchor: { name: 'center' }, + + defaultConnectionPoint: { name: 'bbox' }, + /* CONNECTING */ + connectionStrategy: null, + // Check whether to add a new link to the graph when user clicks on an a magnet. validateMagnet: function(cellView, magnet) { return magnet.getAttribute('magnet') !== 'passive'; @@ -11107,7 +17068,7 @@ joint.dia.Paper = joint.mvc.View.extend({ }, // Determines the way how a cell finds a suitable parent when it's dragged over the paper. - // The cell with the highest z-index (visually on the top) will be choosen. + // The cell with the highest z-index (visually on the top) will be chosen. findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft' // If enabled only the element on the very front is taken into account for the embedding. @@ -11124,6 +17085,11 @@ joint.dia.Paper = joint.mvc.View.extend({ // i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 }; linkPinning: true, + // Custom validation after an interaction with a link ends. + // Recognizes a function. If `false` is returned, the link is disallowed (removed or reverted) + // (linkView, paper) => boolean + allowLink: null, + // Allowed number of mousemove events after which the pointerclick event will be still triggered. clickThreshold: 0, @@ -11138,23 +17104,37 @@ joint.dia.Paper = joint.mvc.View.extend({ }, events: { - + 'dblclick': 'pointerdblclick', + 'click': 'pointerclick', // triggered alongside pointerdown and pointerup if no movement + 'touchend': 'pointerclick', // triggered alongside pointerdown and pointerup if no movement + 'contextmenu': 'contextmenu', 'mousedown': 'pointerdown', - 'dblclick': 'mousedblclick', - 'click': 'mouseclick', 'touchstart': 'pointerdown', - 'touchend': 'mouseclick', - 'touchmove': 'pointermove', - 'mousemove': 'pointermove', - 'mouseover .joint-cell': 'cellMouseover', - 'mouseout .joint-cell': 'cellMouseout', - 'contextmenu': 'contextmenu', + 'mouseover': 'mouseover', + 'mouseout': 'mouseout', + 'mouseenter': 'mouseenter', + 'mouseleave': 'mouseleave', 'mousewheel': 'mousewheel', 'DOMMouseScroll': 'mousewheel', - 'mouseenter .joint-cell': 'cellMouseenter', - 'mouseleave .joint-cell': 'cellMouseleave', - 'mousedown .joint-cell [event]': 'cellEvent', - 'touchstart .joint-cell [event]': 'cellEvent' + 'mouseenter .joint-cell': 'mouseenter', + 'mouseleave .joint-cell': 'mouseleave', + 'mouseenter .joint-tools': 'mouseenter', + 'mouseleave .joint-tools': 'mouseleave', + 'mousedown .joint-cell [event]': 'onevent', // interaction with cell with `event` attribute set + 'touchstart .joint-cell [event]': 'onevent', + 'mousedown .joint-cell [magnet]': 'onmagnet', // interaction with cell with `magnet` attribute set + 'touchstart .joint-cell [magnet]': 'onmagnet', + 'mousedown .joint-link .label': 'onlabel', // interaction with link label + 'touchstart .joint-link .label': 'onlabel', + 'dragstart .joint-cell image': 'onImageDragStart' // firefox fix + }, + + documentEvents: { + 'mousemove': 'pointermove', + 'touchmove': 'pointermove', + 'mouseup': 'pointerup', + 'touchend': 'pointerup', + 'touchcancel': 'pointerup' }, _highlights: {}, @@ -11205,15 +17185,6 @@ joint.dia.Paper = joint.mvc.View.extend({ ); }, - bindDocumentEvents: function() { - var eventNS = this.getEventNamespace(); - this.$document.on('mouseup' + eventNS + ' touchend' + eventNS, this.pointerup); - }, - - unbindDocumentEvents: function() { - this.$document.off(this.getEventNamespace()); - }, - render: function() { this.$el.empty(); @@ -11221,9 +17192,10 @@ joint.dia.Paper = joint.mvc.View.extend({ this.svg = V('svg').attr({ width: '100%', height: '100%' }).node; this.viewport = V('g').addClass(joint.util.addClassNamePrefix('viewport')).node; this.defs = V('defs').node; - + this.tools = V('g').addClass(joint.util.addClassNamePrefix('tools-container')).node; // Append `` element to the SVG document. This is useful for filters and gradients. - V(this.svg).append([this.viewport, this.defs]); + // It's desired to have the defs defined before the viewport (e.g. to make a PDF document pick up defs properly). + V(this.svg).append([this.defs, this.viewport, this.tools]); this.$background = $('
').addClass(joint.util.addClassNamePrefix('paper-background')); if (this.options.background) { @@ -11255,6 +17227,7 @@ joint.dia.Paper = joint.mvc.View.extend({ // For storing the current transformation matrix (CTM) of the paper's viewport. _viewportMatrix: null, + // For verifying whether the CTM is up-to-date. The viewport transform attribute // could have been manipulated directly. _viewportTransformString: null, @@ -11286,7 +17259,10 @@ joint.dia.Paper = joint.mvc.View.extend({ // Setter: ctm = V.createSVGMatrix(ctm); - V(viewport).transform(ctm, { absolute: true }); + ctmString = V.matrixToTransformString(ctm); + viewport.setAttribute('transform', ctmString); + this.tools.setAttribute('transform', ctmString); + this._viewportMatrix = ctm; this._viewportTransformString = viewport.getAttribute('transform'); @@ -11298,15 +17274,18 @@ joint.dia.Paper = joint.mvc.View.extend({ return V.createSVGMatrix(this.viewport.getScreenCTM()); }, + _sortDelayingBatches: ['add', 'to-front', 'to-back'], + _onSort: function() { - if (!this.model.hasActiveBatch('add')) { + if (!this.model.hasActiveBatch(this._sortDelayingBatches)) { this.sortViews(); } }, _onBatchStop: function(data) { var name = data && data.batchName; - if (name === 'add' && !this.model.hasActiveBatch('add')) { + if (this._sortDelayingBatches.includes(name) && + !this.model.hasActiveBatch(this._sortDelayingBatches)) { this.sortViews(); } }, @@ -11315,7 +17294,6 @@ joint.dia.Paper = joint.mvc.View.extend({ //clean up all DOM elements/views to prevent memory leaks this.removeViews(); - this.unbindDocumentEvents(); }, setDimensions: function(width, height) { @@ -11491,6 +17469,13 @@ joint.dia.Paper = joint.mvc.View.extend({ this.translate(newOx, newOy); }, + // Return the dimensions of the content area in local units (without transformations). + getContentArea: function() { + + return V(this.viewport).getBBox(); + }, + + // Return the dimensions of the content bbox in client units (as it appears on screen). getContentBBox: function() { var crect = this.viewport.getBoundingClientRect(); @@ -11621,11 +17606,14 @@ joint.dia.Paper = joint.mvc.View.extend({ view.paper = this; view.render(); + return view; + }, + + onImageDragStart: function() { // This is the only way to prevent image dragging in Firefox that works. // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help. - $(view.el).find('image').on('dragstart', function() { return false; }); - return view; + return false; }, beforeRenderViews: function(cells) { @@ -11881,6 +17869,21 @@ joint.dia.Paper = joint.mvc.View.extend({ }, this); }, + removeTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'remove'); + return this; + }, + + hideTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'hide'); + return this; + }, + + showTools: function() { + joint.dia.CellView.dispatchToolsEvent(this, 'show'); + return this; + }, + getModelById: function(id) { return this.model.getCell(id); @@ -11954,20 +17957,24 @@ joint.dia.Paper = joint.mvc.View.extend({ }, localToPagePoint: function(x, y) { + return this.localToPaperPoint(x, y).offset(this.pageOffset()); }, localToPageRect: function(x, y, width, height) { + return this.localToPaperRect(x, y, width, height).moveAndExpand(this.pageOffset()); }, pageToLocalPoint: function(x, y) { + var pagePoint = g.Point(x, y); var paperPoint = pagePoint.difference(this.pageOffset()); return this.paperToLocalPoint(paperPoint); }, pageToLocalRect: function(x, y, width, height) { + var pageOffset = this.pageOffset(); var paperRect = g.Rect(x, y, width, height); paperRect.x -= pageOffset.x; @@ -11976,72 +17983,38 @@ joint.dia.Paper = joint.mvc.View.extend({ }, clientOffset: function() { + var clientRect = this.svg.getBoundingClientRect(); return g.Point(clientRect.left, clientRect.top); }, pageOffset: function() { + return this.clientOffset().offset(window.scrollX, window.scrollY); }, - linkAllowed: function(linkViewOrModel) { - - var link; + linkAllowed: function(linkView) { - if (linkViewOrModel instanceof joint.dia.Link) { - link = linkViewOrModel; - } else if (linkViewOrModel instanceof joint.dia.LinkView) { - link = linkViewOrModel.model; - } else { - throw new Error('Must provide link model or view.'); + if (!(linkView instanceof joint.dia.LinkView)) { + throw new Error('Must provide a linkView.'); } - if (!this.options.multiLinks) { - - // Do not allow multiple links to have the same source and target. - - var source = link.get('source'); - var target = link.get('target'); - - if (source.id && target.id) { - - var sourceModel = link.getSourceElement(); - - if (sourceModel) { - - var connectedLinks = this.model.getConnectedLinks(sourceModel, { - outbound: true, - inbound: false - }); - - var numSameLinks = connectedLinks.filter(function(_link) { - - var _source = _link.get('source'); - var _target = _link.get('target'); - - return _source && _source.id === source.id && - (!_source.port || (_source.port === source.port)) && - _target && _target.id === target.id && - (!_target.port || (_target.port === target.port)); + var link = linkView.model; + var paperOptions = this.options; + var graph = this.model; + var ns = graph.constructor.validations; - }).length; - - if (numSameLinks > 1) { - return false; - } - } - } + if (!paperOptions.multiLinks) { + if (!ns.multiLinks.call(this, graph, link)) return false; } - if ( - !this.options.linkPinning && - ( - !joint.util.has(link.get('source'), 'id') || - !joint.util.has(link.get('target'), 'id') - ) - ) { + if (!paperOptions.linkPinning) { // Link pinning is not allowed and the link is not connected to the target. - return false; + if (!ns.linkPinning.call(this, graph, link)) return false; + } + + if (typeof paperOptions.allowLink === 'function') { + if (!paperOptions.allowLink.call(this, linkView, this)) return false; } return true; @@ -12056,8 +18029,9 @@ joint.dia.Paper = joint.mvc.View.extend({ : this.options.defaultLink.clone(); }, - // Cell highlighting - // ----------------- + // Cell highlighting. + // ------------------ + resolveHighlighter: function(opt) { opt = opt || {}; @@ -12158,9 +18132,10 @@ joint.dia.Paper = joint.mvc.View.extend({ // Interaction. // ------------ - mousedblclick: function(evt) { + pointerdblclick: function(evt) { evt.preventDefault(); + evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); @@ -12169,18 +18144,16 @@ joint.dia.Paper = joint.mvc.View.extend({ var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { - view.pointerdblclick(evt, localPoint.x, localPoint.y); } else { - this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y); } }, - mouseclick: function(evt) { + pointerclick: function(evt) { - // Trigger event when mouse not moved. + // Trigger event only if mouse has not moved. if (this._mousemoved <= this.options.clickThreshold) { evt = joint.util.normalizeEvent(evt); @@ -12191,46 +18164,19 @@ joint.dia.Paper = joint.mvc.View.extend({ var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { - view.pointerclick(evt, localPoint.x, localPoint.y); } else { - this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y); } } }, - // Guard guards the event received. If the event is not interesting, guard returns `true`. - // Otherwise, it return `false`. - guard: function(evt, view) { - - if (this.options.guard && this.options.guard(evt, view)) { - return true; - } - - if (evt.data && evt.data.guarded !== undefined) { - return evt.data.guarded; - } - - if (view && view.model && (view.model instanceof joint.dia.Cell)) { - return false; - } - - if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { - return false; - } - - return true; // Event guarded. Paper should not react on it in any way. - }, - contextmenu: function(evt) { - evt = joint.util.normalizeEvent(evt); + if (this.options.preventContextMenu) evt.preventDefault(); - if (this.options.preventContextMenu) { - evt.preventDefault(); - } + evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; @@ -12238,89 +18184,151 @@ joint.dia.Paper = joint.mvc.View.extend({ var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { - view.contextmenu(evt, localPoint.x, localPoint.y); } else { - this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y); } }, pointerdown: function(evt) { - this.bindDocumentEvents(); - evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; - this._mousemoved = 0; - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { evt.preventDefault(); - - this.sourceView = view; - view.pointerdown(evt, localPoint.x, localPoint.y); } else { - if (this.options.preventDefaultBlankAction) { - evt.preventDefault(); - } + if (this.options.preventDefaultBlankAction) evt.preventDefault(); this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y); } + + this.delegateDragEvents(view, evt.data); }, pointermove: function(evt) { - var view = this.sourceView; - if (view) { + evt.preventDefault(); - evt.preventDefault(); + // mouse moved counter + var data = this.eventData(evt); + data.mousemoved || (data.mousemoved = 0); + var mousemoved = ++data.mousemoved; + if (mousemoved <= this.options.moveThreshold) return; - // Mouse moved counter. - var mousemoved = ++this._mousemoved; - if (mousemoved > this.options.moveThreshold) { + evt = joint.util.normalizeEvent(evt); - evt = joint.util.normalizeEvent(evt); + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - view.pointermove(evt, localPoint.x, localPoint.y); - } + var view = data.sourceView; + if (view) { + view.pointermove(evt, localPoint.x, localPoint.y); + } else { + this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y); } + + this.eventData(evt, data); }, pointerup: function(evt) { - this.unbindDocumentEvents(); + this.undelegateDocumentEvents(); evt = joint.util.normalizeEvent(evt); var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - if (this.sourceView) { + var view = this.eventData(evt).sourceView; + if (view) { + view.pointerup(evt, localPoint.x, localPoint.y); + } else { + this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); + } + + this.delegateEvents(); + }, + + mouseover: function(evt) { + + evt = joint.util.normalizeEvent(evt); - this.sourceView.pointerup(evt, localPoint.x, localPoint.y); + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; - //"delete sourceView" occasionally throws an error in chrome (illegal access exception) - this.sourceView = null; + if (view) { + view.mouseover(evt); } else { + if (this.el === evt.target) return; // prevent border of paper from triggering this + this.trigger('blank:mouseover', evt); + } + }, - this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); + mouseout: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + + if (view) { + view.mouseout(evt); + + } else { + if (this.el === evt.target) return; // prevent border of paper from triggering this + this.trigger('blank:mouseout', evt); + } + }, + + mouseenter: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + var relatedView = this.findView(evt.relatedTarget); + if (view) { + // mouse moved from tool over view? + if (relatedView === view) return; + view.mouseenter(evt); + } else { + if (relatedView) return; + // `paper` (more descriptive), not `blank` + this.trigger('paper:mouseenter', evt); + } + }, + + mouseleave: function(evt) { + + evt = joint.util.normalizeEvent(evt); + + var view = this.findView(evt.target); + if (this.guard(evt, view)) return; + var relatedView = this.findView(evt.relatedTarget); + if (view) { + // mouse moved from view over tool? + if (relatedView === view) return; + view.mouseleave(evt); + } else { + if (relatedView) return; + // `paper` (more descriptive), not `blank` + this.trigger('paper:mouseleave', evt); } }, mousewheel: function(evt) { evt = joint.util.normalizeEvent(evt); + var view = this.findView(evt.target); if (this.guard(evt, view)) return; @@ -12329,66 +18337,91 @@ joint.dia.Paper = joint.mvc.View.extend({ var delta = Math.max(-1, Math.min(1, (originalEvent.wheelDelta || -originalEvent.detail))); if (view) { - view.mousewheel(evt, localPoint.x, localPoint.y, delta); } else { - this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta); } }, - cellMouseover: function(evt) { + onevent: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view) { - if (this.guard(evt, view)) return; - view.mouseover(evt); + var eventNode = evt.currentTarget; + var eventName = eventNode.getAttribute('event'); + if (eventName) { + var view = this.findView(eventNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + + var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); + view.onevent(evt, eventName, localPoint.x, localPoint.y); + } } }, - cellMouseout: function(evt) { + onmagnet: function(evt) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); + var magnetNode = evt.currentTarget; + var magnetValue = magnetNode.getAttribute('magnet'); + if (magnetValue) { + var view = this.findView(magnetNode); + if (view) { + + evt = joint.util.normalizeEvent(evt); + if (this.guard(evt, view)) return; + if (!this.options.validateMagnet(view, magnetNode)) return; + + var localPoint = this.snapToGrid(evt.clientX, evt.clientY); + view.onmagnet(evt, localPoint.x, localPoint.y); + } + } + }, + + onlabel: function(evt) { + + var labelNode = evt.currentTarget; + var view = this.findView(labelNode); if (view) { + + evt = joint.util.normalizeEvent(evt); if (this.guard(evt, view)) return; - view.mouseout(evt); + + var localPoint = this.snapToGrid(evt.clientX, evt.clientY); + view.onlabel(evt, localPoint.x, localPoint.y); } }, - cellMouseenter: function(evt) { + delegateDragEvents: function(view, data) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view && !this.guard(evt, view)) { - view.mouseenter(evt); - } + data || (data = {}); + this.eventData({ data: data }, { sourceView: view || null, mousemoved: 0 }); + this.delegateDocumentEvents(null, data); + this.undelegateEvents(); }, - cellMouseleave: function(evt) { + // Guard the specified event. If the event is not interesting, guard returns `true`. + // Otherwise, it returns `false`. + guard: function(evt, view) { - evt = joint.util.normalizeEvent(evt); - var view = this.findView(evt.target); - if (view && !this.guard(evt, view)) { - view.mouseleave(evt); + if (this.options.guard && this.options.guard(evt, view)) { + return true; } - }, - cellEvent: function(evt) { + if (evt.data && evt.data.guarded !== undefined) { + return evt.data.guarded; + } - evt = joint.util.normalizeEvent(evt); + if (view && view.model && (view.model instanceof joint.dia.Cell)) { + return false; + } - var currentTarget = evt.currentTarget; - var eventName = currentTarget.getAttribute('event'); - if (eventName) { - var view = this.findView(currentTarget); - if (view && !this.guard(evt, view)) { - var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); - view.event(evt, eventName, localPoint.x, localPoint.y); - } + if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { + return false; } + + return true; // Event guarded. Paper should not react on it in any way. }, setGridSize: function(gridSize) { @@ -12410,22 +18443,22 @@ joint.dia.Paper = joint.mvc.View.extend({ return this; }, - _getGriRefs: function () { + _getGriRefs: function() { if (!this._gridCache) { this._gridCache = { root: V('svg', { width: '100%', height: '100%' }, V('defs')), patterns: {}, - add: function (id, vel) { + add: function(id, vel) { V(this.root.node.childNodes[0]).append(vel); this.patterns[id] = vel; this.root.append(V('rect', { width: "100%", height: "100%", fill: 'url(#' + id + ')' })); }, - get: function (id) { + get: function(id) { return this.patterns[id] }, - exist: function (id) { + exist: function(id) { return this.patterns[id] !== undefined; } } @@ -12434,7 +18467,7 @@ joint.dia.Paper = joint.mvc.View.extend({ return this._gridCache; }, - setGrid:function (drawGrid) { + setGrid: function(drawGrid) { this.clearGrid(); @@ -12442,13 +18475,13 @@ joint.dia.Paper = joint.mvc.View.extend({ this._gridSettings = []; var optionsList = Array.isArray(drawGrid) ? drawGrid : [drawGrid || {}]; - optionsList.forEach(function (item) { + optionsList.forEach(function(item) { this._gridSettings.push.apply(this._gridSettings, this._resolveDrawGridOption(item)); }, this); return this; }, - _resolveDrawGridOption: function (opt) { + _resolveDrawGridOption: function(opt) { var namespace = this.constructor.gridPatterns; if (joint.util.isString(opt) && Array.isArray(namespace[opt])) { @@ -12496,7 +18529,7 @@ joint.dia.Paper = joint.mvc.View.extend({ var ctm = this.matrix(); var refs = this._getGriRefs(); - this._gridSettings.forEach(function (gridLayerSetting, index) { + this._gridSettings.forEach(function(gridLayerSetting, index) { var id = 'pattern_' + index; var options = joint.util.merge(gridLayerSetting, localOptions[index], { @@ -12662,9 +18695,11 @@ joint.dia.Paper = joint.mvc.View.extend({ joint.util.invoke(this._views, 'setInteractivity', value); }, - // Paper Defs + // Paper definitions. + // ------------------ isDefined: function(defId) { + return !!this.svg.getElementById(defId); }, @@ -12783,7 +18818,6 @@ joint.dia.Paper = joint.mvc.View.extend({ return markerId; } - }, { backgroundPatterns: { @@ -13448,9 +19482,23 @@ joint.dia.Paper = joint.mvc.View.extend({ util.assign(joint.dia.ElementView.prototype, { - portContainerMarkup: '', - portMarkup: '', - portLabelMarkup: '', + portContainerMarkup: 'g', + portMarkup: [{ + tagName: 'circle', + selector: 'circle', + attributes: { + 'r': 10, + 'fill': '#FFFFFF', + 'stroke': '#000000' + } + }], + portLabelMarkup: [{ + tagName: 'text', + selector: 'text', + attributes: { + 'fill': '#000000' + } + }], /** @type {Object} */ _portElementsCache: null, @@ -13565,6 +19613,14 @@ joint.dia.Paper = joint.mvc.View.extend({ return this._createPortElement(port); }, + findPortNode: function(portId, selector) { + var portCache = this._portElementsCache[portId]; + if (!portCache) return null; + var portRoot = portCache.portContentElement.node; + var portSelectors = portCache.portContentSelectors; + return this.findBySelector(selector, portRoot, portSelectors)[0]; + }, + /** * @private */ @@ -13591,28 +19647,86 @@ joint.dia.Paper = joint.mvc.View.extend({ */ _createPortElement: function(port) { - var portContentElement = V(this._getPortMarkup(port)); - var portLabelContentElement = V(this._getPortLabelMarkup(port.label)); - if (portContentElement && portContentElement.length > 1) { - throw new Error('ElementView: Invalid port markup - multiple roots.'); + var portElement; + var labelElement; + + var portMarkup = this._getPortMarkup(port); + var portSelectors; + if (Array.isArray(portMarkup)) { + var portDoc = util.parseDOMJSON(portMarkup); + var portFragment = portDoc.fragment; + if (portFragment.childNodes.length > 1) { + portElement = V('g').append(portFragment); + } else { + portElement = V(portFragment.firstChild); + } + portSelectors = portDoc.selectors; + } else { + portElement = V(portMarkup); + if (Array.isArray(portElement)) { + portElement = V('g').append(portElement); + } + } + + if (!portElement) { + throw new Error('ElementView: Invalid port markup.'); } - portContentElement.attr({ + portElement.attr({ 'port': port.id, 'port-group': port.group }); - var portElement = V(this.portContainerMarkup) - .append(portContentElement) - .append(portLabelContentElement); + var labelMarkup = this._getPortLabelMarkup(port.label); + var labelSelectors; + if (Array.isArray(labelMarkup)) { + var labelDoc = util.parseDOMJSON(labelMarkup); + var labelFragment = labelDoc.fragment; + if (labelFragment.childNodes.length > 1) { + labelElement = V('g').append(labelFragment); + } else { + labelElement = V(labelFragment.firstChild); + } + labelSelectors = labelDoc.selectors; + } else { + labelElement = V(labelMarkup); + if (Array.isArray(labelElement)) { + labelElement = V('g').append(labelElement); + } + } + + if (!labelElement) { + throw new Error('ElementView: Invalid port label markup.'); + } + + var portContainerSelectors; + if (portSelectors && labelSelectors) { + for (var key in labelSelectors) { + if (portSelectors[key]) throw new Error('ElementView: selectors within port must be unique.'); + } + portContainerSelectors = util.assign({}, portSelectors, labelSelectors); + } else { + portContainerSelectors = portSelectors || labelSelectors; + } + + var portContainerElement = V(this.portContainerMarkup) + .addClass('joint-port') + .append([ + portElement.addClass('joint-port-body'), + labelElement.addClass('joint-port-label') + ]); this._portElementsCache[port.id] = { - portElement: portElement, - portLabelElement: portLabelContentElement + portElement: portContainerElement, + portLabelElement: labelElement, + portSelectors: portContainerSelectors, + portLabelSelectors: labelSelectors, + portContentElement: portElement, + portContentSelectors: portSelectors }; - return portElement; + return portContainerElement; }, /** @@ -13631,14 +19745,16 @@ joint.dia.Paper = joint.mvc.View.extend({ var portTransformation = metrics.portTransformation; this.applyPortTransform(cached.portElement, portTransformation); this.updateDOMSubtreeAttributes(cached.portElement.node, metrics.portAttrs, { - rootBBox: g.Rect(metrics.portSize) + rootBBox: new g.Rect(metrics.portSize), + selectors: cached.portSelectors }); var labelTransformation = metrics.labelTransformation; if (labelTransformation) { this.applyPortTransform(cached.portLabelElement, labelTransformation, (-portTransformation.angle || 0)); this.updateDOMSubtreeAttributes(cached.portLabelElement.node, labelTransformation.attrs, { - rootBBox: g.Rect(metrics.labelSize) + rootBBox: new g.Rect(metrics.labelSize), + selectors: cached.portLabelSelectors }); } } @@ -13941,7 +20057,7 @@ joint.shapes.basic.PortsModelInterface = { joint.util.assign(attrs, portAttributes); }, this); - joint.util.toArray(('outPorts')).forEach(function(portName, index, ports) { + joint.util.toArray(this.get('outPorts')).forEach(function(portName, index, ports) { var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out'); this._portSelectors = this._portSelectors.concat(Object.keys(portAttributes)); joint.util.assign(attrs, portAttributes); @@ -14058,7 +20174,7 @@ joint.shapes.basic.Generic.define('basic.TextBlock', { updateSize: function(cell, size) { - // Selector `foreignObject' doesn't work accross all browsers, we'r using class selector instead. + // Selector `foreignObject' doesn't work across all browsers, we're using class selector instead. // We have to clone size as we don't want attributes.div.style to be same object as attributes.size. this.attr({ '.fobj': joint.util.assign({}, size), @@ -14075,7 +20191,7 @@ joint.shapes.basic.Generic.define('basic.TextBlock', { // Content element is a
element. this.attr({ '.content': { - html: content + html: joint.util.sanitizeHTML(content) } }); @@ -14170,45 +20286,735 @@ joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({ } }); +(function(dia, util, env, V) { + + 'use strict'; + + // ELEMENTS + + var Element = dia.Element; + + Element.define('standard.Rectangle', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body', + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Circle', { + attrs: { + body: { + refCx: '50%', + refCy: '50%', + refR: '50%', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'circle', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Ellipse', { + attrs: { + body: { + refCx: '50%', + refCy: '50%', + refRx: '50%', + refRy: '50%', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'ellipse', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Path', { + attrs: { + body: { + refD: 'M 0 0 L 10 0 10 10 0 10 Z', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Polygon', { + attrs: { + body: { + refPoints: '0 0 10 0 10 10 0 10', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'polygon', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Polyline', { + attrs: { + body: { + refPoints: '0 0 10 0 10 10 0 10 0 0', + strokeWidth: 2, + stroke: '#333333', + fill: '#FFFFFF' + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'polyline', + selector: 'body' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.Image', { + attrs: { + image: { + refWidth: '100%', + refHeight: '100%', + // xlinkHref: '[URL]' + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'image', + selector: 'image' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.BorderedImage', { + attrs: { + border: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + strokeWidth: 2 + }, + image: { + // xlinkHref: '[URL]' + refWidth: -1, + refHeight: -1, + x: 0.5, + y: 0.5 + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'image', + selector: 'image' + }, { + tagName: 'rect', + selector: 'border', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.EmbeddedImage', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + fill: '#FFFFFF', + strokeWidth: 2 + }, + image: { + // xlinkHref: '[URL]' + refWidth: '30%', + refHeight: -20, + x: 10, + y: 10, + preserveAspectRatio: 'xMidYMin' + }, + label: { + textVerticalAnchor: 'top', + textAnchor: 'left', + refX: '30%', + refX2: 20, // 10 + 10 + refY: 10, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, { + tagName: 'image', + selector: 'image' + }, { + tagName: 'text', + selector: 'label' + }] + }); + + Element.define('standard.HeaderedRectangle', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + header: { + refWidth: '100%', + height: 30, + strokeWidth: 2, + stroke: '#000000', + fill: '#FFFFFF' + }, + headerText: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: 15, + fontSize: 16, + fill: '#333333' + }, + bodyText: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '50%', + refY2: 15, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, { + tagName: 'rect', + selector: 'header' + }, { + tagName: 'text', + selector: 'headerText' + }, { + tagName: 'text', + selector: 'bodyText' + }] + }); + + var CYLINDER_TILT = 10; + + joint.dia.Element.define('standard.Cylinder', { + attrs: { + body: { + lateralArea: CYLINDER_TILT, + fill: '#FFFFFF', + stroke: '#333333', + strokeWidth: 2 + }, + top: { + refCx: '50%', + cy: CYLINDER_TILT, + refRx: '50%', + ry: CYLINDER_TILT, + fill: '#FFFFFF', + stroke: '#333333', + strokeWidth: 2 + }, + label: { + textVerticalAnchor: 'middle', + textAnchor: 'middle', + refX: '50%', + refY: '100%', + refY2: 15, + fontSize: 14, + fill: '#333333' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'body' + }, { + tagName: 'ellipse', + selector: 'top' + }, { + tagName: 'text', + selector: 'label' + }], + + topRy: function(t, opt) { + // getter + if (t === undefined) return this.attr('body/lateralArea'); + + // setter + var isPercentage = util.isPercentage(t); + + var bodyAttrs = { lateralArea: t }; + var topAttrs = isPercentage + ? { refCy: t, refRy: t, cy: null, ry: null } + : { refCy: null, refRy: null, cy: t, ry: t }; + + return this.attr({ body: bodyAttrs, top: topAttrs }, opt); + } + + }, { + attributes: { + lateralArea: { + set: function(t, refBBox) { + var isPercentage = util.isPercentage(t); + if (isPercentage) t = parseFloat(t) / 100; + + var x = refBBox.x; + var y = refBBox.y; + var w = refBBox.width; + var h = refBBox.height; + + // curve control point variables + var rx = w / 2; + var ry = isPercentage ? (h * t) : t; + + var kappa = V.KAPPA; + var cx = kappa * rx; + var cy = kappa * (isPercentage ? (h * t) : t); + + // shape variables + var xLeft = x; + var xCenter = x + (w / 2); + var xRight = x + w; + + var ySideTop = y + ry; + var yCurveTop = ySideTop - ry; + var ySideBottom = y + h - ry; + var yCurveBottom = y + h; + + // return calculated shape + var data = [ + 'M', xLeft, ySideTop, + 'L', xLeft, ySideBottom, + 'C', x, (ySideBottom + cy), (xCenter - cx), yCurveBottom, xCenter, yCurveBottom, + 'C', (xCenter + cx), yCurveBottom, xRight, (ySideBottom + cy), xRight, ySideBottom, + 'L', xRight, ySideTop, + 'C', xRight, (ySideTop - cy), (xCenter + cx), yCurveTop, xCenter, yCurveTop, + 'C', (xCenter - cx), yCurveTop, xLeft, (ySideTop - cy), xLeft, ySideTop, + 'Z' + ]; + return { d: data.join(' ') }; + } + } + } + }); + + var foLabelMarkup = { + tagName: 'foreignObject', + selector: 'foreignObject', + attributes: { + 'overflow': 'hidden' + }, + children: [{ + tagName: 'div', + namespaceURI: 'http://www.w3.org/1999/xhtml', + selector: 'label', + style: { + width: '100%', + height: '100%', + position: 'static', + backgroundColor: 'transparent', + textAlign: 'center', + margin: 0, + padding: '0px 5px', + boxSizing: 'border-box', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }] + }; + + var svgLabelMarkup = { + tagName: 'text', + selector: 'label', + attributes: { + 'text-anchor': 'middle' + } + }; + + Element.define('standard.TextBlock', { + attrs: { + body: { + refWidth: '100%', + refHeight: '100%', + stroke: '#333333', + fill: '#ffffff', + strokeWidth: 2 + }, + foreignObject: { + refWidth: '100%', + refHeight: '100%' + }, + label: { + style: { + fontSize: 14 + } + } + } + }, { + markup: [{ + tagName: 'rect', + selector: 'body' + }, + (env.test('svgforeignobject')) ? foLabelMarkup : svgLabelMarkup + ] + }, { + attributes: { + text: { + set: function(text, refBBox, node, attrs) { + if (node instanceof HTMLElement) { + node.textContent = text; + } else { + // No foreign object + var style = attrs.style || {}; + var wrapValue = { text: text, width: -5, height: '100%' }; + var wrapAttrs = util.assign({ textVerticalAnchor: 'middle' }, style); + dia.attributes.textWrap.set.call(this, wrapValue, refBBox, node, wrapAttrs); + return { fill: style.color || null }; + } + }, + position: function(text, refBBox, node) { + // No foreign object + if (node instanceof SVGElement) return refBBox.center(); + } + } + } + }); + + // LINKS + + var Link = dia.Link; + + Link.define('standard.Link', { + attrs: { + line: { + connection: true, + stroke: '#333333', + strokeWidth: 2, + strokeLinejoin: 'round', + targetMarker: { + type: 'path', + d: 'M 10 -5 0 0 10 5 z' + } + }, + wrapper: { + connection: true, + strokeWidth: 10, + strokeLinejoin: 'round' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'wrapper', + attributes: { + 'fill': 'none', + 'cursor': 'pointer', + 'stroke': 'transparent' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none', + 'pointer-events': 'none' + } + }] + }); + + Link.define('standard.DoubleLink', { + attrs: { + line: { + connection: true, + stroke: '#DDDDDD', + strokeWidth: 4, + strokeLinejoin: 'round', + targetMarker: { + type: 'path', + stroke: '#000000', + d: 'M 10 -3 10 -10 -2 0 10 10 10 3' + } + }, + outline: { + connection: true, + stroke: '#000000', + strokeWidth: 6, + strokeLinejoin: 'round' + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'outline', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none' + } + }] + }); + + Link.define('standard.ShadowLink', { + attrs: { + line: { + connection: true, + stroke: '#FF0000', + strokeWidth: 20, + strokeLinejoin: 'round', + targetMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M 0 -10 -10 0 0 10 z' + }, + sourceMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M -10 -10 0 0 -10 10 0 10 0 -10 z' + } + }, + shadow: { + connection: true, + refX: 3, + refY: 6, + stroke: '#000000', + strokeOpacity: 0.2, + strokeWidth: 20, + strokeLinejoin: 'round', + targetMarker: { + 'type': 'path', + 'd': 'M 0 -10 -10 0 0 10 z', + 'stroke': 'none' + }, + sourceMarker: { + 'type': 'path', + 'stroke': 'none', + 'd': 'M -10 -10 0 0 -10 10 0 10 0 -10 z' + } + } + } + }, { + markup: [{ + tagName: 'path', + selector: 'shadow', + attributes: { + 'fill': 'none' + } + }, { + tagName: 'path', + selector: 'line', + attributes: { + 'fill': 'none' + } + }] + }); + + +})(joint.dia, joint.util, joint.env, V); + joint.routers.manhattan = (function(g, _, joint, util) { 'use strict'; var config = { - // size of the step to find a route + // size of the step to find a route (the grid of the manhattan pathfinder) step: 10, - // use of the perpendicular linkView option to connect center of element with first vertex + // the number of route finding loops that cause the router to abort + // returns fallback route instead + maximumLoops: 2000, + + // the number of decimal places to round floating point coordinates + precision: 10, + + // maximum change of direction + maxAllowedDirectionChange: 90, + + // should the router use perpendicular linkView option? + // does not connect anchor of element but rather a point close-by that is orthogonal + // this looks much better perpendicular: true, - // should be source or target not to be consider as an obstacle + // should the source and/or target not be considered as obstacles? excludeEnds: [], // 'source', 'target' - // should be any element with a certain type not to be consider as an obstacle + // should certain types of elements not be considered as obstacles? excludeTypes: ['basic.Text'], - // if number of route finding loops exceed the maximum, stops searching and returns - // fallback route - maximumLoops: 2000, - // possible starting directions from an element - startDirections: ['left', 'right', 'top', 'bottom'], + startDirections: ['top', 'right', 'bottom', 'left'], // possible ending directions to an element - endDirections: ['left', 'right', 'top', 'bottom'], + endDirections: ['top', 'right', 'bottom', 'left'], - // specify directions above + // specify the directions used above and what they mean directionMap: { + top: { x: 0, y: -1 }, right: { x: 1, y: 0 }, bottom: { x: 0, y: 1 }, - left: { x: -1, y: 0 }, - top: { x: 0, y: -1 } + left: { x: -1, y: 0 } + }, + + // cost of an orthogonal step + cost: function() { + + return this.step; + }, + + // an array of directions to find next points on the route + // different from start/end directions + directions: function() { + + var step = this.step; + var cost = this.cost(); + + return [ + { offsetX: step , offsetY: 0 , cost: cost }, + { offsetX: 0 , offsetY: step , cost: cost }, + { offsetX: -step , offsetY: 0 , cost: cost }, + { offsetX: 0 , offsetY: -step , cost: cost } + ]; + }, + + // a penalty received for direction change + penalties: function() { + + return { + 0: 0, + 45: this.step / 2, + 90: this.step / 2 + }; }, - // maximum change of the direction - maxAllowedDirectionChange: 90, - // padding applied on the element bounding boxes paddingBox: function() { @@ -14222,52 +21028,43 @@ joint.routers.manhattan = (function(g, _, joint, util) { }; }, - // an array of directions to find next points on the route - directions: function() { + // a router to use when the manhattan router fails + // (one of the partial routes returns null) + fallbackRouter: function(vertices, opt, linkView) { - var step = this.step; + if (!util.isFunction(joint.routers.orthogonal)) { + throw new Error('Manhattan requires the orthogonal router as default fallback.'); + } - return [ - { offsetX: step , offsetY: 0 , cost: step }, - { offsetX: 0 , offsetY: step , cost: step }, - { offsetX: -step , offsetY: 0 , cost: step }, - { offsetX: 0 , offsetY: -step , cost: step } - ]; + return joint.routers.orthogonal(vertices, util.assign({}, config, opt), linkView); }, - // a penalty received for direction change - penalties: function() { + /* Deprecated */ + // a simple route used in situations when main routing method fails + // (exceed max number of loop iterations, inaccessible) + fallbackRoute: function(from, to, opt) { - return { - 0: 0, - 45: this.step / 2, - 90: this.step / 2 - }; - }, + return null; // null result will trigger the fallbackRouter - // * Deprecated * - // a simple route used in situations, when main routing method fails - // (exceed loops, inaccessible). - /* i.e. - function(from, to, opts) { - // Find an orthogonal route ignoring obstacles. - var point = ((opts.previousDirAngle || 0) % 180 === 0) - ? g.point(from.x, to.y) - : g.point(to.x, from.y); - return [point, to]; - }, - */ - fallbackRoute: function() { - return null; + // left for reference: + /*// Find an orthogonal route ignoring obstacles. + + var point = ((opt.previousDirAngle || 0) % 180 === 0) + ? new g.Point(from.x, to.y) + : new g.Point(to.x, from.y); + + return [point];*/ }, // if a function is provided, it's used to route the link while dragging an end - // i.e. function(from, to, opts) { return []; } + // i.e. function(from, to, opt) { return []; } draggingRoute: null }; + // HELPER CLASSES // + // Map of obstacles - // Helper structure to identify whether a point lies in an obstacle. + // Helper structure to identify whether a point lies inside an obstacle. function ObstacleMap(opt) { this.map = {}; @@ -14280,9 +21077,9 @@ joint.routers.manhattan = (function(g, _, joint, util) { var opt = this.options; - // source or target element could be excluded from set of obstacles var excludedEnds = util.toArray(opt.excludeEnds).reduce(function(res, item) { + var end = link.get(item); if (end) { var cell = graph.getCell(end.id); @@ -14290,6 +21087,7 @@ joint.routers.manhattan = (function(g, _, joint, util) { res.push(cell); } } + return res; }, []); @@ -14306,11 +21104,11 @@ joint.routers.manhattan = (function(g, _, joint, util) { excludedAncestors = util.union(excludedAncestors, target.getAncestors().map(function(cell) { return cell.id })); } - // builds a map of all elements for quicker obstacle queries (i.e. is a point contained - // in any obstacle?) (a simplified grid search) - // The paper is divided to smaller cells, where each of them holds an information which - // elements belong to it. When we query whether a point is in an obstacle we don't need - // to go through all obstacles, we check only those in a particular cell. + // Builds a map of all elements for quicker obstacle queries (i.e. is a point contained + // in any obstacle?) (a simplified grid search). + // The paper is divided into smaller cells, where each holds information about which + // elements belong to it. When we query whether a point lies inside an obstacle we + // don't need to go through all obstacles, we check only those in a particular cell. var mapGridSize = this.mapGridSize; graph.getElements().reduce(function(map, element) { @@ -14321,19 +21119,20 @@ joint.routers.manhattan = (function(g, _, joint, util) { var isExcluded = isExcludedType || isExcludedEnd || isExcludedAncestor; if (!isExcluded) { - var bBox = element.getBBox().moveAndExpand(opt.paddingBox); + var bbox = element.getBBox().moveAndExpand(opt.paddingBox); - var origin = bBox.origin().snapToGrid(mapGridSize); - var corner = bBox.corner().snapToGrid(mapGridSize); + var origin = bbox.origin().snapToGrid(mapGridSize); + var corner = bbox.corner().snapToGrid(mapGridSize); for (var x = origin.x; x <= corner.x; x += mapGridSize) { for (var y = origin.y; y <= corner.y; y += mapGridSize) { var gridKey = x + '@' + y; map[gridKey] = map[gridKey] || []; - map[gridKey].push(bBox); + map[gridKey].push(bbox); } } } + return map; }, this.map); @@ -14378,154 +21177,363 @@ joint.routers.manhattan = (function(g, _, joint, util) { }; SortedSet.prototype.remove = function(item) { + this.hash[item] = this.CLOSE; }; SortedSet.prototype.isOpen = function(item) { + return this.hash[item] === this.OPEN; }; SortedSet.prototype.isClose = function(item) { + return this.hash[item] === this.CLOSE; }; SortedSet.prototype.isEmpty = function() { + return this.items.length === 0; }; SortedSet.prototype.pop = function() { + var item = this.items.shift(); this.remove(item); return item; }; + // HELPERS // + + // return source bbox + function getSourceBBox(linkView, opt) { + + // expand by padding box + if (opt && opt.paddingBox) return linkView.sourceBBox.clone().moveAndExpand(opt.paddingBox); + + return linkView.sourceBBox.clone(); + } + + // return target bbox + function getTargetBBox(linkView, opt) { + + // expand by padding box + if (opt && opt.paddingBox) return linkView.targetBBox.clone().moveAndExpand(opt.paddingBox); + + return linkView.targetBBox.clone(); + } + + // return source anchor + function getSourceAnchor(linkView, opt) { + + if (linkView.sourceAnchor) return linkView.sourceAnchor; + + // fallback: center of bbox + var sourceBBox = getSourceBBox(linkView, opt); + return sourceBBox.center(); + } + + // return target anchor + function getTargetAnchor(linkView, opt) { + + if (linkView.targetAnchor) return linkView.targetAnchor; + + // fallback: center of bbox + var targetBBox = getTargetBBox(linkView, opt); + return targetBBox.center(); // default + } + + // returns a direction index from start point to end point + // corrects for grid deformation between start and end + function getDirectionAngle(start, end, numDirections, grid, opt) { + + var quadrant = 360 / numDirections; + var angleTheta = start.theta(fixAngleEnd(start, end, grid, opt)); + var normalizedAngle = g.normalizeAngle(angleTheta + (quadrant / 2)); + return quadrant * Math.floor(normalizedAngle / quadrant); + } + + // helper function for getDirectionAngle() + // corrects for grid deformation + // (if a point is one grid steps away from another in both dimensions, + // it is considered to be 45 degrees away, even if the real angle is different) + // this causes visible angle discrepancies if `opt.step` is much larger than `paper.gridSize` + function fixAngleEnd(start, end, grid, opt) { + + var step = opt.step; + + var diffX = end.x - start.x; + var diffY = end.y - start.y; + + var gridStepsX = diffX / grid.x; + var gridStepsY = diffY / grid.y + + var distanceX = gridStepsX * step; + var distanceY = gridStepsY * step; + + return new g.Point(start.x + distanceX, start.y + distanceY); + } + + // return the change in direction between two direction angles + function getDirectionChange(angle1, angle2) { + + var directionChange = Math.abs(angle1 - angle2); + return (directionChange > 180) ? (360 - directionChange) : directionChange; + } + + // fix direction offsets according to current grid + function getGridOffsets(directions, grid, opt) { + + var step = opt.step; + + util.toArray(opt.directions).forEach(function(direction) { + + direction.gridOffsetX = (direction.offsetX / step) * grid.x; + direction.gridOffsetY = (direction.offsetY / step) * grid.y; + }); + } + + // get grid size in x and y dimensions, adapted to source and target positions + function getGrid(step, source, target) { + + return { + source: source.clone(), + x: getGridDimension(target.x - source.x, step), + y: getGridDimension(target.y - source.y, step) + } + } + + // helper function for getGrid() + function getGridDimension(diff, step) { + + // return step if diff = 0 + if (!diff) return step; + + var absDiff = Math.abs(diff); + var numSteps = Math.round(absDiff / step); + + // return absDiff if less than one step apart + if (!numSteps) return absDiff; + + // otherwise, return corrected step + var roundedDiff = numSteps * step; + var remainder = absDiff - roundedDiff; + var stepCorrection = remainder / numSteps; + + return step + stepCorrection; + } + + // return a clone of point snapped to grid + function snapToGrid(point, grid) { + + var source = grid.source; + + var snappedX = g.snapToGrid(point.x - source.x, grid.x) + source.x; + var snappedY = g.snapToGrid(point.y - source.y, grid.y) + source.y; + + return new g.Point(snappedX, snappedY); + } + + // round the point to opt.precision + function round(point, opt) { + + if (!point) return point; + + return point.round(opt.precision); + } + + // return a string representing the point + // string is rounded to nearest int in both dimensions + function getKey(point) { + + return point.clone().round().toString(); + } + + // return a normalized vector from given point + // used to determine the direction of a difference of two points function normalizePoint(point) { - return g.point( + + return new g.Point( point.x === 0 ? 0 : Math.abs(point.x) / point.x, point.y === 0 ? 0 : Math.abs(point.y) / point.y ); } - // reconstructs a route by concating points with their parents - function reconstructRoute(parents, point, startCenter, endCenter) { + // PATHFINDING // + + // reconstructs a route by concatenating points with their parents + function reconstructRoute(parents, points, tailPoint, from, to, opt) { var route = []; - var prevDiff = normalizePoint(endCenter.difference(point)); - var current = point; - var parent; - while ((parent = parents[current])) { + var prevDiff = normalizePoint(to.difference(tailPoint)); - var diff = normalizePoint(current.difference(parent)); + var currentKey = getKey(tailPoint); + var parent = parents[currentKey]; - if (!diff.equals(prevDiff)) { + var point; + while (parent) { - route.unshift(current); + point = round(points[currentKey], opt); + + var diff = normalizePoint(point.difference(round(parent.clone(), opt))); + if (!diff.equals(prevDiff)) { + route.unshift(point); prevDiff = diff; } - current = parent; + currentKey = getKey(parent); + parent = parents[currentKey]; } - var startDiff = normalizePoint(g.point(current).difference(startCenter)); - if (!startDiff.equals(prevDiff)) { - route.unshift(current); + var leadPoint = round(points[currentKey], opt); + + var fromDiff = normalizePoint(leadPoint.difference(from)); + if (!fromDiff.equals(prevDiff)) { + route.unshift(leadPoint); } return route; } - // find points around the rectangle taking given directions in the account - function getRectPoints(bbox, directionList, opt) { + // heuristic method to determine the distance between two points + function estimateCost(from, endPoints) { - var step = opt.step; - var center = bbox.center(); - var keys = util.isObject(opt.directionMap) ? Object.keys(opt.directionMap) : []; - var dirLis = util.toArray(directionList); - return keys.reduce(function(res, key) { + var min = Infinity; + + for (var i = 0, len = endPoints.length; i < len; i++) { + var cost = from.manhattanDistance(endPoints[i]); + if (cost < min) min = cost; + } + + return min; + } - if (dirLis.includes(key)) { + // find points around the bbox taking given directions into account + // lines are drawn from anchor in given directions, intersections recorded + // if anchor is outside bbox, only those directions that intersect get a rect point + // the anchor itself is returned as rect point (representing some directions) + // (since those directions are unobstructed by the bbox) + function getRectPoints(anchor, bbox, directionList, grid, opt) { - var direction = opt.directionMap[key]; + var directionMap = opt.directionMap; - var x = direction.x * bbox.width / 2; - var y = direction.y * bbox.height / 2; + var snappedAnchor = round(snapToGrid(anchor, grid), opt); + var snappedCenter = round(snapToGrid(bbox.center(), grid), opt); + var anchorCenterVector = snappedAnchor.difference(snappedCenter); - var point = center.clone().offset(x, y); + var keys = util.isObject(directionMap) ? Object.keys(directionMap) : []; + var dirList = util.toArray(directionList); + var rectPoints = keys.reduce(function(res, key) { - if (bbox.containsPoint(point)) { + if (dirList.includes(key)) { + var direction = directionMap[key]; - point.offset(direction.x * step, direction.y * step); + // create a line that is guaranteed to intersect the bbox if bbox is in the direction + // even if anchor lies outside of bbox + var endpoint = new g.Point( + snappedAnchor.x + direction.x * (Math.abs(anchorCenterVector.x) + bbox.width), + snappedAnchor.y + direction.y * (Math.abs(anchorCenterVector.y) + bbox.height) + ); + var intersectionLine = new g.Line(anchor, endpoint); + + // get the farther intersection, in case there are two + // (that happens if anchor lies next to bbox) + var intersections = intersectionLine.intersect(bbox) || []; + var numIntersections = intersections.length; + var farthestIntersectionDistance; + var farthestIntersection = null; + for (var i = 0; i < numIntersections; i++) { + var currentIntersection = intersections[i]; + var distance = snappedAnchor.squaredDistance(currentIntersection); + if (farthestIntersectionDistance === undefined || (distance > farthestIntersectionDistance)) { + farthestIntersectionDistance = distance; + farthestIntersection = snapToGrid(currentIntersection, grid); + } } + var point = round(farthestIntersection, opt); + + // if an intersection was found in this direction, it is our rectPoint + if (point) { + // if the rectPoint lies inside the bbox, offset it by one more step + if (bbox.containsPoint(point)) { + round(point.offset(direction.x * grid.x, direction.y * grid.y), opt); + } - res.push(point.snapToGrid(step)); + // then add the point to the result array + res.push(point); + } } - return res; + return res; }, []); - } - // returns a direction index from start point to end point - function getDirectionAngle(start, end, dirLen) { + // if anchor lies outside of bbox, add it to the array of points + if (!bbox.containsPoint(snappedAnchor)) rectPoints.push(snappedAnchor); - var q = 360 / dirLen; - return Math.floor(g.normalizeAngle(start.theta(end) + q / 2) / q) * q; + return rectPoints; } - function getDirectionChange(angle1, angle2) { + // finds the route between two points/rectangles (`from`, `to`) implementing A* algorithm + // rectangles get rect points assigned by getRectPoints() + function findRoute(from, to, map, opt) { - var dirChange = Math.abs(angle1 - angle2); - return dirChange > 180 ? 360 - dirChange : dirChange; - } + // Get grid for this route. - // heurestic method to determine the distance between two points - function estimateCost(from, endPoints) { + var sourceAnchor, targetAnchor; - var min = Infinity; + if (from instanceof g.Rect) { // `from` is sourceBBox + sourceAnchor = getSourceAnchor(this, opt).clone(); + } else { + sourceAnchor = from.clone(); + } - for (var i = 0, len = endPoints.length; i < len; i++) { - var cost = from.manhattanDistance(endPoints[i]); - if (cost < min) min = cost; + if (to instanceof g.Rect) { // `to` is targetBBox + targetAnchor = getTargetAnchor(this, opt).clone(); + } else { + targetAnchor = to.clone(); } - return min; - } + var grid = getGrid(opt.step, sourceAnchor, targetAnchor); - // finds the route between to points/rectangles implementing A* alghoritm - function findRoute(start, end, map, opt) { + // Get pathfinding points. - var step = opt.step; + var start, end; var startPoints, endPoints; - var startCenter, endCenter; // set of points we start pathfinding from - if (start instanceof g.rect) { - startPoints = getRectPoints(start, opt.startDirections, opt); - startCenter = start.center().snapToGrid(step); + if (from instanceof g.Rect) { // `from` is sourceBBox + start = round(snapToGrid(sourceAnchor, grid), opt); + startPoints = getRectPoints(start, from, opt.startDirections, grid, opt); + } else { - startCenter = start.clone().snapToGrid(step); - startPoints = [startCenter]; + start = round(snapToGrid(sourceAnchor, grid), opt); + startPoints = [start]; } // set of points we want the pathfinding to finish at - if (end instanceof g.rect) { - endPoints = getRectPoints(end, opt.endDirections, opt); - endCenter = end.center().snapToGrid(step); + if (to instanceof g.Rect) { // `to` is targetBBox + end = round(snapToGrid(targetAnchor, grid), opt); + endPoints = getRectPoints(targetAnchor, to, opt.endDirections, grid, opt); + } else { - endCenter = end.clone().snapToGrid(step); - endPoints = [endCenter]; + end = round(snapToGrid(targetAnchor, grid), opt); + endPoints = [end]; } - // take into account only accessible end points + // take into account only accessible rect points (those not under obstacles) startPoints = startPoints.filter(map.isPointAccessible, map); endPoints = endPoints.filter(map.isPointAccessible, map); - // Check if there is a accessible end point. - // We would have to use a fallback route otherwise. - if (startPoints.length > 0 && endPoints.length > 0) { + // Check that there is an accessible route point on both sides. + // Otherwise, use fallbackRoute(). + if (startPoints.length > 0 && endPoints.length > 0) { // The set of tentative points to be evaluated, initially containing the start points. + // Rounded to nearest integer for simplicity. var openSet = new SortedSet(); + // Keeps reference to actual points for given elements of the open set. + var points = {}; // Keeps reference to a point that is immediate predecessor of given element. var parents = {}; // Cost from start to a point along best known path. @@ -14534,78 +21542,109 @@ joint.routers.manhattan = (function(g, _, joint, util) { for (var i = 0, n = startPoints.length; i < n; i++) { var point = startPoints[i]; - var key = point.toString(); + var key = getKey(point); openSet.add(key, estimateCost(point, endPoints)); + points[key] = point; costs[key] = 0; } + var previousRouteDirectionAngle = opt.previousDirectionAngle; // undefined for first route + var isPathBeginning = (previousRouteDirectionAngle === undefined); + // directions - var dir, dirChange; - var dirs = opt.directions; - var dirLen = dirs.length; - var loopsRemain = opt.maximumLoops; - var endPointsKeys = util.invoke(endPoints, 'toString'); + var direction, directionChange; + var directions = opt.directions; + getGridOffsets(directions, grid, opt); + + var numDirections = directions.length; + + var endPointsKeys = util.toArray(endPoints).reduce(function(res, endPoint) { + + var key = getKey(endPoint); + res.push(key); + return res; + }, []); // main route finding loop - while (!openSet.isEmpty() && loopsRemain > 0) { + var loopsRemaining = opt.maximumLoops; + while (!openSet.isEmpty() && loopsRemaining > 0) { // remove current from the open list var currentKey = openSet.pop(); - var currentPoint = g.point(currentKey); - var currentDist = costs[currentKey]; - var previousDirAngle = currentDirAngle; - var currentDirAngle = parents[currentKey] - ? getDirectionAngle(parents[currentKey], currentPoint, dirLen) - : opt.previousDirAngle != null ? opt.previousDirAngle : getDirectionAngle(startCenter, currentPoint, dirLen); - - // Check if we reached any endpoint + var currentPoint = points[currentKey]; + var currentParent = parents[currentKey]; + var currentCost = costs[currentKey]; + + var isRouteBeginning = (currentParent === undefined); // undefined for route starts + var isStart = currentPoint.equals(start); // (is source anchor or `from` point) = can leave in any direction + + var previousDirectionAngle; + if (!isRouteBeginning) previousDirectionAngle = getDirectionAngle(currentParent, currentPoint, numDirections, grid, opt); // a vertex on the route + else if (!isPathBeginning) previousDirectionAngle = previousRouteDirectionAngle; // beginning of route on the path + else if (!isStart) previousDirectionAngle = getDirectionAngle(start, currentPoint, numDirections, grid, opt); // beginning of path, start rect point + else previousDirectionAngle = null; // beginning of path, source anchor or `from` point + + // check if we reached any endpoint if (endPointsKeys.indexOf(currentKey) >= 0) { - // We don't want to allow route to enter the end point in opposite direction. - dirChange = getDirectionChange(currentDirAngle, getDirectionAngle(currentPoint, endCenter, dirLen)); - if (currentPoint.equals(endCenter) || dirChange < 180) { - opt.previousDirAngle = currentDirAngle; - return reconstructRoute(parents, currentPoint, startCenter, endCenter); - } + opt.previousDirectionAngle = previousDirectionAngle; + return reconstructRoute(parents, points, currentPoint, start, end, opt); } - // Go over all possible directions and find neighbors. - for (i = 0; i < dirLen; i++) { + // go over all possible directions and find neighbors + for (i = 0; i < numDirections; i++) { + direction = directions[i]; + + var directionAngle = direction.angle; + directionChange = getDirectionChange(previousDirectionAngle, directionAngle); - dir = dirs[i]; - dirChange = getDirectionChange(currentDirAngle, dir.angle); - // if the direction changed rapidly don't use this point - // Note that check is relevant only for points with previousDirAngle i.e. + // if the direction changed rapidly, don't use this point // any direction is allowed for starting points - if (previousDirAngle && dirChange > opt.maxAllowedDirectionChange) { - continue; - } + if (!(isPathBeginning && isStart) && directionChange > opt.maxAllowedDirectionChange) continue; + + var neighborPoint = currentPoint.clone().offset(direction.gridOffsetX, direction.gridOffsetY); + var neighborKey = getKey(neighborPoint); - var neighborPoint = currentPoint.clone().offset(dir.offsetX, dir.offsetY); - var neighborKey = neighborPoint.toString(); // Closed points from the openSet were already evaluated. - if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) { - continue; + if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) continue; + + // We can only enter end points at an acceptable angle. + if (endPointsKeys.indexOf(neighborKey) >= 0) { // neighbor is an end point + round(neighborPoint, opt); // remove rounding errors + + var isNeighborEnd = neighborPoint.equals(end); // (is target anchor or `to` point) = can be entered in any direction + + if (!isNeighborEnd) { + var endDirectionAngle = getDirectionAngle(neighborPoint, end, numDirections, grid, opt); + var endDirectionChange = getDirectionChange(directionAngle, endDirectionAngle); + + if (endDirectionChange > opt.maxAllowedDirectionChange) continue; + } } - // The current direction is ok to proccess. - var costFromStart = currentDist + dir.cost + opt.penalties[dirChange]; + // The current direction is ok. + + var neighborCost = direction.cost; + var neighborPenalty = isStart ? 0 : opt.penalties[directionChange]; // no penalties for start point + var costFromStart = currentCost + neighborCost + neighborPenalty; - if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) { - // neighbor point has not been processed yet or the cost of the path - // from start is lesser than previously calcluated. + if (!openSet.isOpen(neighborKey) || (costFromStart < costs[neighborKey])) { + // neighbor point has not been processed yet + // or the cost of the path from start is lower than previously calculated + + points[neighborKey] = neighborPoint; parents[neighborKey] = currentPoint; costs[neighborKey] = costFromStart; openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints)); } } - loopsRemain--; + loopsRemaining--; } } - // no route found ('to' point wasn't either accessible or finding route took - // way to much calculations) - return opt.fallbackRoute(startCenter, endCenter, opt); + // no route found (`to` point either wasn't accessible or finding route took + // way too much calculation) + return opt.fallbackRoute.call(this, start, end, opt); } // resolve some of the options @@ -14617,33 +21656,35 @@ joint.routers.manhattan = (function(g, _, joint, util) { util.toArray(opt.directions).forEach(function(direction) { - var point1 = g.point(0, 0); - var point2 = g.point(direction.offsetX, direction.offsetY); + var point1 = new g.Point(0, 0); + var point2 = new g.Point(direction.offsetX, direction.offsetY); direction.angle = g.normalizeAngle(point1.theta(point2)); }); } - // initiation of the route finding - function router(vertices, opt) { + // initialization of the route finding + function router(vertices, opt, linkView) { resolveOptions(opt); // enable/disable linkView perpendicular option - this.options.perpendicular = !!opt.perpendicular; + linkView.options.perpendicular = !!opt.perpendicular; + + var sourceBBox = getSourceBBox(linkView, opt); + var targetBBox = getTargetBBox(linkView, opt); - // expand boxes by specific padding - var sourceBBox = g.rect(this.sourceBBox).moveAndExpand(opt.paddingBox); - var targetBBox = g.rect(this.targetBBox).moveAndExpand(opt.paddingBox); + var sourceAnchor = getSourceAnchor(linkView, opt); + //var targetAnchor = getTargetAnchor(linkView, opt); // pathfinding - var map = (new ObstacleMap(opt)).build(this.paper.model, this.model); - var oldVertices = util.toArray(vertices).map(g.point); + var map = (new ObstacleMap(opt)).build(linkView.paper.model, linkView.model); + var oldVertices = util.toArray(vertices).map(g.Point); var newVertices = []; - var tailPoint = sourceBBox.center().snapToGrid(opt.step); + var tailPoint = sourceAnchor; // the origin of first route's grid, does not need snapping - // find a route by concating all partial routes (routes need to go through the vertices) - // startElement -> vertex[1] -> ... -> vertex[n] -> endElement + // find a route by concatenating all partial routes (routes need to pass through vertices) + // source -> vertex[1] -> ... -> vertex[n] -> target for (var i = 0, len = oldVertices.length; i <= len; i++) { var partialRoute = null; @@ -14652,39 +21693,38 @@ joint.routers.manhattan = (function(g, _, joint, util) { var to = oldVertices[i]; if (!to) { + // this is the last iteration + // we ran through all vertices in oldVertices + // 'to' is not a vertex. to = targetBBox; - // 'to' is not a vertex. If the target is a point (i.e. it's not an element), we - // might use dragging route instead of main routing method if that is enabled. - var endingAtPoint = !this.model.get('source').id || !this.model.get('target').id; + // If the target is a point (i.e. it's not an element), we + // should use dragging route instead of main routing method if it has been provided. + var isEndingAtPoint = !linkView.model.get('source').id || !linkView.model.get('target').id; - if (endingAtPoint && util.isFunction(opt.draggingRoute)) { - // Make sure we passing points only (not rects). - var dragFrom = from instanceof g.rect ? from.center() : from; - partialRoute = opt.draggingRoute(dragFrom, to.origin(), opt); + if (isEndingAtPoint && util.isFunction(opt.draggingRoute)) { + // Make sure we are passing points only (not rects). + var dragFrom = (from === sourceBBox) ? sourceAnchor : from; + var dragTo = to.origin(); + + partialRoute = opt.draggingRoute.call(linkView, dragFrom, dragTo, opt); } } // if partial route has not been calculated yet use the main routing method to find one - partialRoute = partialRoute || findRoute(from, to, map, opt); + partialRoute = partialRoute || findRoute.call(linkView, from, to, map, opt); - if (partialRoute === null) { - // The partial route could not be found. - // use orthogonal (do not avoid elements) route instead. - if (!util.isFunction(joint.routers.orthogonal)) { - throw new Error('Manhattan requires the orthogonal router.'); - } - return joint.routers.orthogonal(vertices, opt, this); + if (partialRoute === null) { // the partial route cannot be found + return opt.fallbackRouter(vertices, opt, linkView); } var leadPoint = partialRoute[0]; - if (leadPoint && leadPoint.equals(tailPoint)) { - // remove the first point if the previous partial route had the same point as last - partialRoute.shift(); - } + // remove the first point if the previous partial route had the same point as last + if (leadPoint && leadPoint.equals(tailPoint)) partialRoute.shift(); + // save tailPoint for next iteration tailPoint = partialRoute[partialRoute.length - 1] || tailPoint; Array.prototype.push.apply(newVertices, partialRoute); @@ -14696,49 +21736,54 @@ joint.routers.manhattan = (function(g, _, joint, util) { // public function return function(vertices, opt, linkView) { - return router.call(linkView, vertices, util.assign({}, config, opt)); + return router(vertices, util.assign({}, config, opt), linkView); }; })(g, _, joint, joint.util); joint.routers.metro = (function(util) { - if (!util.isFunction(joint.routers.manhattan)) { + var config = { - throw new Error('Metro requires the manhattan router.'); - } + maxAllowedDirectionChange: 45, - var config = { + // cost of a diagonal step + diagonalCost: function() { - // cost of a diagonal step (calculated if not defined). - diagonalCost: null, + var step = this.step; + return Math.ceil(Math.sqrt(step * step << 1)); + }, // an array of directions to find next points on the route + // different from start/end directions directions: function() { var step = this.step; - var diagonalCost = this.diagonalCost || Math.ceil(Math.sqrt(step * step << 1)); + var cost = this.cost(); + var diagonalCost = this.diagonalCost(); return [ - { offsetX: step , offsetY: 0 , cost: step }, + { offsetX: step , offsetY: 0 , cost: cost }, { offsetX: step , offsetY: step , cost: diagonalCost }, - { offsetX: 0 , offsetY: step , cost: step }, + { offsetX: 0 , offsetY: step , cost: cost }, { offsetX: -step , offsetY: step , cost: diagonalCost }, - { offsetX: -step , offsetY: 0 , cost: step }, + { offsetX: -step , offsetY: 0 , cost: cost }, { offsetX: -step , offsetY: -step , cost: diagonalCost }, - { offsetX: 0 , offsetY: -step , cost: step }, + { offsetX: 0 , offsetY: -step , cost: cost }, { offsetX: step , offsetY: -step , cost: diagonalCost } ]; }, - maxAllowedDirectionChange: 45, - // a simple route used in situations, when main routing method fails - // (exceed loops, inaccessible). - fallbackRoute: function(from, to, opts) { + + // a simple route used in situations when main routing method fails + // (exceed max number of loop iterations, inaccessible) + fallbackRoute: function(from, to, opt) { // Find a route which breaks by 45 degrees ignoring all obstacles. var theta = from.theta(to); + var route = []; + var a = { x: to.x, y: from.y }; var b = { x: from.x, y: to.y }; @@ -14749,25 +21794,40 @@ joint.routers.metro = (function(util) { } var p1 = (theta % 90) < 45 ? a : b; - - var l1 = g.line(from, p1); + var l1 = new g.Line(from, p1); var alpha = 90 * Math.ceil(theta / 90); - var p2 = g.point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); + var p2 = g.Point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); + var l2 = new g.Line(to, p2); + + var intersectionPoint = l1.intersection(l2); + var point = intersectionPoint ? intersectionPoint : to; + + var directionFrom = intersectionPoint ? point : from; + + var quadrant = 360 / opt.directions.length; + var angleTheta = directionFrom.theta(to); + var normalizedAngle = g.normalizeAngle(angleTheta + (quadrant / 2)); + var directionAngle = quadrant * Math.floor(normalizedAngle / quadrant); - var l2 = g.line(to, p2); + opt.previousDirectionAngle = directionAngle; - var point = l1.intersection(l2); + if (point) route.push(point.round()); + route.push(to); - return point ? [point.round(), to] : [to]; + return route; } }; // public function - return function(vertices, opts, linkView) { + return function(vertices, opt, linkView) { + + if (!util.isFunction(joint.routers.manhattan)) { + throw new Error('Metro requires the manhattan router.'); + } - return joint.routers.manhattan(vertices, util.assign({}, config, opts), linkView); + return joint.routers.manhattan(vertices, util.assign({}, config, opt), linkView); }; })(joint.util); @@ -14841,7 +21901,7 @@ joint.routers.oneSide = function(vertices, opt, linkView) { joint.routers.orthogonal = (function(util) { // bearing -> opposite bearing - var opposite = { + var opposites = { N: 'S', S: 'N', E: 'W', @@ -14858,105 +21918,126 @@ joint.routers.orthogonal = (function(util) { // HELPERS // - // simple bearing method (calculates only orthogonal cardinals) - function bearing(from, to) { - if (from.x == to.x) return from.y > to.y ? 'N' : 'S'; - if (from.y == to.y) return from.x > to.x ? 'W' : 'E'; - return null; + // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained + // in the given box + function freeJoin(p1, p2, bbox) { + + var p = new g.Point(p1.x, p2.y); + if (bbox.containsPoint(p)) p = new g.Point(p2.x, p1.y); + // kept for reference + // if (bbox.containsPoint(p)) p = null; + + return p; } // returns either width or height of a bbox based on the given bearing - function boxSize(bbox, brng) { - return bbox[brng == 'W' || brng == 'E' ? 'width' : 'height']; + function getBBoxSize(bbox, bearing) { + + return bbox[(bearing === 'W' || bearing === 'E') ? 'width' : 'height']; } - // expands a box by specific value - function expand(bbox, val) { - return g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val }); + // simple bearing method (calculates only orthogonal cardinals) + function getBearing(from, to) { + + if (from.x === to.x) return (from.y > to.y) ? 'N' : 'S'; + if (from.y === to.y) return (from.x > to.x) ? 'W' : 'E'; + return null; } // transform point to a rect - function pointBox(p) { - return g.rect(p.x, p.y, 0, 0); + function getPointBox(p) { + + return new g.Rect(p.x, p.y, 0, 0); } - // returns a minimal rect which covers the given boxes - function boundary(bbox1, bbox2) { + // return source bbox + function getSourceBBox(linkView, opt) { + + var padding = (opt && opt.elementPadding) || 20; + return linkView.sourceBBox.clone().inflate(padding); + } - var x1 = Math.min(bbox1.x, bbox2.x); - var y1 = Math.min(bbox1.y, bbox2.y); - var x2 = Math.max(bbox1.x + bbox1.width, bbox2.x + bbox2.width); - var y2 = Math.max(bbox1.y + bbox1.height, bbox2.y + bbox2.height); + // return target bbox + function getTargetBBox(linkView, opt) { - return g.rect(x1, y1, x2 - x1, y2 - y1); + var padding = (opt && opt.elementPadding) || 20; + return linkView.targetBBox.clone().inflate(padding); } - // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained - // in the given box - function freeJoin(p1, p2, bbox) { + // return source anchor + function getSourceAnchor(linkView, opt) { - var p = g.point(p1.x, p2.y); - if (bbox.containsPoint(p)) p = g.point(p2.x, p1.y); - // kept for reference - // if (bbox.containsPoint(p)) p = null; - return p; + if (linkView.sourceAnchor) return linkView.sourceAnchor; + + // fallback: center of bbox + var sourceBBox = getSourceBBox(linkView, opt); + return sourceBBox.center(); + } + + // return target anchor + function getTargetAnchor(linkView, opt) { + + if (linkView.targetAnchor) return linkView.targetAnchor; + + // fallback: center of bbox + var targetBBox = getTargetBBox(linkView, opt); + return targetBBox.center(); // default } // PARTIAL ROUTERS // - function vertexVertex(from, to, brng) { + function vertexVertex(from, to, bearing) { - var p1 = g.point(from.x, to.y); - var p2 = g.point(to.x, from.y); - var d1 = bearing(from, p1); - var d2 = bearing(from, p2); - var xBrng = opposite[brng]; + var p1 = new g.Point(from.x, to.y); + var p2 = new g.Point(to.x, from.y); + var d1 = getBearing(from, p1); + var d2 = getBearing(from, p2); + var opposite = opposites[bearing]; - var p = (d1 == brng || (d1 != xBrng && (d2 == xBrng || d2 != brng))) ? p1 : p2; + var p = (d1 === bearing || (d1 !== opposite && (d2 === opposite || d2 !== bearing))) ? p1 : p2; - return { points: [p], direction: bearing(p, to) }; + return { points: [p], direction: getBearing(p, to) }; } function elementVertex(from, to, fromBBox) { var p = freeJoin(from, to, fromBBox); - return { points: [p], direction: bearing(p, to) }; + return { points: [p], direction: getBearing(p, to) }; } - function vertexElement(from, to, toBBox, brng) { + function vertexElement(from, to, toBBox, bearing) { var route = {}; - var pts = [g.point(from.x, to.y), g.point(to.x, from.y)]; - var freePts = pts.filter(function(pt) { return !toBBox.containsPoint(pt); }); - var freeBrngPts = freePts.filter(function(pt) { return bearing(pt, from) != brng; }); + var points = [new g.Point(from.x, to.y), new g.Point(to.x, from.y)]; + var freePoints = points.filter(function(pt) { return !toBBox.containsPoint(pt); }); + var freeBearingPoints = freePoints.filter(function(pt) { return getBearing(pt, from) !== bearing; }); var p; - if (freeBrngPts.length > 0) { + if (freeBearingPoints.length > 0) { + // Try to pick a point which bears the same direction as the previous segment. - // try to pick a point which bears the same direction as the previous segment - p = freeBrngPts.filter(function(pt) { return bearing(from, pt) == brng; }).pop(); - p = p || freeBrngPts[0]; + p = freeBearingPoints.filter(function(pt) { return getBearing(from, pt) === bearing; }).pop(); + p = p || freeBearingPoints[0]; route.points = [p]; - route.direction = bearing(p, to); + route.direction = getBearing(p, to); } else { - // Here we found only points which are either contained in the element or they would create // a link segment going in opposite direction from the previous one. // We take the point inside element and move it outside the element in the direction the // route is going. Now we can join this point with the current end (using freeJoin). - p = util.difference(pts, freePts)[0]; + p = util.difference(points, freePoints)[0]; - var p2 = g.point(to).move(p, -boxSize(toBBox, brng) / 2); + var p2 = (new g.Point(to)).move(p, -getBBoxSize(toBBox, bearing) / 2); var p1 = freeJoin(p2, from, toBBox); route.points = [p1, p2]; - route.direction = bearing(p2, to); + route.direction = getBearing(p2, to); } return route; @@ -14974,9 +22055,9 @@ joint.routers.orthogonal = (function(util) { if (toBBox.containsPoint(p2)) { - var fromBorder = g.point(from).move(p2, -boxSize(fromBBox, bearing(from, p2)) / 2); - var toBorder = g.point(to).move(p1, -boxSize(toBBox, bearing(to, p1)) / 2); - var mid = g.line(fromBorder, toBorder).midpoint(); + var fromBorder = (new g.Point(from)).move(p2, -getBBoxSize(fromBBox, getBearing(from, p2)) / 2); + var toBorder = (new g.Point(to)).move(p1, -getBBoxSize(toBBox, getBearing(to, p1)) / 2); + var mid = (new g.Line(fromBorder, toBorder)).midpoint(); var startRoute = elementVertex(from, mid, fromBBox); var endRoute = vertexVertex(mid, to, startRoute.direction); @@ -14989,79 +22070,91 @@ joint.routers.orthogonal = (function(util) { return route; } - // Finds route for situations where one of end is inside the other. - // Typically the route is conduct outside the outer element first and - // let go back to the inner element. - function insideElement(from, to, fromBBox, toBBox, brng) { + // Finds route for situations where one element is inside the other. + // Typically the route is directed outside the outer element first and + // then back towards the inner element. + function insideElement(from, to, fromBBox, toBBox, bearing) { var route = {}; - var bndry = expand(boundary(fromBBox, toBBox), 1); + var boundary = fromBBox.union(toBBox).inflate(1); // start from the point which is closer to the boundary - var reversed = bndry.center().distance(to) > bndry.center().distance(from); + var reversed = boundary.center().distance(to) > boundary.center().distance(from); var start = reversed ? to : from; var end = reversed ? from : to; var p1, p2, p3; - if (brng) { + if (bearing) { // Points on circle with radius equals 'W + H` are always outside the rectangle // with width W and height H if the center of that circle is the center of that rectangle. - p1 = g.point.fromPolar(bndry.width + bndry.height, radians[brng], start); - p1 = bndry.pointNearestToPoint(p1).move(p1, -1); + p1 = g.Point.fromPolar(boundary.width + boundary.height, radians[bearing], start); + p1 = boundary.pointNearestToPoint(p1).move(p1, -1); + } else { - p1 = bndry.pointNearestToPoint(start).move(start, 1); + p1 = boundary.pointNearestToPoint(start).move(start, 1); } - p2 = freeJoin(p1, end, bndry); + p2 = freeJoin(p1, end, boundary); if (p1.round().equals(p2.round())) { - p2 = g.point.fromPolar(bndry.width + bndry.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); - p2 = bndry.pointNearestToPoint(p2).move(end, 1).round(); - p3 = freeJoin(p1, p2, bndry); + p2 = g.Point.fromPolar(boundary.width + boundary.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); + p2 = boundary.pointNearestToPoint(p2).move(end, 1).round(); + p3 = freeJoin(p1, p2, boundary); route.points = reversed ? [p2, p3, p1] : [p1, p3, p2]; + } else { route.points = reversed ? [p2, p1] : [p1, p2]; } - route.direction = reversed ? bearing(p1, to) : bearing(p2, to); + route.direction = reversed ? getBearing(p1, to) : getBearing(p2, to); return route; } // MAIN ROUTER // - // Return points that one needs to draw a connection through in order to have a orthogonal link + // Return points through which a connection needs to be drawn in order to obtain an orthogonal link // routing from source to target going through `vertices`. - function findOrthogonalRoute(vertices, opt, linkView) { + function router(vertices, opt, linkView) { var padding = opt.elementPadding || 20; - var orthogonalVertices = []; - var sourceBBox = expand(linkView.sourceBBox, padding); - var targetBBox = expand(linkView.targetBBox, padding); + var sourceBBox = getSourceBBox(linkView, opt); + var targetBBox = getTargetBBox(linkView, opt); + + var sourceAnchor = getSourceAnchor(linkView, opt); + var targetAnchor = getTargetAnchor(linkView, opt); + + // if anchor lies outside of bbox, the bbox expands to include it + sourceBBox = sourceBBox.union(getPointBox(sourceAnchor)); + targetBBox = targetBBox.union(getPointBox(targetAnchor)); - vertices = util.toArray(vertices).map(g.point); - vertices.unshift(sourceBBox.center()); - vertices.push(targetBBox.center()); + vertices = util.toArray(vertices).map(g.Point); + vertices.unshift(sourceAnchor); + vertices.push(targetAnchor); - var brng; + var bearing; // bearing of previous route segment + var orthogonalVertices = []; // the array of found orthogonal vertices to be returned for (var i = 0, max = vertices.length - 1; i < max; i++) { var route = null; + var from = vertices[i]; var to = vertices[i + 1]; - var isOrthogonal = !!bearing(from, to); - if (i == 0) { + var isOrthogonal = !!getBearing(from, to); + + if (i === 0) { // source - if (i + 1 == max) { // route source -> target + if (i + 1 === max) { // route source -> target - // Expand one of elements by 1px so we detect also situations when they - // are positioned one next other with no gap between. - if (sourceBBox.intersect(expand(targetBBox, 1))) { + // Expand one of the elements by 1px to detect situations when the two + // elements are positioned next to each other with no gap in between. + if (sourceBBox.intersect(targetBBox.clone().inflate(1))) { route = insideElement(from, to, sourceBBox, targetBBox); + } else if (!isOrthogonal) { route = elementElement(from, to, sourceBBox, targetBBox); } @@ -15069,34 +22162,42 @@ joint.routers.orthogonal = (function(util) { } else { // route source -> vertex if (sourceBBox.containsPoint(to)) { - route = insideElement(from, to, sourceBBox, expand(pointBox(to), padding)); + route = insideElement(from, to, sourceBBox, getPointBox(to).inflate(padding)); + } else if (!isOrthogonal) { route = elementVertex(from, to, sourceBBox); } } - } else if (i + 1 == max) { // route vertex -> target + } else if (i + 1 === max) { // route vertex -> target - var orthogonalLoop = isOrthogonal && bearing(to, from) == brng; + // prevent overlaps with previous line segment + var isOrthogonalLoop = isOrthogonal && getBearing(to, from) === bearing; + + if (targetBBox.containsPoint(from) || isOrthogonalLoop) { + route = insideElement(from, to, getPointBox(from).inflate(padding), targetBBox, bearing); - if (targetBBox.containsPoint(from) || orthogonalLoop) { - route = insideElement(from, to, expand(pointBox(from), padding), targetBBox, brng); } else if (!isOrthogonal) { - route = vertexElement(from, to, targetBBox, brng); + route = vertexElement(from, to, targetBBox, bearing); } } else if (!isOrthogonal) { // route vertex -> vertex - route = vertexVertex(from, to, brng); + route = vertexVertex(from, to, bearing); } + // applicable to all routes: + + // set bearing for next iteration if (route) { Array.prototype.push.apply(orthogonalVertices, route.points); - brng = route.direction; + bearing = route.direction; + } else { // orthogonal route and not looped - brng = bearing(from, to); + bearing = getBearing(from, to); } + // push `to` point to identified orthogonal vertices array if (i + 1 < max) { orthogonalVertices.push(to); } @@ -15105,81 +22206,116 @@ joint.routers.orthogonal = (function(util) { return orthogonalVertices; } - return findOrthogonalRoute; + return router; })(joint.util); -joint.connectors.normal = function(sourcePoint, targetPoint, vertices) { +joint.connectors.normal = function(sourcePoint, targetPoint, route, opt) { - // Construct the `d` attribute of the `` element. - var d = ['M', sourcePoint.x, sourcePoint.y]; + var raw = opt && opt.raw; + var points = [sourcePoint].concat(route).concat([targetPoint]); - joint.util.toArray(vertices).forEach(function(vertex) { + var polyline = new g.Polyline(points); + var path = new g.Path(polyline); - d.push(vertex.x, vertex.y); - }); + return (raw) ? path : path.serialize(); +}; - d.push(targetPoint.x, targetPoint.y); +joint.connectors.rounded = function(sourcePoint, targetPoint, route, opt) { - return d.join(' '); -}; + opt || (opt = {}); -joint.connectors.rounded = function(sourcePoint, targetPoint, vertices, opts) { + var offset = opt.radius || 10; + var raw = opt.raw; + var path = new g.Path(); + var segment; - opts = opts || {}; + segment = g.Path.createSegment('M', sourcePoint); + path.appendSegment(segment); - var offset = opts.radius || 10; + var _13 = 1 / 3; + var _23 = 2 / 3; - var c1, c2, d1, d2, prev, next; + var curr; + var prev, next; + var prevDistance, nextDistance; + var startMove, endMove; + var roundedStart, roundedEnd; + var control1, control2; - // Construct the `d` attribute of the `` element. - var d = ['M', sourcePoint.x, sourcePoint.y]; + for (var index = 0, n = route.length; index < n; index++) { - joint.util.toArray(vertices).forEach(function(vertex, index) { + curr = new g.Point(route[index]); - // the closest vertices - prev = vertices[index - 1] || sourcePoint; - next = vertices[index + 1] || targetPoint; + prev = route[index - 1] || sourcePoint; + next = route[index + 1] || targetPoint; - // a half distance to the closest vertex - d1 = d2 || g.point(vertex).distance(prev) / 2; - d2 = g.point(vertex).distance(next) / 2; + prevDistance = nextDistance || (curr.distance(prev) / 2); + nextDistance = curr.distance(next) / 2; - // control points - c1 = g.point(vertex).move(prev, -Math.min(offset, d1)).round(); - c2 = g.point(vertex).move(next, -Math.min(offset, d2)).round(); + startMove = -Math.min(offset, prevDistance); + endMove = -Math.min(offset, nextDistance); - d.push(c1.x, c1.y, 'S', vertex.x, vertex.y, c2.x, c2.y, 'L'); - }); + roundedStart = curr.clone().move(prev, startMove).round(); + roundedEnd = curr.clone().move(next, endMove).round(); + + control1 = new g.Point((_13 * roundedStart.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedStart.y)); + control2 = new g.Point((_13 * roundedEnd.x) + (_23 * curr.x), (_23 * curr.y) + (_13 * roundedEnd.y)); - d.push(targetPoint.x, targetPoint.y); + segment = g.Path.createSegment('L', roundedStart); + path.appendSegment(segment); - return d.join(' '); + segment = g.Path.createSegment('C', control1, control2, roundedEnd); + path.appendSegment(segment); + } + + segment = g.Path.createSegment('L', targetPoint); + path.appendSegment(segment); + + return (raw) ? path : path.serialize(); }; -joint.connectors.smooth = function(sourcePoint, targetPoint, vertices) { +joint.connectors.smooth = function(sourcePoint, targetPoint, route, opt) { + + var raw = opt && opt.raw; + var path; - var d; + if (route && route.length !== 0) { - if (vertices.length) { + var points = [sourcePoint].concat(route).concat([targetPoint]); + var curves = g.Curve.throughPoints(points); - d = g.bezier.curveThroughPoints([sourcePoint].concat(vertices).concat([targetPoint])); + path = new g.Path(curves); } else { - // if we have no vertices use a default cubic bezier curve, cubic bezier requires - // two control points. The two control points are both defined with X as mid way - // between the source and target points. SourceControlPoint Y is equal to sourcePoint Y - // and targetControlPointY being equal to targetPointY. - var controlPointX = (sourcePoint.x + targetPoint.x) / 2; - - d = [ - 'M', sourcePoint.x, sourcePoint.y, - 'C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, - targetPoint.x, targetPoint.y - ]; + // if we have no route, use a default cubic bezier curve + // cubic bezier requires two control points + // the control points have `x` midway between source and target + // this produces an S-like curve + + path = new g.Path(); + + var segment; + + segment = g.Path.createSegment('M', sourcePoint); + path.appendSegment(segment); + + if ((Math.abs(sourcePoint.x - targetPoint.x)) >= (Math.abs(sourcePoint.y - targetPoint.y))) { + var controlPointX = (sourcePoint.x + targetPoint.x) / 2; + + segment = g.Path.createSegment('C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, targetPoint.x, targetPoint.y); + path.appendSegment(segment); + + } else { + var controlPointY = (sourcePoint.y + targetPoint.y) / 2; + + segment = g.Path.createSegment('C', sourcePoint.x, controlPointY, targetPoint.x, controlPointY, targetPoint.x, targetPoint.y); + path.appendSegment(segment); + + } } - return d.join(' '); + return (raw) ? path : path.serialize(); }; joint.connectors.jumpover = (function(_, g, util) { @@ -15188,6 +22324,7 @@ joint.connectors.jumpover = (function(_, g, util) { var JUMP_SIZE = 5; // available jump types + // first one taken as default var JUMP_TYPES = ['arc', 'gap', 'cubic']; // takes care of math. error for case when jump is too close to end of line @@ -15197,15 +22334,15 @@ joint.connectors.jumpover = (function(_, g, util) { var IGNORED_CONNECTORS = ['smooth']; /** - * Transform start/end and vertices into series of lines + * Transform start/end and route into series of lines * @param {g.point} sourcePoint start point * @param {g.point} targetPoint end point - * @param {g.point[]} vertices optional list of vertices + * @param {g.point[]} route optional list of route * @return {g.line[]} [description] */ - function createLines(sourcePoint, targetPoint, vertices) { + function createLines(sourcePoint, targetPoint, route) { // make a flattened array of all points - var points = [].concat(sourcePoint, vertices, targetPoint); + var points = [].concat(sourcePoint, route, targetPoint); return points.reduce(function(resultLines, point, idx) { // if there is a next point, make a line with it var nextPoint = points[idx + 1]; @@ -15350,60 +22487,102 @@ joint.connectors.jumpover = (function(_, g, util) { * @return {string} */ function buildPath(lines, jumpSize, jumpType) { + + var path = new g.Path(); + var segment; + // first move to the start of a first line - var start = ['M', lines[0].start.x, lines[0].start.y]; + segment = g.Path.createSegment('M', lines[0].start); + path.appendSegment(segment); // make a paths from lines - var paths = util.toArray(lines).reduce(function(res, line) { + joint.util.toArray(lines).forEach(function(line, index) { + if (line.isJump) { - var diff; - if (jumpType === 'arc') { - diff = line.start.difference(line.end); + var angle, diff; + + var control1, control2; + + if (jumpType === 'arc') { // approximates semicircle with 2 curves + angle = -90; // determine rotation of arc based on difference between points - var xAxisRotate = Number(diff.x < 0 && diff.y < 0); - // for a jump line we create an arc instead - res.push('A', jumpSize, jumpSize, 0, 0, xAxisRotate, line.end.x, line.end.y); + diff = line.start.difference(line.end); + // make sure the arc always points up (or right) + var xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0)); + if (xAxisRotate) angle += 180; + + var midpoint = line.midpoint(); + var centerLine = new g.Line(midpoint, line.end).rotate(midpoint, angle); + + var halfLine; + + // first half + halfLine = new g.Line(line.start, midpoint); + + control1 = halfLine.pointAt(2 / 3).rotate(line.start, angle); + control2 = centerLine.pointAt(1 / 3).rotate(centerLine.end, -angle); + + segment = g.Path.createSegment('C', control1, control2, centerLine.end); + path.appendSegment(segment); + + // second half + halfLine = new g.Line(midpoint, line.end); + + control1 = centerLine.pointAt(1 / 3).rotate(centerLine.end, angle); + control2 = halfLine.pointAt(1 / 3).rotate(line.end, -angle); + + segment = g.Path.createSegment('C', control1, control2, line.end); + path.appendSegment(segment); + } else if (jumpType === 'gap') { - res = res.concat(['M', line.end.x, line.end.y]); - } else if (jumpType === 'cubic') { - diff = line.start.difference(line.end); - var angle = line.start.theta(line.end); + segment = g.Path.createSegment('M', line.end); + path.appendSegment(segment); + + } else if (jumpType === 'cubic') { // approximates semicircle with 1 curve + angle = line.start.theta(line.end); + var xOffset = jumpSize * 0.6; var yOffset = jumpSize * 1.35; - // determine rotation of curve based on difference between points - if (diff.x < 0 && diff.y < 0) { - yOffset *= -1; - } - var controlStartPoint = g.point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); - var controlEndPoint = g.point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); - // create a cubic bezier curve - res.push('C', controlStartPoint.x, controlStartPoint.y, controlEndPoint.x, controlEndPoint.y, line.end.x, line.end.y); + + // determine rotation of arc based on difference between points + diff = line.start.difference(line.end); + // make sure the arc always points up (or right) + xAxisRotate = Number((diff.x < 0) || (diff.x === 0 && diff.y < 0)); + if (xAxisRotate) yOffset *= -1; + + control1 = g.Point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); + control2 = g.Point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); + + segment = g.Path.createSegment('C', control1, control2, line.end); + path.appendSegment(segment); } + } else { - res.push('L', line.end.x, line.end.y); + segment = g.Path.createSegment('L', line.end); + path.appendSegment(segment); } - return res; - }, start); + }); - return paths.join(' '); + return path; } /** * Actual connector function that will be run on every update. * @param {g.point} sourcePoint start point of this link * @param {g.point} targetPoint end point of this link - * @param {g.point[]} vertices of this link - * @param {object} opts options + * @param {g.point[]} route of this link + * @param {object} opt options * @property {number} size optional size of a jump arc * @return {string} created `D` attribute of SVG path */ - return function(sourcePoint, targetPoint, vertices, opts) { // eslint-disable-line max-params + return function(sourcePoint, targetPoint, route, opt) { // eslint-disable-line max-params setupUpdating(this); - var jumpSize = opts.size || JUMP_SIZE; - var jumpType = opts.jump && ('' + opts.jump).toLowerCase(); - var ignoreConnectors = opts.ignoreConnectors || IGNORED_CONNECTORS; + var raw = opt.raw; + var jumpSize = opt.size || JUMP_SIZE; + var jumpType = opt.jump && ('' + opt.jump).toLowerCase(); + var ignoreConnectors = opt.ignoreConnectors || IGNORED_CONNECTORS; // grab the first jump type as a default if specified one is invalid if (JUMP_TYPES.indexOf(jumpType) === -1) { @@ -15417,7 +22596,7 @@ joint.connectors.jumpover = (function(_, g, util) { // there is just one link, draw it directly if (allLinks.length === 1) { return buildPath( - createLines(sourcePoint, targetPoint, vertices), + createLines(sourcePoint, targetPoint, route), jumpSize, jumpType ); } @@ -15452,7 +22631,7 @@ joint.connectors.jumpover = (function(_, g, util) { var thisLines = createLines( sourcePoint, targetPoint, - vertices + route ); // create lines for all other links @@ -15498,8 +22677,8 @@ joint.connectors.jumpover = (function(_, g, util) { return resultLines; }, []); - - return buildPath(jumpingLines, jumpSize, jumpType); + var path = buildPath(jumpingLines, jumpSize, jumpType); + return (raw) ? path : path.serialize(); }; }(_, g, joint.util)); @@ -15947,147 +23126,1435 @@ joint.highlighters.addClass = { } }; -joint.highlighters.opacity = { +joint.highlighters.opacity = { + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + */ + highlight: function(cellView, magnetEl) { + + V(magnetEl).addClass(joint.util.addClassNamePrefix('highlight-opacity')); + }, + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + */ + unhighlight: function(cellView, magnetEl) { + + V(magnetEl).removeClass(joint.util.addClassNamePrefix('highlight-opacity')); + } +}; + +joint.highlighters.stroke = { + + defaultOptions: { + padding: 3, + rx: 0, + ry: 0, + attrs: { + 'stroke-width': 3, + stroke: '#FEB663' + } + }, + + _views: {}, + + getHighlighterId: function(magnetEl, opt) { + + return magnetEl.id + JSON.stringify(opt); + }, + + removeHighlighter: function(id) { + if (this._views[id]) { + this._views[id].remove(); + this._views[id] = null; + } + }, + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt + */ + highlight: function(cellView, magnetEl, opt) { + + var id = this.getHighlighterId(magnetEl, opt); + + // Only highlight once. + if (this._views[id]) return; + + var options = joint.util.defaults(opt || {}, this.defaultOptions); + + var magnetVel = V(magnetEl); + var magnetBBox; + + try { + + var pathData = magnetVel.convertToPathData(); + + } catch (error) { + + // Failed to get path data from magnet element. + // Draw a rectangle around the entire cell view instead. + magnetBBox = magnetVel.bbox(true/* without transforms */); + pathData = V.rectToPath(joint.util.assign({}, options, magnetBBox)); + } + + var highlightVel = V('path').attr({ + d: pathData, + 'pointer-events': 'none', + 'vector-effect': 'non-scaling-stroke', + 'fill': 'none' + }).attr(options.attrs); + + var highlightMatrix = magnetVel.getTransformToElement(cellView.el); + + // Add padding to the highlight element. + var padding = options.padding; + if (padding) { + + magnetBBox || (magnetBBox = magnetVel.bbox(true)); + + var cx = magnetBBox.x + (magnetBBox.width / 2); + var cy = magnetBBox.y + (magnetBBox.height / 2); + + magnetBBox = V.transformRect(magnetBBox, highlightMatrix); + + var width = Math.max(magnetBBox.width, 1); + var height = Math.max(magnetBBox.height, 1); + var sx = (width + padding) / width; + var sy = (height + padding) / height; + + var paddingMatrix = V.createSVGMatrix({ + a: sx, + b: 0, + c: 0, + d: sy, + e: cx - sx * cx, + f: cy - sy * cy + }); + + highlightMatrix = highlightMatrix.multiply(paddingMatrix); + } + + highlightVel.transform(highlightMatrix); + + // joint.mvc.View will handle the theme class name and joint class name prefix. + var highlightView = this._views[id] = new joint.mvc.View({ + svgElement: true, + className: 'highlight-stroke', + el: highlightVel.node + }); + + // Remove the highlight view when the cell is removed from the graph. + var removeHandler = this.removeHighlighter.bind(this, id); + var cell = cellView.model; + highlightView.listenTo(cell, 'remove', removeHandler); + highlightView.listenTo(cell.graph, 'reset', removeHandler); + + cellView.vel.append(highlightVel); + }, + + /** + * @param {joint.dia.CellView} cellView + * @param {Element} magnetEl + * @param {object=} opt + */ + unhighlight: function(cellView, magnetEl, opt) { + + this.removeHighlighter(this.getHighlighterId(magnetEl, opt)); + } +}; + +(function(joint, util) { + + function bboxWrapper(method) { + + return function(view, magnet, ref, opt) { + + var rotate = !!opt.rotate; + var bbox = (rotate) ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet); + var anchor = bbox[method](); + + var dx = opt.dx; + if (dx) { + var dxPercentage = util.isPercentage(dx); + dx = parseFloat(dx); + if (isFinite(dx)) { + if (dxPercentage) { + dx /= 100; + dx *= bbox.width; + } + anchor.x += dx; + } + } + + var dy = opt.dy; + if (dy) { + var dyPercentage = util.isPercentage(dy); + dy = parseFloat(dy); + if (isFinite(dy)) { + if (dyPercentage) { + dy /= 100; + dy *= bbox.height; + } + anchor.y += dy; + } + } + + return (rotate) ? anchor.rotate(view.model.getBBox().center(), -view.model.angle()) : anchor; + } + } + + function resolveRefAsBBoxCenter(fn) { + + return function(view, magnet, ref, opt) { + + if (ref instanceof Element) { + var refView = this.paper.findView(ref); + var refPoint = refView.getNodeBBox(ref).center(); + + return fn.call(this, view, magnet, refPoint, opt) + } + + return fn.apply(this, arguments); + } + } + + function perpendicular(view, magnet, refPoint, opt) { + + var angle = view.model.angle(); + var bbox = view.getNodeBBox(magnet); + var anchor = bbox.center(); + var topLeft = bbox.origin(); + var bottomRight = bbox.corner(); + + var padding = opt.padding + if (!isFinite(padding)) padding = 0; + + if ((topLeft.y + padding) <= refPoint.y && refPoint.y <= (bottomRight.y - padding)) { + var dy = (refPoint.y - anchor.y); + anchor.x += (angle === 0 || angle === 180) ? 0 : dy * 1 / Math.tan(g.toRad(angle)); + anchor.y += dy; + } else if ((topLeft.x + padding) <= refPoint.x && refPoint.x <= (bottomRight.x - padding)) { + var dx = (refPoint.x - anchor.x); + anchor.y += (angle === 90 || angle === 270) ? 0 : dx * Math.tan(g.toRad(angle)); + anchor.x += dx; + } + + return anchor; + } + + function midSide(view, magnet, refPoint, opt) { + + var rotate = !!opt.rotate; + var bbox, angle, center; + if (rotate) { + bbox = view.getNodeUnrotatedBBox(magnet); + center = view.model.getBBox().center(); + angle = view.model.angle(); + } else { + bbox = view.getNodeBBox(magnet); + } + + var padding = opt.padding; + if (isFinite(padding)) bbox.inflate(padding); + + if (rotate) refPoint.rotate(center, angle); + + var side = bbox.sideNearestToPoint(refPoint); + var anchor; + switch (side) { + case 'left': anchor = bbox.leftMiddle(); break; + case 'right': anchor = bbox.rightMiddle(); break; + case 'top': anchor = bbox.topMiddle(); break; + case 'bottom': anchor = bbox.bottomMiddle(); break; + } + + return (rotate) ? anchor.rotate(center, -angle) : anchor; + } + + // Can find anchor from model, when there is no selector or the link end + // is connected to a port + function modelCenter(view, magnet) { + + var model = view.model; + var bbox = model.getBBox(); + var center = bbox.center(); + var angle = model.angle(); + + var portId = view.findAttribute('port', magnet); + if (portId) { + portGroup = model.portProp(portId, 'group'); + var portsPositions = model.getPortsPositions(portGroup); + var anchor = new g.Point(portsPositions[portId]).offset(bbox.origin()); + anchor.rotate(center, -angle); + return anchor; + } + + return center; + } + + joint.anchors = { + center: bboxWrapper('center'), + top: bboxWrapper('topMiddle'), + bottom: bboxWrapper('bottomMiddle'), + left: bboxWrapper('leftMiddle'), + right: bboxWrapper('rightMiddle'), + topLeft: bboxWrapper('origin'), + topRight: bboxWrapper('topRight'), + bottomLeft: bboxWrapper('bottomLeft'), + bottomRight: bboxWrapper('corner'), + perpendicular: resolveRefAsBBoxCenter(perpendicular), + midSide: resolveRefAsBBoxCenter(midSide), + modelCenter: modelCenter + } + +})(joint, joint.util); + +(function(joint, util, g, V) { + + function closestIntersection(intersections, refPoint) { + + if (intersections.length === 1) return intersections[0]; + return util.sortBy(intersections, function(i) { return i.squaredDistance(refPoint) })[0]; + } + + function offset(p1, p2, offset) { + + if (!isFinite(offset)) return p1; + var length = p1.distance(p2); + if (offset === 0 && length > 0) return p1; + return p1.move(p2, -Math.min(offset, length - 1)); + } + + function stroke(magnet) { + + var stroke = magnet.getAttribute('stroke-width'); + if (stroke === null) return 0; + return parseFloat(stroke) || 0; + } + + // Connection Points + + function anchor(line, view, magnet, opt) { + + return offset(line.end, line.start, opt.offset); + } + + function bboxIntersection(line, view, magnet, opt) { + + var bbox = view.getNodeBBox(magnet); + if (opt.stroke) bbox.inflate(stroke(magnet) / 2); + var intersections = line.intersect(bbox); + var cp = (intersections) + ? closestIntersection(intersections, line.start) + : line.end; + return offset(cp, line.start, opt.offset); + } + + function rectangleIntersection(line, view, magnet, opt) { + + var angle = view.model.angle(); + if (angle === 0) { + return bboxIntersection(line, view, magnet, opt); + } + + var bboxWORotation = view.getNodeUnrotatedBBox(magnet); + if (opt.stroke) bboxWORotation.inflate(stroke(magnet) / 2); + var center = bboxWORotation.center(); + var lineWORotation = line.clone().rotate(center, angle); + var intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation); + var cp = (intersections) + ? closestIntersection(intersections, lineWORotation.start).rotate(center, -angle) + : line.end; + return offset(cp, line.start, opt.offset); + } + + var BNDR_SUBDIVISIONS = 'segmentSubdivisons'; + var BNDR_SHAPE_BBOX = 'shapeBBox'; + + function boundaryIntersection(line, view, magnet, opt) { + + var node, intersection; + var selector = opt.selector; + var anchor = line.end; + + if (typeof selector === 'string') { + node = view.findBySelector(selector)[0]; + } else if (Array.isArray(selector)) { + node = util.getByPath(magnet, selector); + } else { + // Find the closest non-group descendant + node = magnet; + do { + var tagName = node.tagName.toUpperCase(); + if (tagName === 'G') { + node = node.firstChild; + } else if (tagName === 'TITLE') { + node = node.nextSibling; + } else break; + } while (node) + } + + if (!(node instanceof Element)) return anchor; + + var localShape = view.getNodeShape(node); + var magnetMatrix = view.getNodeMatrix(node); + var translateMatrix = view.getRootTranslateMatrix(); + var rotateMatrix = view.getRootRotateMatrix(); + var targetMatrix = translateMatrix.multiply(rotateMatrix).multiply(magnetMatrix); + var localMatrix = targetMatrix.inverse(); + var localLine = V.transformLine(line, localMatrix); + var localRef = localLine.start.clone(); + var data = view.getNodeData(node); + + if (opt.insideout === false) { + if (!data[BNDR_SHAPE_BBOX]) data[BNDR_SHAPE_BBOX] = localShape.bbox(); + var localBBox = data[BNDR_SHAPE_BBOX]; + if (localBBox.containsPoint(localRef)) return anchor; + } + + // Caching segment subdivisions for paths + var pathOpt + if (localShape instanceof g.Path) { + var precision = opt.precision || 2; + if (!data[BNDR_SUBDIVISIONS]) data[BNDR_SUBDIVISIONS] = localShape.getSegmentSubdivisions({ precision: precision }); + segmentSubdivisions = data[BNDR_SUBDIVISIONS]; + pathOpt = { + precision: precision, + segmentSubdivisions: data[BNDR_SUBDIVISIONS] + } + } + + if (opt.extrapolate === true) localLine.setLength(1e6); + + intersection = localLine.intersect(localShape, pathOpt); + if (intersection) { + // More than one intersection + if (V.isArray(intersection)) intersection = closestIntersection(intersection, localRef); + } else if (opt.sticky === true) { + // No intersection, find the closest point instead + if (localShape instanceof g.Rect) { + intersection = localShape.pointNearestToPoint(localRef); + } else if (localShape instanceof g.Ellipse) { + intersection = localShape.intersectionWithLineFromCenterToPoint(localRef); + } else { + intersection = localShape.closestPoint(localRef, pathOpt); + } + } + + var cp = (intersection) ? V.transformPoint(intersection, targetMatrix) : anchor; + var cpOffset = opt.offset || 0; + if (opt.stroke) cpOffset += stroke(node) / 2; + + return offset(cp, line.start, cpOffset); + } + + joint.connectionPoints = { + anchor: anchor, + bbox: bboxIntersection, + rectangle: rectangleIntersection, + boundary: boundaryIntersection + } + +})(joint, joint.util, g, V); + +(function(joint, util) { + + function abs2rel(value, max) { + + if (max === 0) return '0%'; + return Math.round(value / max * 100) + '%'; + } + + function pin(relative) { + + return function(end, view, magnet, coords) { + + var angle = view.model.angle(); + var bbox = view.getNodeUnrotatedBBox(magnet); + var origin = view.model.getBBox().center(); + coords.rotate(origin, angle); + var dx = coords.x - bbox.x; + var dy = coords.y - bbox.y; + + if (relative) { + dx = abs2rel(dx, bbox.width); + dy = abs2rel(dy, bbox.height); + } + + end.anchor = { + name: 'topLeft', + args: { + dx: dx, + dy: dy, + rotate: true + } + }; + + return end; + } + } - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - */ - highlight: function(cellView, magnetEl) { + joint.connectionStrategies = { + useDefaults: util.noop, + pinAbsolute: pin(false), + pinRelative: pin(true) + } - V(magnetEl).addClass(joint.util.addClassNamePrefix('highlight-opacity')); - }, +})(joint, joint.util); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - */ - unhighlight: function(cellView, magnetEl) { +(function(joint, util, V, g) { - V(magnetEl).removeClass(joint.util.addClassNamePrefix('highlight-opacity')); + function getAnchor(coords, view, magnet) { + // take advantage of an existing logic inside of the + // pin relative connection strategy + var end = joint.connectionStrategies.pinRelative.call( + this.paper, + {}, + view, + magnet, + coords, + this.model + ); + return end.anchor; } -}; -joint.highlighters.stroke = { + function snapAnchor(coords, view, magnet, type, relatedView, toolView) { + var snapRadius = toolView.options.snapRadius; + var isSource = (type === 'source'); + var refIndex = (isSource ? 0 : -1); + var ref = this.model.vertex(refIndex) || this.getEndAnchor(isSource ? 'target' : 'source'); + if (ref) { + if (Math.abs(ref.x - coords.x) < snapRadius) coords.x = ref.x; + if (Math.abs(ref.y - coords.y) < snapRadius) coords.y = ref.y; + } + return coords; + } - defaultOptions: { - padding: 3, - rx: 0, - ry: 0, - attrs: { - 'stroke-width': 3, - stroke: '#FEB663' + var ToolView = joint.dia.ToolView; + + // Vertex Handles + var VertexHandle = joint.mvc.View.extend({ + tagName: 'circle', + svgElement: true, + className: 'marker-vertex', + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown', + dblclick: 'onDoubleClick' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + attributes: { + 'r': 6, + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move' + }, + position: function(x, y) { + this.vel.attr({ cx: x, cy: y }); + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + this.options.paper.undelegateEvents(); + this.delegateDocumentEvents(null, evt.data); + this.trigger('will-change'); + }, + onPointerMove: function(evt) { + this.trigger('changing', this, evt); + }, + onDoubleClick: function(evt) { + this.trigger('remove', this, evt); + }, + onPointerUp: function(evt) { + this.trigger('changed', this, evt); + this.undelegateDocumentEvents(); + this.options.paper.delegateEvents(); } - }, + }); - _views: {}, + var Vertices = ToolView.extend({ + name: 'vertices', + options: { + handleClass: VertexHandle, + snapRadius: 20, + redundancyRemoval: true, + vertexAdding: true, + }, + children: [{ + tagName: 'path', + selector: 'connection', + className: 'joint-vertices-path', + attributes: { + 'fill': 'none', + 'stroke': 'transparent', + 'stroke-width': 10, + 'cursor': 'cell' + } + }], + handles: null, + events: { + 'mousedown .joint-vertices-path': 'onPathPointerDown' + }, + onRender: function() { + this.resetHandles(); + if (this.options.vertexAdding) { + this.renderChildren(); + this.updatePath(); + } + var relatedView = this.relatedView; + var vertices = relatedView.model.vertices(); + for (var i = 0, n = vertices.length; i < n; i++) { + var vertex = vertices[i]; + var handle = new (this.options.handleClass)({ index: i, paper: this.paper }); + handle.render(); + handle.position(vertex.x, vertex.y); + this.simulateRelatedView(handle.el); + handle.vel.appendTo(this.el); + this.handles.push(handle); + this.startHandleListening(handle); + } + return this; + }, + update: function() { + this.render(); + return this; + }, + updatePath: function() { + var connection = this.childNodes.connection; + if (connection) connection.setAttribute('d', this.relatedView.getConnection().serialize()); + }, + startHandleListening: function(handle) { + var relatedView = this.relatedView; + if (relatedView.can('vertexMove')) { + this.listenTo(handle, 'will-change', this.onHandleWillChange); + this.listenTo(handle, 'changing', this.onHandleChanging); + this.listenTo(handle, 'changed', this.onHandleChanged); + } + if (relatedView.can('vertexRemove')) { + this.listenTo(handle, 'remove', this.onHandleRemove); + } + }, + resetHandles: function() { + var handles = this.handles; + this.handles = []; + this.stopListening(); + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + handles[i].remove(); + } + }, + getNeighborPoints: function(index) { + var linkView = this.relatedView; + var vertices = linkView.model.vertices(); + var prev = (index > 0) ? vertices[index - 1] : linkView.sourceAnchor; + var next = (index < vertices.length - 1) ? vertices[index + 1] : linkView.targetAnchor; + return { + prev: new g.Point(prev), + next: new g.Point(next) + } + }, + onHandleWillChange: function(handle, evt) { + this.focus(); + this.relatedView.model.startBatch('vertex-move', { ui: true, tool: this.cid }); + }, + onHandleChanging: function(handle, evt) { + var relatedView = this.relatedView; + var paper = relatedView.paper; + var index = handle.options.index; + var vertex = paper.snapToGrid(evt.clientX, evt.clientY).toJSON(); + this.snapVertex(vertex, index); + relatedView.model.vertex(index, vertex, { ui: true, tool: this.cid }); + handle.position(vertex.x, vertex.y); + }, + snapVertex: function(vertex, index) { + var snapRadius = this.options.snapRadius; + if (snapRadius > 0) { + var neighbors = this.getNeighborPoints(index); + var prev = neighbors.prev; + var next = neighbors.next; + if (Math.abs(vertex.x - prev.x) < snapRadius) { + vertex.x = prev.x; + } else if (Math.abs(vertex.x - next.x) < snapRadius) { + vertex.x = next.x; + } + if (Math.abs(vertex.y - prev.y) < snapRadius) { + vertex.y = neighbors.prev.y; + } else if (Math.abs(vertex.y - next.y) < snapRadius) { + vertex.y = next.y; + } + } + }, + onHandleChanged: function(handle, evt) { + if (this.options.vertexAdding) this.updatePath(); + if (!this.options.redundancyRemoval) return; + var linkView = this.relatedView; + var verticesRemoved = linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + if (verticesRemoved) this.render(); + this.blur(); + linkView.model.stopBatch('vertex-move', { ui: true, tool: this.cid }); + if (this.eventData(evt).vertexAdded) { + linkView.model.stopBatch('vertex-add', { ui: true, tool: this.cid }); + } + }, + onHandleRemove: function(handle) { + var index = handle.options.index; + this.relatedView.model.removeVertex(index, { ui: true }); + }, + onPathPointerDown: function(evt) { + evt.stopPropagation(); + var vertex = this.paper.snapToGrid(evt.clientX, evt.clientY).toJSON(); + var relatedView = this.relatedView; + relatedView.model.startBatch('vertex-add', { ui: true, tool: this.cid }); + var index = relatedView.getVertexIndex(vertex.x, vertex.y); + this.snapVertex(vertex, index); + relatedView.model.insertVertex(index, vertex, { ui: true, tool: this.cid }); + this.render(); + var handle = this.handles[index]; + this.eventData(evt, { vertexAdded: true }); + handle.onPointerDown(evt); + }, + onRemove: function() { + this.resetHandles(); + } + }, { + VertexHandle: VertexHandle // keep as class property + }); - getHighlighterId: function(magnetEl, opt) { + var SegmentHandle = joint.mvc.View.extend({ + tagName: 'g', + svgElement: true, + className: 'marker-segment', + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + children: [{ + tagName: 'line', + selector: 'line', + attributes: { + 'stroke': '#33334F', + 'stroke-width': 2, + 'fill': 'none', + 'pointer-events': 'none' + } + }, { + tagName: 'rect', + selector: 'handle', + attributes: { + 'width': 20, + 'height': 8, + 'x': -10, + 'y': -4, + 'rx': 4, + 'ry': 4, + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2 + } + }], + onRender: function() { + this.renderChildren(); + }, + position: function(x, y, angle, view) { - return magnetEl.id + JSON.stringify(opt); - }, + var matrix = V.createSVGMatrix().translate(x, y).rotate(angle); + var handle = this.childNodes.handle; + handle.setAttribute('transform', V.matrixToTransformString(matrix)); + handle.setAttribute('cursor', (angle % 180 === 0) ? 'row-resize' : 'col-resize'); - removeHighlighter: function(id) { - if (this._views[id]) { - this._views[id].remove(); - this._views[id] = null; + var viewPoint = view.getClosestPoint(new g.Point(x, y)); + var line = this.childNodes.line; + line.setAttribute('x1', x); + line.setAttribute('y1', y); + line.setAttribute('x2', viewPoint.x); + line.setAttribute('y2', viewPoint.y); + }, + onPointerDown: function(evt) { + this.trigger('change:start', this, evt); + evt.stopPropagation(); + this.options.paper.undelegateEvents(); + this.delegateDocumentEvents(null, evt.data); + }, + onPointerMove: function(evt) { + this.trigger('changing', this, evt); + }, + onPointerUp: function(evt) { + this.undelegateDocumentEvents(); + this.options.paper.delegateEvents(); + this.trigger('change:end', this, evt); + }, + show: function() { + this.el.style.display = ''; + }, + hide: function() { + this.el.style.display = 'none'; } - }, + }); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - highlight: function(cellView, magnetEl, opt) { + var Segments = ToolView.extend({ + name: 'segments', + precision: .5, + options: { + handleClass: SegmentHandle, + segmentLengthThreshold: 40, + redundancyRemoval: true, + anchor: getAnchor, + snapRadius: 10, + snapHandle: true + }, + handles: null, + onRender: function() { + this.resetHandles(); + var relatedView = this.relatedView; + var vertices = relatedView.model.vertices(); + vertices.unshift(relatedView.sourcePoint); + vertices.push(relatedView.targetPoint); + for (var i = 0, n = vertices.length; i < n - 1; i++) { + var vertex = vertices[i]; + var nextVertex = vertices[i + 1]; + var handle = this.renderHandle(vertex, nextVertex); + this.simulateRelatedView(handle.el); + this.handles.push(handle); + handle.options.index = i; + } + return this; + }, + renderHandle: function(vertex, nextVertex) { + var handle = new (this.options.handleClass)({ paper: this.paper }); + handle.render(); + this.updateHandle(handle, vertex, nextVertex); + handle.vel.appendTo(this.el); + this.startHandleListening(handle); + return handle; + }, + update: function() { + this.render(); + return this; + }, + startHandleListening: function(handle) { + this.listenTo(handle, 'change:start', this.onHandleChangeStart); + this.listenTo(handle, 'changing', this.onHandleChanging); + this.listenTo(handle, 'change:end', this.onHandleChangeEnd); + }, + resetHandles: function() { + var handles = this.handles; + this.handles = []; + this.stopListening(); + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + handles[i].remove(); + } + }, + shiftHandleIndexes: function(value) { + var handles = this.handles; + for (var i = 0, n = handles.length; i < n; i++) handles[i].options.index += value; + }, + resetAnchor: function(type, anchor) { + var relatedModel = this.relatedView.model; + if (anchor) { + relatedModel.prop([type, 'anchor'], anchor, { + rewrite: true, + ui: true, + tool: this.cid + }); + } else { + relatedModel.removeProp([type, 'anchor'], { + ui: true, + tool: this.cid + }); + } + }, + snapHandle: function(handle, position, data) { + + var index = handle.options.index; + var linkView = this.relatedView; + var link = linkView.model; + var vertices = link.vertices(); + var axis = handle.options.axis; + var prev = vertices[index - 2] || data.sourceAnchor; + var next = vertices[index + 1] || data.targetAnchor; + var snapRadius = this.options.snapRadius; + if (Math.abs(position[axis] - prev[axis]) < snapRadius) { + position[axis] = prev[axis]; + } else if (Math.abs(position[axis] - next[axis]) < snapRadius) { + position[axis] = next[axis]; + } + return position; + }, + + onHandleChanging: function(handle, evt) { + + var data = this.eventData(evt); + var relatedView = this.relatedView; + var paper = relatedView.paper; + var index = handle.options.index - 1; + var coords = paper.snapToGrid(evt.clientX, evt.clientY); + var position = this.snapHandle(handle, coords.clone(), data); + var axis = handle.options.axis; + var offset = (this.options.snapHandle) ? 0 : (coords[axis] - position[axis]); + var link = relatedView.model; + var vertices = util.cloneDeep(link.vertices()); + var vertex = vertices[index]; + var nextVertex = vertices[index + 1]; + var anchorFn = this.options.anchor; + if (typeof anchorFn !== 'function') anchorFn = null; + + // First Segment + var sourceView = relatedView.sourceView; + var sourceBBox = relatedView.sourceBBox; + var changeSourceAnchor = false; + var deleteSourceAnchor = false; + if (!vertex) { + vertex = relatedView.sourceAnchor.toJSON(); + vertex[axis] = position[axis]; + if (sourceBBox.containsPoint(vertex)) { + vertex[axis] = position[axis]; + changeSourceAnchor = true; + } else { + // we left the area of the source magnet for the first time + vertices.unshift(vertex); + this.shiftHandleIndexes(1); + delateSourceAnchor = true; + } + } else if (index === 0) { + if (sourceBBox.containsPoint(vertex)) { + vertices.shift(); + this.shiftHandleIndexes(-1); + changeSourceAnchor = true; + } else { + vertex[axis] = position[axis]; + deleteSourceAnchor = true; + } + } else { + vertex[axis] = position[axis]; + } - var id = this.getHighlighterId(magnetEl, opt); + if (anchorFn && sourceView) { + if (changeSourceAnchor) { + var sourceAnchorPosition = data.sourceAnchor.clone(); + sourceAnchorPosition[axis] = position[axis]; + var sourceAnchor = anchorFn.call(relatedView, sourceAnchorPosition, sourceView, relatedView.sourceMagnet || sourceView.el, 'source', relatedView); + this.resetAnchor('source', sourceAnchor); + } + if (deleteSourceAnchor) { + this.resetAnchor('source', data.sourceAnchorDef); + } + } - // Only highlight once. - if (this._views[id]) return; + // Last segment + var targetView = relatedView.targetView; + var targetBBox = relatedView.targetBBox; + var changeTargetAnchor = false; + var deleteTargetAnchor = false; + if (!nextVertex) { + nextVertex = relatedView.targetAnchor.toJSON(); + nextVertex[axis] = position[axis]; + if (targetBBox.containsPoint(nextVertex)) { + changeTargetAnchor = true; + } else { + // we left the area of the target magnet for the first time + vertices.push(nextVertex); + deleteTargetAnchor = true; + } + } else if (index === vertices.length - 2) { + if (targetBBox.containsPoint(nextVertex)) { + vertices.pop(); + changeTargetAnchor = true; + } else { + nextVertex[axis] = position[axis]; + deleteTargetAnchor = true; + } + } else { + nextVertex[axis] = position[axis]; + } - var options = joint.util.defaults(opt || {}, this.defaultOptions); + if (anchorFn && targetView) { + if (changeTargetAnchor) { + var targetAnchorPosition = data.targetAnchor.clone(); + targetAnchorPosition[axis] = position[axis]; + var targetAnchor = anchorFn.call(relatedView, targetAnchorPosition, targetView, relatedView.targetMagnet || targetView.el, 'target', relatedView); + this.resetAnchor('target', targetAnchor); + } + if (deleteTargetAnchor) { + this.resetAnchor('target', data.targetAnchorDef); + } + } - var magnetVel = V(magnetEl); - var magnetBBox; + link.vertices(vertices, { ui: true, tool: this.cid }); + this.updateHandle(handle, vertex, nextVertex, offset); + }, + onHandleChangeStart: function(handle, evt) { + var index = handle.options.index; + var handles = this.handles; + if (!Array.isArray(handles)) return; + for (var i = 0, n = handles.length; i < n; i++) { + if (i !== index) handles[i].hide() + } + this.focus(); + var relatedView = this.relatedView; + var relatedModel = relatedView.model; + this.eventData(evt, { + sourceAnchor: relatedView.sourceAnchor.clone(), + targetAnchor: relatedView.targetAnchor.clone(), + sourceAnchorDef: util.clone(relatedModel.prop(['source', 'anchor'])), + targetAnchorDef: util.clone(relatedModel.prop(['target', 'anchor'])) + }); + relatedView.model.startBatch('segment-move', { ui: true, tool: this.cid }); + }, + onHandleChangeEnd: function(handle) { + var linkView = this.relatedView; + if (this.options.redundancyRemoval) { + linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + } + this.render(); + this.blur(); + linkView.model.stopBatch('segment-move', { ui: true, tool: this.cid }); + }, + updateHandle: function(handle, vertex, nextVertex, offset) { + var vertical = Math.abs(vertex.x - nextVertex.x) < this.precision; + var horizontal = Math.abs(vertex.y - nextVertex.y) < this.precision; + if (vertical || horizontal) { + var segmentLine = new g.Line(vertex, nextVertex); + var length = segmentLine.length(); + if (length < this.options.segmentLengthThreshold) { + handle.hide(); + } else { + var position = segmentLine.midpoint() + var axis = (vertical) ? 'x' : 'y'; + position[axis] += offset || 0; + var angle = segmentLine.vector().vectorAngle(new g.Point(1, 0)); + handle.position(position.x, position.y, angle, this.relatedView); + handle.show(); + handle.options.axis = axis; + } + } else { + handle.hide(); + } + }, + onRemove: function() { + this.resetHandles(); + } + }, { + SegmentHandle: SegmentHandle // keep as class property + }); - try { + // End Markers + var Arrowhead = ToolView.extend({ + tagName: 'path', + xAxisVector: new g.Point(1, 0), + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + onRender: function() { + this.update() + }, + update: function() { + var ratio = this.ratio; + var view = this.relatedView; + var tangent = view.getTangentAtRatio(ratio); + var position, angle; + if (tangent) { + position = tangent.start; + angle = tangent.vector().vectorAngle(this.xAxisVector) || 0; + } else { + position = view.getPointAtRatio(ratio); + angle = 0; + } + var matrix = V.createSVGMatrix().translate(position.x, position.y).rotate(angle); + this.vel.transform(matrix, { absolute: true }); + return this; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + var relatedView = this.relatedView; + relatedView.model.startBatch('arrowhead-move', { ui: true, tool: this.cid }); + if (relatedView.can('arrowheadMove')) { + relatedView.startArrowheadMove(this.arrowheadType); + this.delegateDocumentEvents(); + relatedView.paper.undelegateEvents(); + } + this.focus(); + this.el.style.pointerEvents = 'none'; + }, + onPointerMove: function(evt) { + var coords = this.paper.snapToGrid(evt.clientX, evt.clientY); + this.relatedView.pointermove(evt, coords.x, coords.y); + }, + onPointerUp: function(evt) { + this.undelegateDocumentEvents(); + var relatedView = this.relatedView; + var paper = relatedView.paper; + var coords = paper.snapToGrid(evt.clientX, evt.clientY); + relatedView.pointerup(evt, coords.x, coords.y); + paper.delegateEvents(); + this.blur(); + this.el.style.pointerEvents = ''; + relatedView.model.stopBatch('arrowhead-move', { ui: true, tool: this.cid }); + } + }); - var pathData = magnetVel.convertToPathData(); + var TargetArrowhead = Arrowhead.extend({ + name: 'target-arrowhead', + ratio: 1, + arrowheadType: 'target', + attributes: { + 'd': 'M -10 -8 10 0 -10 8 Z', + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move', + 'class': 'target-arrowhead' + } + }); - } catch (error) { + var SourceArrowhead = Arrowhead.extend({ + name: 'source-arrowhead', + ratio: 0, + arrowheadType: 'source', + attributes: { + 'd': 'M 10 -8 -10 0 10 8 Z', + 'fill': '#33334F', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'cursor': 'move', + 'class': 'source-arrowhead' + } + }); - // Failed to get path data from magnet element. - // Draw a rectangle around the entire cell view instead. - magnetBBox = magnetVel.bbox(true/* without transforms */); - pathData = V.rectToPath(joint.util.assign({}, options, magnetBBox)); + var Button = ToolView.extend({ + name: 'button', + events: { + 'mousedown': 'onPointerDown', + 'touchstart': 'onPointerDown' + }, + options: { + distance: 0, + offset: 0, + rotate: false + }, + onRender: function() { + this.renderChildren(this.options.markup); + this.update() + }, + update: function() { + var tangent, position, angle; + var distance = this.options.distance || 0; + if (util.isPercentage(distance)) { + tangent = this.relatedView.getTangentAtRatio(parseFloat(distance) / 100); + } else { + tangent = this.relatedView.getTangentAtLength(distance) + } + if (tangent) { + position = tangent.start; + angle = tangent.vector().vectorAngle(new g.Point(1,0)) || 0; + } else { + position = this.relatedView.getConnection().start; + angle = 0; + } + var matrix = V.createSVGMatrix() + .translate(position.x, position.y) + .rotate(angle) + .translate(0, this.options.offset || 0); + if (!this.options.rotate) matrix = matrix.rotate(-angle); + this.vel.transform(matrix, { absolute: true }); + return this; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + var actionFn = this.options.action; + if (typeof actionFn === 'function') { + actionFn.call(this.relatedView, evt, this.relatedView); + } } + }); - var highlightVel = V('path').attr({ - d: pathData, - 'pointer-events': 'none', - 'vector-effect': 'non-scaling-stroke', - 'fill': 'none' - }).attr(options.attrs); - var highlightMatrix = magnetVel.getTransformToElement(cellView.el); + var Remove = Button.extend({ + children: [{ + tagName: 'circle', + selector: 'button', + attributes: { + 'r': 7, + 'fill': '#FF1D00', + 'cursor': 'pointer' + } + }, { + tagName: 'path', + selector: 'icon', + attributes: { + 'd': 'M -3 -3 3 3 M -3 3 3 -3', + 'fill': 'none', + 'stroke': '#FFFFFF', + 'stroke-width': 2, + 'pointer-events': 'none' + } + }], + options: { + distance: 60, + offset: 0, + action: function(evt) { + this.model.remove({ ui: true, tool: this.cid }); + } + } + }); - // Add padding to the highlight element. - var padding = options.padding; - if (padding) { + var Boundary = ToolView.extend({ + name: 'boundary', + tagName: 'rect', + options: { + padding: 10 + }, + attributes: { + 'fill': 'none', + 'stroke': '#33334F', + 'stroke-width': .5, + 'stroke-dasharray': '5, 5', + 'pointer-events': 'none' + }, + onRender: function() { + this.update(); + }, + update: function() { + var padding = this.options.padding; + if (!isFinite(padding)) padding = 0; + var bbox = this.relatedView.getConnection().bbox().inflate(padding); + this.vel.attr(bbox.toJSON()); + return this; + } + }); - magnetBBox || (magnetBBox = magnetVel.bbox(true)); + var Anchor = ToolView.extend({ + tagName: 'g', + type: null, + children: [{ + tagName: 'circle', + selector: 'anchor', + attributes: { + 'cursor': 'pointer' + } + }, { + tagName: 'rect', + selector: 'area', + attributes: { + 'pointer-events': 'none', + 'fill': 'none', + 'stroke': '#33334F', + 'stroke-dasharray': '2,4', + 'rx': 5, + 'ry': 5 + } + }], + events: { + mousedown: 'onPointerDown', + touchstart: 'onPointerDown', + dblclick: 'onPointerDblClick' + }, + documentEvents: { + mousemove: 'onPointerMove', + touchmove: 'onPointerMove', + mouseup: 'onPointerUp', + touchend: 'onPointerUp' + }, + options: { + snap: snapAnchor, + anchor: getAnchor, + customAnchorAttributes: { + 'stroke-width': 4, + 'stroke': '#33334F', + 'fill': '#FFFFFF', + 'r': 5 + }, + defaultAnchorAttributes: { + 'stroke-width': 2, + 'stroke': '#FFFFFF', + 'fill': '#33334F', + 'r': 6 + }, + areaPadding: 6, + snapRadius: 10, + restrictArea: true, + redundancyRemoval: true + }, + onRender: function() { + this.renderChildren(); + this.toggleArea(false); + this.update(); + }, + update: function() { + var type = this.type; + var relatedView = this.relatedView; + var view = relatedView.getEndView(type); + if (view) { + this.updateAnchor(); + this.updateArea(); + this.el.style.display = ''; + } else { + this.el.style.display = 'none'; + } + return this; + }, + updateAnchor: function() { + var childNodes = this.childNodes; + if (!childNodes) return; + var anchorNode = childNodes.anchor; + if (!anchorNode) return; + var relatedView = this.relatedView; + var type = this.type; + var position = relatedView.getEndAnchor(type); + var options = this.options; + var customAnchor = relatedView.model.prop([type, 'anchor']); + anchorNode.setAttribute('transform', 'translate(' + position.x + ',' + position.y + ')'); + var anchorAttributes = (customAnchor) ? options.customAnchorAttributes : options.defaultAnchorAttributes; + for (var attrName in anchorAttributes) { + anchorNode.setAttribute(attrName, anchorAttributes[attrName]); + } + }, + updateArea: function() { + var childNodes = this.childNodes; + if (!childNodes) return; + var areaNode = childNodes.area; + if (!areaNode) return; + var relatedView = this.relatedView; + var type = this.type; + var view = relatedView.getEndView(type); + var magnet = relatedView.getEndMagnet(type); + var padding = this.options.areaPadding; + if (!isFinite(padding)) padding = 0; + var bbox = view.getNodeUnrotatedBBox(magnet).inflate(padding); + var angle = view.model.angle(); + areaNode.setAttribute('x', -bbox.width / 2); + areaNode.setAttribute('y', -bbox.height / 2); + areaNode.setAttribute('width', bbox.width); + areaNode.setAttribute('height', bbox.height); + var origin = view.model.getBBox().center(); + var center = bbox.center().rotate(origin, -angle) + areaNode.setAttribute('transform', 'translate(' + center.x + ',' + center.y + ') rotate(' + angle +')'); + }, + toggleArea: function(visible) { + this.childNodes.area.style.display = (visible) ? '' : 'none'; + }, + onPointerDown: function(evt) { + evt.stopPropagation(); + this.paper.undelegateEvents(); + this.delegateDocumentEvents(); + this.focus(); + this.toggleArea(this.options.restrictArea); + this.relatedView.model.startBatch('anchor-move', { ui: true, tool: this.cid }); + }, + resetAnchor: function(anchor) { + var type = this.type; + var relatedModel = this.relatedView.model; + if (anchor) { + relatedModel.prop([type, 'anchor'], anchor, { + rewrite: true, + ui: true, + tool: this.cid + }); + } else { + relatedModel.removeProp([type, 'anchor'], { + ui: true, + tool: this.cid + }); + } + }, + onPointerMove: function(evt) { - var cx = magnetBBox.x + (magnetBBox.width / 2); - var cy = magnetBBox.y + (magnetBBox.height / 2); + var relatedView = this.relatedView; + var type = this.type; + var view = relatedView.getEndView(type); + var magnet = relatedView.getEndMagnet(type); - magnetBBox = V.transformRect(magnetBBox, highlightMatrix); + var coords = this.paper.clientToLocalPoint(evt.clientX, evt.clientY); + var snapFn = this.options.snap; + if (typeof snapFn === 'function') { + coords = snapFn.call(relatedView, coords, view, magnet, type, relatedView, this); + coords = new g.Point(coords); + } - var width = Math.max(magnetBBox.width, 1); - var height = Math.max(magnetBBox.height, 1); - var sx = (width + padding) / width; - var sy = (height + padding) / height; + if (this.options.restrictArea) { + // snap coords within node bbox + var bbox = view.getNodeUnrotatedBBox(magnet); + var angle = view.model.angle(); + var origin = view.model.getBBox().center(); + var rotatedCoords = coords.clone().rotate(origin, angle); + if (!bbox.containsPoint(rotatedCoords)) { + coords = bbox.pointNearestToPoint(rotatedCoords).rotate(origin, -angle); + } + } - var paddingMatrix = V.createSVGMatrix({ - a: sx, - b: 0, - c: 0, - d: sy, - e: cx - sx * cx, - f: cy - sy * cy - }); + var anchor; + var anchorFn = this.options.anchor; + if (typeof anchorFn === 'function') { + anchor = anchorFn.call(relatedView, coords, view, magnet, type, relatedView); + } - highlightMatrix = highlightMatrix.multiply(paddingMatrix); - } + this.resetAnchor(anchor); + this.update(); + }, - highlightVel.transform(highlightMatrix); + onPointerUp: function(evt) { + this.paper.delegateEvents(); + this.undelegateDocumentEvents(); + this.blur(); + this.toggleArea(false); + var linkView = this.relatedView; + if (this.options.redundancyRemoval) linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid }); + linkView.model.stopBatch('anchor-move', { ui: true, tool: this.cid }); + }, - // joint.mvc.View will handle the theme class name and joint class name prefix. - var highlightView = this._views[id] = new joint.mvc.View({ - svgElement: true, - className: 'highlight-stroke', - el: highlightVel.node - }); + onPointerDblClick: function() { + this.resetAnchor(); + this.update(); + } + }); - // Remove the highlight view when the cell is removed from the graph. - var removeHandler = this.removeHighlighter.bind(this, id); - var cell = cellView.model; - highlightView.listenTo(cell, 'remove', removeHandler); - highlightView.listenTo(cell.graph, 'reset', removeHandler); + var SourceAnchor = Anchor.extend({ + name: 'source-anchor', + type: 'source' + }); - cellView.vel.append(highlightVel); - }, + var TargetAnchor = Anchor.extend({ + name: 'target-anchor', + type: 'target' + }); - /** - * @param {joint.dia.CellView} cellView - * @param {Element} magnetEl - * @param {object=} opt - */ - unhighlight: function(cellView, magnetEl, opt) { + // Export + joint.linkTools = { + Vertices: Vertices, + Segments: Segments, + SourceArrowhead: SourceArrowhead, + TargetArrowhead: TargetArrowhead, + SourceAnchor: SourceAnchor, + TargetAnchor: TargetAnchor, + Button: Button, + Remove: Remove, + Boundary: Boundary + }; - this.removeHighlighter(this.getHighlighterId(magnetEl, opt)); - } -}; +})(joint, joint.util, V, g); joint.dia.Element.define('erd.Entity', { size: { width: 150, height: 60 }, @@ -16790,7 +25257,7 @@ joint.shapes.basic.Generic.define('uml.Class', { }); -joint.shapes.uml.ClassView = joint.dia.ElementView.extend({}, { +joint.shapes.uml.ClassView = joint.dia.ElementView.extend({ initialize: function() { @@ -17221,7 +25688,8 @@ joint.layout.DirectedGraph = { graph = graphOrCells; } else { // Reset cells in dry mode so the graph reference is not stored on the cells. - graph = (new joint.dia.Graph()).resetCells(graphOrCells, { dry: true }); + // `sort: false` to prevent elements to change their order based on the z-index + graph = (new joint.dia.Graph()).resetCells(graphOrCells, { dry: true, sort: false }); } // This is not needed anymore. diff --git a/dist/joint.nowrap.min.js b/dist/joint.nowrap.min.js index fa606f10b..02175c923 100644 --- a/dist/joint.nowrap.min.js +++ b/dist/joint.nowrap.min.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -6,20 +6,23 @@ 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(){function a(a){this.message=a}var b="undefined"!=typeof exports?exports:this,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a.prototype=new Error,a.prototype.name="InvalidCharacterError",b.btoa||(b.btoa=function(b){for(var d,e,f=String(b),g=0,h=c,i="";f.charAt(0|g)||(h="=",g%1);i+=h.charAt(63&d>>8-g%1*8)){if(e=f.charCodeAt(g+=.75),e>255)throw new a("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");d=d<<8|e}return i}),b.atob||(b.atob=function(b){var d=String(b).replace(/=+$/,"");if(d.length%4==1)throw new a("'atob' failed: The string to be decoded is not correctly encoded.");for(var e,f,g=0,h=0,i="";f=d.charAt(h++);~f&&(e=g%4?64*e+f:f,g++%4)?i+=String.fromCharCode(255&e>>(-2*g&6)):0)f=c.indexOf(f);return i})}(),function(){function a(a,b){return this.slice(a,b)}function b(a,b){arguments.length<2&&(b=0);for(var c=0,d=a.length;c>>0;if(0===e)return!1;for(var f=0|b,g=Math.max(f>=0?f:e-Math.abs(f),0);g>>0;if("function"!=typeof a)throw new TypeError("predicate must be a function");for(var d=arguments[1],e=0;e0?1:-1)*Math.floor(Math.abs(b)):b},d=Math.pow(2,53)-1,e=function(a){var b=c(a);return Math.min(Math.max(b,0),d)};return function(a){var c=this,d=Object(a);if(null==a)throw new TypeError("Array.from requires an array-like object - not null or undefined");var f,g=arguments.length>1?arguments[1]:void 0;if("undefined"!=typeof g){if(!b(g))throw new TypeError("Array.from: when provided, the second argument must be a function");arguments.length>2&&(f=arguments[2])}for(var h,i=e(d.length),j=b(c)?Object(new c(i)):new Array(i),k=0;k>>0;if("function"!=typeof a)throw new TypeError("predicate must be a function");for(var d=arguments[1],e=0;ethis.length)&&this.indexOf(a,b)!==-1}),String.prototype.startsWith||(String.prototype.startsWith=function(a,b){return this.substr(b||0,a.length)===a}),Number.isFinite=Number.isFinite||function(a){return"number"==typeof a&&isFinite(a)},Number.isNaN=Number.isNaN||function(a){return a!==a}; -var g=function(){var a={},b=Math,c=b.abs,d=b.cos,e=b.sin,f=b.sqrt,g=b.min,h=b.max,i=b.atan2,j=b.round,k=b.floor,l=b.PI,m=b.random,n=b.pow;a.bezier={curveThroughPoints:function(a){for(var b=this.getCurveControlPoints(a),c=["M",a[0].x,a[0].y],d=0;dj.x+h/2,n=fj.x?g-e:g+e,d=h*h/(f-k)-h*h*(g-l)*(c-l)/(i*i*(f-k))+k):(d=g>j.y?f+e:f-e,c=i*i/(g-l)-i*i*(f-k)*(d-k)/(h*h*(g-l))+l),a.point(d,c).theta(b)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a),b&&a.rotate(q(this.x,this.y),b);var c,d=a.x-this.x,e=a.y-this.y;if(0===d)return c=this.bbox().pointNearestToPoint(a),b?c.rotate(q(this.x,this.y),-b):c;var g=e/d,h=g*g,i=this.a*this.a,j=this.b*this.b,k=f(1/(1/i+h/j));k=d<0?-k:k;var l=g*k;return c=q(this.x+k,this.y+l),b?c.rotate(q(this.x,this.y),-b):c},toString:function(){return q(this.x,this.y).toString()+" "+this.a+" "+this.b}};var p=a.Line=function(a,b){return this instanceof p?a instanceof p?p(a.start,a.end):(this.start=q(a),void(this.end=q(b))):new p(a,b)};a.Line.prototype={bearing:function(){var a=w(this.start.y),b=w(this.end.y),c=this.start.x,f=this.end.x,g=w(f-c),h=e(g)*d(b),j=d(a)*e(b)-e(a)*d(b)*d(g),k=v(i(h,j)),l=["NE","E","SE","S","SW","W","NW","N"],m=k-22.5;return m<0&&(m+=360),m=parseInt(m/45),l[m]},clone:function(){return p(this.start,this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.end.x===a.end.x&&this.end.y===a.end.y},intersect:function(a){if(a instanceof p){var b=q(this.end.x-this.start.x,this.end.y-this.start.y),c=q(a.end.x-a.start.x,a.end.y-a.start.y),d=b.x*c.y-b.y*c.x,e=q(a.start.x-this.start.x,a.start.y-this.start.y),f=e.x*c.y-e.y*c.x,g=e.x*b.y-e.y*b.x;if(0===d||f*d<0||g*d<0)return null;if(d>0){if(f>d||g>d)return null}else if(f0?l:null}return null},length:function(){return f(this.squaredLength())},midpoint:function(){return q((this.start.x+this.end.x)/2,(this.start.y+this.end.y)/2)},pointAt:function(a){var b=(1-a)*this.start.x+a*this.end.x,c=(1-a)*this.start.y+a*this.end.y;return q(b,c)},pointOffset:function(a){return((this.end.x-this.start.x)*(a.y-this.start.y)-(this.end.y-this.start.y)*(a.x-this.start.x))/2},vector:function(){return q(this.end.x-this.start.x,this.end.y-this.start.y)},closestPoint:function(a){return this.pointAt(this.closestPointNormalizedLength(a))},closestPointNormalizedLength:function(a){var b=this.vector().dot(p(this.start,a).vector());return Math.min(1,Math.max(0,b/this.squaredLength()))},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},toString:function(){return this.start.toString()+" "+this.end.toString()}},a.Line.prototype.intersection=a.Line.prototype.intersect;var q=a.Point=function(a,b){if(!(this instanceof q))return new q(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseInt(c[0],10),b=parseInt(c[1],10)}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};a.Point.fromPolar=function(a,b,f){f=f&&q(f)||q(0,0);var g=c(a*d(b)),h=c(a*e(b)),i=t(v(b));return i<90?h=-h:i<180?(g=-g,h=-h):i<270&&(g=-g),q(f.x+g,f.y+h)},a.Point.random=function(a,b,c,d){return q(k(m()*(b-a+1)+a),k(m()*(d-c+1)+c))},a.Point.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=g(h(this.x,a.x),a.x+a.width),this.y=g(h(this.y,a.y),a.y+a.height),this)},bearing:function(a){return p(this,a).bearing()},changeInAngle:function(a,b,c){return q(this).offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return q(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),q(this.x-(a||0),this.y-(b||0))},distance:function(a){return p(this,a).length()},squaredDistance:function(a){return p(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return f(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return c(a.x-this.x)+c(a.y-this.y)},move:function(a,b){var c=w(q(a).theta(this));return this.offset(d(c)*b,-e(c)*b)},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return q(a).move(this,this.distance(a))},rotate:function(a,b){b=(b+360)%360,this.toPolar(a),this.y+=w(b);var c=q.fromPolar(this.x,this.y,a);return this.x=c.x,this.y=c.y,this},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this},scale:function(a,b,c){return c=c&&q(c)||q(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=u(this.x,a),this.y=u(this.y,b||a),this},theta:function(a){a=q(a);var b=-(a.y-this.y),c=a.x-this.x,d=i(b,c);return d<0&&(d=2*l+d),180*d/l},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=q(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&q(a)||q(0,0);var b=this.x,c=this.y;return this.x=f((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=w(a.theta(q(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN}};var r=a.Rect=function(a,b,c,d){return this instanceof r?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new r(a,b,c,d)};a.Rect.fromEllipse=function(a){return a=o(a),r(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},a.Rect.prototype={bbox:function(a){var b=w(a||0),f=c(e(b)),g=c(d(b)),h=this.width*g+this.height*f,i=this.width*f+this.height*g;return r(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return q(this.x,this.y+this.height)},bottomLine:function(){return p(this.bottomLeft(),this.corner())},bottomMiddle:function(){return q(this.x+this.width/2,this.y+this.height)},center:function(){return q(this.x+this.width/2,this.y+this.height/2)},clone:function(){return r(this)},containsPoint:function(a){return a=q(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=r(this).normalize(),c=r(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return q(this.x+this.width,this.y+this.height)},equals:function(a){var b=r(this).normalize(),c=r(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=Math.max(b.x,d.x),g=Math.max(b.y,d.y);return r(f,g,Math.min(c.x,e.x)-f,Math.min(c.y,e.y)-g)},intersectionWithLineFromCenterToPoint:function(a,b){a=q(a);var c,d=q(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[p(this.origin(),this.topRight()),p(this.topRight(),this.corner()),p(this.corner(),this.bottomLeft()),p(this.bottomLeft(),this.origin())],f=p(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return p(this.origin(),this.bottomLeft())},leftMiddle:function(){return q(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return q.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return q(this.x,this.y)},pointNearestToPoint:function(a){if(a=q(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return q(this.x+this.width,a.y);case"left":return q(this.x,a.y);case"bottom":return q(a.x,this.y+this.height);case"top":return q(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return p(this.topRight(),this.corner())},rightMiddle:function(){return q(this.x+this.width,this.y+this.height/2)},round:function(a){var b=n(10,a||0);return this.x=j(this.x*b)/b,this.y=j(this.y*b)/b,this.width=j(this.width*b)/b,this.height=j(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(b,c){b=a.Rect(b),c||(c=b.center());var d,e,f,g,h,i,j,k,l=c.x,m=c.y;d=e=f=g=h=i=j=k=1/0;var n=b.origin();n.xl&&(e=(this.x+this.width-l)/(o.x-l)),o.y>m&&(i=(this.y+this.height-m)/(o.y-m));var p=b.topRight();p.x>l&&(f=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:Math.min(d,e,f,g),sy:Math.min(h,i,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return Math.min(c.sx,c.sy)},sideNearestToPoint:function(a){a=q(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cc.x&&(c=d[a]);var e=[];for(b=d.length,a=0;a2){var h=e[e.length-1];e.unshift(h)}for(var i,j,k,l,m,n,o={},p=[];0!==e.length;)if(i=e.pop(),j=i[0],!o.hasOwnProperty(i[0]+"@@"+i[1]))for(var q=!1;!q;)if(p.length<2)p.push(i),q=!0;else{k=p.pop(),l=k[0],m=p.pop(),n=m[0];var r=n.cross(l,j);if(r<0)p.push(m),p.push(k),p.push(i),q=!0;else if(0===r){var t=1e-10,u=l.angleBetween(n,j);Math.abs(u-180)2&&p.pop();var v,w=-1;for(b=p.length,a=0;a0){var z=p.slice(w),A=p.slice(0,w);y=z.concat(A)}else y=p;var B=[];for(b=y.length,a=0;a1){var g,h,i=[];for(g=0,h=f.childNodes.length;gu&&(u=z),c=d("tspan",y.attrs),b.includeAnnotationIndices&&c.attr("annotations",y.annotations),y.attrs.class&&c.addClass(y.attrs.class),e&&x===w&&p!==s&&(y.t+=e),c.node.textContent=y.t}else e&&x===w&&p!==s&&(y+=e),c=document.createTextNode(y||" ");r.append(c)}"auto"===b.lineHeight&&u&&0!==p&&r.attr("dy",1.2*u+"px")}else e&&p!==s&&(t+=e),r.node.textContent=t;0===p&&(o=u)}else r.addClass("v-empty-line"),r.node.style.fillOpacity=0,r.node.style.strokeOpacity=0,r.node.textContent="-";d(g).append(r),l+=t.length+1}var A=this.attr("y");return null===A&&this.attr("y",o||"0.8em"),this},d.prototype.removeAttr=function(a){var b=d.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},d.prototype.attr=function(a,b){if(d.isUndefined(a)){for(var c=this.node.attributes,e={},f=0;f'+(a||"")+"",f=d.parseXML(e,{async:!1});return f.documentElement},d.idCounter=0,d.uniqueId=function(){return"v-"+ ++d.idCounter},d.toNode=function(a){return d.isV(a)?a.node:a.nodeName&&a||a[0]},d.ensureId=function(a){return a=d.toNode(a),a.id||(a.id=d.uniqueId())},d.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},d.isUndefined=function(a){return"undefined"==typeof a},d.isString=function(a){return"string"==typeof a},d.isObject=function(a){return a&&"object"==typeof a},d.isArray=Array.isArray,d.parseXML=function(a,b){b=b||{};var c;try{var e=new DOMParser;d.isUndefined(b.async)||(e.async=b.async),c=e.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},d.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var c=a.split(":");return{ns:b[c[0]],local:c[1]}}return{ns:null,local:a}},d.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,d.transformSeparatorRegex=/[ ,]+/,d.transformationListRegex=/^(\w+)\((.*)\)/,d.transformStringToMatrix=function(a){var b=d.createSVGMatrix(),c=a&&a.match(d.transformRegex);if(!c)return b;for(var e=0,f=c.length;e=0){var g=d.transformStringToMatrix(a),h=d.decomposeMatrix(g);b=[h.translateX,h.translateY],e=[h.scaleX,h.scaleY],c=[h.rotation];var i=[];0===b[0]&&0===b[0]||i.push("translate("+b+")"),1===e[0]&&1===e[1]||i.push("scale("+e+")"),0!==c[0]&&i.push("rotate("+c+")"),a=i.join(" ")}else{var j=a.match(/translate\((.*?)\)/);j&&(b=j[1].split(f));var k=a.match(/rotate\((.*?)\)/);k&&(c=k[1].split(f));var l=a.match(/scale\((.*?)\)/);l&&(e=l[1].split(f))}}var m=e&&e[0]?parseFloat(e[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:m,sy:e&&e[1]?parseFloat(e[1]):m}}},d.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},d.decomposeMatrix=function(a){var b=d.deltaTransformPoint(a,{x:0,y:1}),c=d.deltaTransformPoint(a,{x:1,y:0}),e=180/Math.PI*Math.atan2(b.y,b.x)-90,f=180/Math.PI*Math.atan2(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:Math.sqrt(a.a*a.a+a.b*a.b),scaleY:Math.sqrt(a.c*a.c+a.d*a.d),skewX:e,skewY:f,rotation:e}},d.matrixToScale=function(a){var b,c,e,f;return a?(b=d.isUndefined(a.a)?1:a.a,f=d.isUndefined(a.d)?1:a.d,c=a.b,e=a.c):b=f=1,{sx:c?Math.sqrt(b*b+c*c):b,sy:e?Math.sqrt(e*e+f*f):f}},d.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=d.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(Math.atan2(b.y,b.x))-90)}},d.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},d.isV=function(a){return a instanceof d},d.isVElement=d.isV;var e=d("svg").node;return d.createSVGMatrix=function(a){var b=e.createSVGMatrix();for(var c in a)b[c]=a[c];return b},d.createSVGTransform=function(a){return d.isUndefined(a)?e.createSVGTransform():(a instanceof SVGMatrix||(a=d.createSVGMatrix(a)),e.createSVGTransformFromMatrix(a))},d.createSVGPoint=function(a,b){var c=e.createSVGPoint();return c.x=a,c.y=b,c},d.transformRect=function(a,b){var c=e.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var f=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var h=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var i=c.matrixTransform(b),j=Math.min(d.x,f.x,h.x,i.x),k=Math.max(d.x,f.x,h.x,i.x),l=Math.min(d.y,f.y,h.y,i.y),m=Math.max(d.y,f.y,h.y,i.y);return g.Rect(j,l,k-j,m-l)},d.transformPoint=function(a,b){return g.Point(d.createSVGPoint(a.x,a.y).matrixTransform(b))},d.styleToObject=function(a){for(var b={},c=a.split(";"),d=0;d=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L"+f*n+","+f*o+"A"+f+","+f+" 0 "+k+",0 "+f*l+","+f*m+"Z":"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L0,0Z"},d.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?d.isObject(a[c])&&d.isObject(b[c])?a[c]=d.mergeAttrs(a[c],b[c]):d.isObject(a[c])?a[c]=d.mergeAttrs(a[c],d.styleToObject(b[c])):d.isObject(b[c])?a[c]=d.mergeAttrs(d.styleToObject(a[c]),b[c]):a[c]=d.mergeAttrs(d.styleToObject(a[c]),d.styleToObject(b[c])):a[c]=b[c];return a},d.annotateString=function(a,b,c){b=b||[],c=c||{};for(var e,f,g,h=c.offset||0,i=[],j=[],k=0;k=n&&k=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},d.convertLineToPathData=function(a){a=d(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},d.convertPolygonToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b)+" Z":null},d.convertPolylineToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b):null},d.svgPointsToPath=function(a){var b;for(b=0;b0){var f=joint.util.getByPath(a,d,c);f&&delete f[e]}else delete a[e];return a},flattenObject:function(a,b,c){b=b||"/";var d={};for(var e in a)if(a.hasOwnProperty(e)){var f="object"==typeof a[e];if(f&&c&&c(a[e])&&(f=!1),f){var g=this.flattenObject(a[e],b,c);for(var h in g)g.hasOwnProperty(h)&&(d[e+b+h]=g[h])}else d[e]=a[e]}return d},uuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"==a?b:3&b|8;return c.toString(16)})},guid:function(a){return this.guid.id=this.guid.id||1,a.id=void 0===a.id?"j_"+this.guid.id++:a.id,a.id},toKebabCase:function(a){return a.replace(/[A-Z]/g,"-$&").toLowerCase()},mixin:_.assign,supplement:_.defaults,deepMixin:_.mixin,deepSupplement:_.defaultsDeep,normalizeEvent:function(a){var b=a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0];if(b){for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return a},nextFrame:function(){var a;if("undefined"!=typeof window&&(a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame),!a){var b=0;a=function(a){var c=(new Date).getTime(),d=Math.max(0,16-(c-b)),e=setTimeout(function(){a(c+d)},d);return b=c+d,e}}return function(b,c){return a(c?b.bind(c):b)}}(),cancelFrame:function(){var a,b="undefined"!=typeof window;return b&&(a=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame),a=a||clearTimeout,b?a.bind(window):a}(),shapePerimeterConnectionPoint:function(a,b,c,d){var e,f;if(!c){var g=b.$(".scalable")[0],h=b.$(".rotatable")[0];g&&g.firstChild?c=g.firstChild:h&&h.firstChild&&(c=h.firstChild)}return c?(f=V(c).findIntersection(d,a.paper.viewport),f||(e=V(c).getBBox({target:a.paper.viewport}))):(e=b.model.getBBox(),f=e.intersectionWithLineFromCenterToPoint(d)),f||e.center()},parseCssNumeric:function(a,b){b=b||[];var c={value:parseFloat(a)};if(Number.isNaN(c.value))return null;var d=b.join("|");if(joint.util.isString(a)){var e=new RegExp("(\\d+)("+d+")$").exec(a);if(!e)return null;e[2]&&(c.unit=e[2])}return c},breakText:function(a,b,c,d){d=d||{};var e=b.width,f=b.height,g=d.svgDocument||V("svg").node,h=V("").attr(c||{}).node,i=h.firstChild,j=document.createTextNode("");h.style.opacity=0,h.style.display="block",i.style.display="block",i.appendChild(j),g.appendChild(h),d.svgDocument||document.body.appendChild(g);for(var k,l,m=a.split(" "),n=[],o=[],p=0,q=0,r=m.length;pf){o.splice(Math.floor(f/l));break}}}return d.svgDocument?g.removeChild(h):document.body.removeChild(g),o.join("\n")},imageToDataUri:function(a,b){if(!a||"data:"===a.substr(0,"data:".length))return setTimeout(function(){b(null,a)},0);var c=function(b,c){if(200===b.status){var d=new FileReader;d.onload=function(a){var b=a.target.result;c(null,b)},d.onerror=function(){c(new Error("Failed to load image "+a))},d.readAsDataURL(b.response)}else c(new Error("Failed to load image "+a))},d=function(b,c){var d=function(a){for(var b=32768,c=[],d=0;d=1)return 1;var b=a*a,c=b*a;return 4*(a<.5?c:3*(a-b)+c-.75)},exponential:function(a){return Math.pow(2,10*(a-1))},bounce:function(a){for(var b=0,c=1;1;b+=c,c/=2)if(a>=(7-4*b)/11){var d=(11-6*b-11*a)/4;return-d*d+c*c}},reverse:function(a){return function(b){return 1-a(1-b)}},reflect:function(a){return function(b){return.5*(b<.5?a(2*b):2-a(2-2*b))}},clamp:function(a,b,c){return b=b||0,c=c||1,function(d){var e=a(d);return ec?c:e}},back:function(a){return a||(a=1.70158),function(b){return b*b*((a+1)*b-a)}},elastic:function(a){return a||(a=1.5),function(b){return Math.pow(2,10*(b-1))*Math.cos(20*Math.PI*a/3*b)}}},interpolate:{number:function(a,b){var c=b-a;return function(b){return a+c*b}},object:function(a,b){var c=Object.keys(a);return function(d){var e,f,g={};for(e=c.length-1;e!=-1;e--)f=c[e],g[f]=a[f]+(b[f]-a[f])*d;return g}},hexColor:function(a,b){var c=parseInt(a.slice(1),16),d=parseInt(b.slice(1),16),e=255&c,f=(255&d)-e,g=65280&c,h=(65280&d)-g,i=16711680&c,j=(16711680&d)-i;return function(a){var b=e+f*a&255,c=g+h*a&65280,d=i+j*a&16711680;return"#"+(1<<24|b|c|d).toString(16).slice(1)}},unit:function(a,b){var c=/(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/,d=c.exec(a),e=c.exec(b),f=e[1].indexOf("."),g=f>0?e[1].length-f-1:0;a=+d[1];var h=+e[1]-a,i=d[2];return function(b){return(a+h*b).toFixed(g)+i}}},filter:{outline:function(a){var b='',c=Number.isFinite(a.margin)?a.margin:2,d=Number.isFinite(a.width)?a.width:1;return joint.util.template(b)({color:a.color||"blue",opacity:Number.isFinite(a.opacity)?a.opacity:1,outerRadius:c+d,innerRadius:c})},highlight:function(a){var b='';return joint.util.template(b)({color:a.color||"red",width:Number.isFinite(a.width)?a.width:1,blur:Number.isFinite(a.blur)?a.blur:0,opacity:Number.isFinite(a.opacity)?a.opacity:1})},blur:function(a){var b=Number.isFinite(a.x)?a.x:2;return joint.util.template('')({stdDeviation:Number.isFinite(a.y)?[b,a.y]:b})},dropShadow:function(a){var b="SVGFEDropShadowElement"in window?'':'';return joint.util.template(b)({dx:a.dx||0,dy:a.dy||0,opacity:Number.isFinite(a.opacity)?a.opacity:1,color:a.color||"black",blur:Number.isFinite(a.blur)?a.blur:4})},grayscale:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},sepia:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},saturate:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:1-b})},hueRotate:function(a){return joint.util.template('')({angle:a.angle||0})},invert:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:1-b})},brightness:function(a){return joint.util.template('')({amount:Number.isFinite(a.amount)?a.amount:1})},contrast:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:.5-b/2})}},format:{number:function(a,b,c){function d(a){for(var b=a.length,d=[],e=0,f=c.grouping[0];b>0&&f>0;)d.push(a.substring(b-=f,b+f)),f=c.grouping[e=(e+1)%c.grouping.length];return d.reverse().join(c.thousands)}c=c||{currency:["$",""],decimal:".",thousands:",",grouping:[3]};var e=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,f=e.exec(a),g=f[1]||" ",h=f[2]||">",i=f[3]||"",j=f[4]||"",k=f[5],l=+f[6],m=f[7],n=f[8],o=f[9],p=1,q="",r="",s=!1;switch(n&&(n=+n.substring(1)),(k||"0"===g&&"="===h)&&(k=g="0",h="=",m&&(l-=Math.floor((l-1)/4))),o){case"n":m=!0,o="g";break;case"%":p=100,r="%",o="f";break;case"p":p=100,r="%",o="r";break;case"b":case"o":case"x":case"X":"#"===j&&(q="0"+o.toLowerCase());break;case"c":case"d":s=!0,n=0;break;case"s":p=-1,o="r"}"$"===j&&(q=c.currency[0],r=c.currency[1]),"r"!=o||n||(o="g"),null!=n&&("g"==o?n=Math.max(1,Math.min(21,n)):"e"!=o&&"f"!=o||(n=Math.max(0,Math.min(20,n))));var t=k&&m;if(s&&b%1)return"";var u=b<0||0===b&&1/b<0?(b=-b,"-"):i,v=r;if(p<0){var w=this.prefix(b,n);b=w.scale(b),v=w.symbol+r}else b*=p;b=this.convert(o,b,n);var x=b.lastIndexOf("."),y=x<0?b:b.substring(0,x),z=x<0?"":c.decimal+b.substring(x+1);!k&&m&&c.grouping&&(y=d(y));var A=q.length+y.length+z.length+(t?0:u.length),B=A"===h?B+u+b:"^"===h?B.substring(0,A>>=1)+u+b+B.substring(A):u+(t?b:B+b))+v},string:function(a,b){for(var c,d="{",e=!1,f=[];(c=a.indexOf(d))!==-1;){var g,h,i;if(g=a.slice(0,c),e){h=g.split(":"),i=h.shift().split("."),g=b;for(var j=0;j8?function(a){return a/c}:function(a){return a*c},symbol:a}}),d=0;return a&&(a<0&&(a*=-1),b&&(a=this.round(a,this.precision(a,b))),d=1+Math.floor(1e-12+Math.log(a)/Math.LN10),d=Math.max(-24,Math.min(24,3*Math.floor((d<=0?d+1:d-1)/3)))),c[8+d/3]}},template:function(a){var b=/<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g;return function(c){return c=c||{},a.replace(b,function(a){for(var b=Array.from(arguments),d=b.slice(1,4).find(function(a){return!!a}),e=d.split("."),f=c[e.shift()];void 0!==f&&e.length;)f=f[e.shift()];return void 0!==f?f:""})}},toggleFullScreen:function(a){function b(a,b){for(var c=["webkit","moz","ms","o",""],d=0;d0&&b[0]||[],e=c>1&&b[c-1]||{};return Array.isArray(d)||(e instanceof joint.dia.Cell?d=b:d instanceof joint.dia.Cell&&(b.length>1&&b.pop(),d=b)),e instanceof joint.dia.Cell&&(e={}),a.call(this,d,e)}}},sortedIndex:_.sortedIndexBy||_.sortedIndex,uniq:_.uniqBy||_.uniq,uniqueId:_.uniqueId,sortBy:_.sortBy,isFunction:_.isFunction,result:_.result,union:_.union,invoke:_.invokeMap||_.invoke,difference:_.difference,intersection:_.intersection,omit:_.omit,pick:_.pick,has:_.has,bindAll:_.bindAll,assign:_.assign,defaults:_.defaults,defaultsDeep:_.defaultsDeep,isPlainObject:_.isPlainObject,isEmpty:_.isEmpty,isEqual:_.isEqual,noop:function(){},cloneDeep:_.cloneDeep,toArray:_.toArray,flattenDeep:_.flattenDeep,camelCase:_.camelCase,groupBy:_.groupBy,forIn:_.forIn,without:_.without,debounce:_.debounce,clone:_.clone,isBoolean:function(a){var b=Object.prototype.toString;return a===!0||a===!1||!!a&&"object"==typeof a&&"[object Boolean]"===b.call(a)},isObject:function(a){return!!a&&("object"==typeof a||"function"==typeof a)},isNumber:function(a){var b=Object.prototype.toString;return"number"==typeof a||!!a&&"object"==typeof a&&"[object Number]"===b.call(a)},isString:function(a){var b=Object.prototype.toString;return"string"==typeof a||!!a&&"object"==typeof a&&"[object String]"===b.call(a)},merge:function(){if(_.mergeWith){var a=Array.from(arguments),b=a[a.length-1],c=this.isFunction(b)?b:this.noop;return a.push(function(a,b){var d=c(a,b);return void 0!==d?d:Array.isArray(a)&&!Array.isArray(b)?b:void 0}),_.mergeWith.apply(this,a)}return _.merge.apply(this,arguments)}}};joint.mvc.View=Backbone.View.extend({options:{},theme:null,themeClassNamePrefix:joint.util.addClassNamePrefix("theme-"),requireSetThemeOverride:!1,defaultTheme:joint.config.defaultTheme,constructor:function(a){this.requireSetThemeOverride=a&&!!a.theme,this.options=joint.util.assign({},this.options,a),Backbone.View.call(this,a)},initialize:function(a){joint.util.bindAll(this,"setTheme","onSetTheme","remove","onRemove"),joint.mvc.views[this.cid]=this,this.setTheme(this.options.theme||this.defaultTheme),this.init()},_ensureElement:function(){if(this.el)this.setElement(joint.util.result(this,"el"));else{var a=joint.util.result(this,"tagName"),b=joint.util.assign({},joint.util.result(this,"attributes"));this.id&&(b.id=joint.util.result(this,"id")),this.setElement(this._createElement(a)),this._setAttributes(b)}this._ensureElClassName()},_setAttributes:function(a){this.svgElement?this.vel.attr(a):this.$el.attr(a)},_createElement:function(a){return this.svgElement?document.createElementNS(V.namespace.xmlns,a):document.createElement(a)},_setElement:function(a){this.$el=a instanceof Backbone.$?a:Backbone.$(a),this.el=this.$el[0],this.svgElement&&(this.vel=V(this.el))},_ensureElClassName:function(){var a=joint.util.result(this,"className"),b=joint.util.addClassNamePrefix(a);this.svgElement?this.vel.removeClass(a).addClass(b):this.$el.removeClass(a).addClass(b)},init:function(){},onRender:function(){},setTheme:function(a,b){return b=b||{},this.theme&&this.requireSetThemeOverride&&!b.override?this:(this.removeThemeClassName(),this.addThemeClassName(a),this.onSetTheme(this.theme,a),this.theme=a,this)},addThemeClassName:function(a){a=a||this.theme;var b=this.themeClassNamePrefix+a;return this.$el.addClass(b),this},removeThemeClassName:function(a){a=a||this.theme;var b=this.themeClassNamePrefix+a;return this.$el.removeClass(b),this},onSetTheme:function(a,b){},remove:function(){return this.onRemove(),joint.mvc.views[this.cid]=null,Backbone.View.prototype.remove.apply(this,arguments),this},onRemove:function(){},getEventNamespace:function(){return".joint-event-ns-"+this.cid}},{extend:function(){var a=Array.from(arguments),b=a[0]&&joint.util.assign({},a[0])||{},c=a[1]&&joint.util.assign({},a[1])||{},d=b.render||this.prototype&&this.prototype.render||null;return b.render=function(){return d&&d.apply(this,arguments),this.onRender(),this},Backbone.View.extend.call(this,b,c)}}),joint.dia.GraphCells=Backbone.Collection.extend({cellNamespace:joint.shapes,initialize:function(a,b){b.cellNamespace&&(this.cellNamespace=b.cellNamespace),this.graph=b.graph},model:function(a,b){var c=b.collection,d=c.cellNamespace,e="link"===a.type?joint.dia.Link:joint.util.getByPath(d,a.type,".")||joint.dia.Element,f=new e(a,b);return f.graph=c.graph,f},comparator:function(a){return a.get("z")||0}}),joint.dia.Graph=Backbone.Model.extend({_batches:{},initialize:function(a,b){b=b||{};var c=new joint.dia.GraphCells([],{model:b.cellModel,cellNamespace:b.cellNamespace,graph:this});Backbone.Model.prototype.set.call(this,"cells",c),c.on("all",this.trigger,this),this.on("change:z",this._sortOnChangeZ,this),this.on("batch:stop",this._onBatchStop,this),this._out={},this._in={},this._nodes={},this._edges={},c.on("add",this._restructureOnAdd,this),c.on("remove",this._restructureOnRemove,this),c.on("reset",this._restructureOnReset,this),c.on("change:source",this._restructureOnChangeSource,this),c.on("change:target",this._restructureOnChangeTarget,this),c.on("remove",this._removeCell,this)},_sortOnChangeZ:function(){this.hasActiveBatch("to-front")||this.hasActiveBatch("to-back")||this.get("cells").sort()},_onBatchStop:function(a){var b=a&&a.batchName;"to-front"!==b&&"to-back"!==b||this.hasActiveBatch(b)||this.get("cells").sort()},_restructureOnAdd:function(a){if(a.isLink()){this._edges[a.id]=!0;var b=a.get("source"),c=a.get("target");b.id&&((this._out[b.id]||(this._out[b.id]={}))[a.id]=!0),c.id&&((this._in[c.id]||(this._in[c.id]={}))[a.id]=!0)}else this._nodes[a.id]=!0},_restructureOnRemove:function(a){if(a.isLink()){delete this._edges[a.id];var b=a.get("source"),c=a.get("target");b.id&&this._out[b.id]&&this._out[b.id][a.id]&&delete this._out[b.id][a.id],c.id&&this._in[c.id]&&this._in[c.id][a.id]&&delete this._in[c.id][a.id]}else delete this._nodes[a.id]},_restructureOnReset:function(a){a=a.models,this._out={},this._in={},this._nodes={},this._edges={},a.forEach(this._restructureOnAdd,this)},_restructureOnChangeSource:function(a){var b=a.previous("source");b.id&&this._out[b.id]&&delete this._out[b.id][a.id];var c=a.get("source");c.id&&((this._out[c.id]||(this._out[c.id]={}))[a.id]=!0)},_restructureOnChangeTarget:function(a){var b=a.previous("target");b.id&&this._in[b.id]&&delete this._in[b.id][a.id];var c=a.get("target");c.id&&((this._in[c.id]||(this._in[c.id]={}))[a.id]=!0)},getOutboundEdges:function(a){return this._out&&this._out[a]||{}},getInboundEdges:function(a){return this._in&&this._in[a]||{}},toJSON:function(){var a=Backbone.Model.prototype.toJSON.apply(this,arguments);return a.cells=this.get("cells").toJSON(),a},fromJSON:function(a,b){if(!a.cells)throw new Error("Graph JSON must contain cells array.");return this.set(a,b)},set:function(a,b,c){var d;return"object"==typeof a?(d=a,c=b):(d={})[a]=b,d.hasOwnProperty("cells")&&(this.resetCells(d.cells,c),d=joint.util.omit(d,"cells")),Backbone.Model.prototype.set.call(this,d,c)},clear:function(a){a=joint.util.assign({},a,{clear:!0});var b=this.get("cells");if(0===b.length)return this;this.startBatch("clear",a);var c=b.sortBy(function(a){return a.isLink()?1:2});do c.shift().remove(a);while(c.length>0);return this.stopBatch("clear"),this},_prepareCell:function(a,b){var c;if(a instanceof Backbone.Model?(c=a.attributes,a.graph||b&&b.dry||(a.graph=this)):c=a,!joint.util.isString(c.type))throw new TypeError("dia.Graph: cell type must be a string.");return a},maxZIndex:function(){var a=this.get("cells").last();return a?a.get("z")||0:0},addCell:function(a,b){return Array.isArray(a)?this.addCells(a,b):(a instanceof Backbone.Model?a.has("z")||a.set("z",this.maxZIndex()+1):void 0===a.z&&(a.z=this.maxZIndex()+1),this.get("cells").add(this._prepareCell(a,b),b||{}),this)},addCells:function(a,b){return a.length&&(a=joint.util.flattenDeep(a),b.position=a.length,this.startBatch("add"),a.forEach(function(a){b.position--,this.addCell(a,b)},this),this.stopBatch("add")),this},resetCells:function(a,b){var c=joint.util.toArray(a).map(function(a){return this._prepareCell(a,b)},this);return this.get("cells").reset(c,b),this},removeCells:function(a,b){return a.length&&(this.startBatch("remove"),joint.util.invoke(a,"remove",b),this.stopBatch("remove")),this},_removeCell:function(a,b,c){c=c||{},c.clear||(c.disconnectLinks?this.disconnectLinks(a,c):this.removeLinks(a,c)),this.get("cells").remove(a,{silent:!0}),a.graph===this&&(a.graph=null)},getCell:function(a){return this.get("cells").get(a)},getCells:function(){return this.get("cells").toArray()},getElements:function(){return Object.keys(this._nodes).map(this.getCell,this)},getLinks:function(){return Object.keys(this._edges).map(this.getCell,this)},getFirstCell:function(){return this.get("cells").first()},getLastCell:function(){return this.get("cells").last()},getConnectedLinks:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=[],f={};if(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),b.deep){var g=a.getEmbeddedCells({deep:!0}),h={};g.forEach(function(a){a.isLink()&&(h[a.id]=!0)}),g.forEach(function(a){a.isLink()||(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)))},this)}return e},getNeighbors:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=this.getConnectedLinks(a,b).reduce(function(e,f){var g=f.get("source"),h=f.get("target"),i=f.hasLoop(b);if(c&&joint.util.has(g,"id")&&!e[g.id]){var j=this.getCell(g.id);!i&&(!j||j===a||b.deep&&j.isEmbeddedIn(a))||(e[g.id]=j)}if(d&&joint.util.has(h,"id")&&!e[h.id]){var k=this.getCell(h.id);!i&&(!k||k===a||b.deep&&k.isEmbeddedIn(a))||(e[h.id]=k)}return e}.bind(this),{});return joint.util.toArray(e)},getCommonAncestor:function(){var a=Array.from(arguments).map(function(a){for(var b=[],c=a.get("parent");c;)b.push(c),c=this.getCell(c).get("parent");return b},this);a=a.sort(function(a,b){return a.length-b.length});var b=joint.util.toArray(a.shift()).find(function(b){return a.every(function(a){return a.includes(b)})});return this.getCell(b)},getSuccessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{outbound:!0})),c},cloneCells:function(a){a=joint.util.uniq(a);var b=joint.util.toArray(a).reduce(function(a,b){return a[b.id]=b.clone(),a},{});return joint.util.toArray(a).forEach(function(a){var c=b[a.id];if(c.isLink()){var d=c.get("source"),e=c.get("target");d.id&&b[d.id]&&c.prop("source/id",b[d.id].id),e.id&&b[e.id]&&c.prop("target/id",b[e.id].id)}var f=a.get("parent");f&&b[f]&&c.set("parent",b[f].id);var g=joint.util.toArray(a.get("embeds")).reduce(function(a,c){return b[c]&&a.push(b[c].id),a},[]);joint.util.isEmpty(g)||c.set("embeds",g)}),b},cloneSubgraph:function(a,b){var c=this.getSubgraph(a,b);return this.cloneCells(c)},getSubgraph:function(a,b){b=b||{};var c=[],d={},e=[],f=[];return joint.util.toArray(a).forEach(function(a){if(d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a)),b.deep){var g=a.getEmbeddedCells({deep:!0});g.forEach(function(a){d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a))})}}),f.forEach(function(a){var b=a.get("source"),f=a.get("target");if(b.id&&!d[b.id]){var g=this.getCell(b.id);c.push(g),d[g.id]=g,e.push(g)}if(f.id&&!d[f.id]){var h=this.getCell(f.id);c.push(this.getCell(f.id)),d[h.id]=h,e.push(h)}},this),e.forEach(function(a){var e=this.getConnectedLinks(a,b);e.forEach(function(a){var b=a.get("source"),e=a.get("target");!d[a.id]&&b.id&&d[b.id]&&e.id&&d[e.id]&&(c.push(a),d[a.id]=a)})},this),c},getPredecessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{inbound:!0})),c},search:function(a,b,c){c=c||{},c.breadthFirst?this.bfs(a,b,c):this.dfs(a,b,c)},bfs:function(a,b,c){c=c||{};var d={},e={},f=[];for(f.push(a),e[a.id]=0;f.length>0;){var g=f.shift();if(!d[g.id]){if(d[g.id]=!0,b(g,e[g.id])===!1)return;this.getNeighbors(g,c).forEach(function(a){e[a.id]=e[g.id]+1,f.push(a)})}}},dfs:function(a,b,c,d,e){c=c||{};var f=d||{},g=e||0;b(a,g)!==!1&&(f[a.id]=!0,this.getNeighbors(a,c).forEach(function(a){f[a.id]||this.dfs(a,b,c,f,g+1)},this))},getSources:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._in[c]&&!joint.util.isEmpty(this._in[c])||a.push(this.getCell(c))}.bind(this)),a},getSinks:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._out[c]&&!joint.util.isEmpty(this._out[c])||a.push(this.getCell(c))}.bind(this)),a},isSource:function(a){return!this._in[a.id]||joint.util.isEmpty(this._in[a.id])},isSink:function(a){return!this._out[a.id]||joint.util.isEmpty(this._out[a.id])},isSuccessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{outbound:!0}),c},isPredecessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{inbound:!0}),c},isNeighbor:function(a,b,c){c=c||{};var d=c.inbound,e=c.outbound;void 0===d&&void 0===e&&(d=e=!0);var f=!1;return this.getConnectedLinks(a,c).forEach(function(a){var c=a.get("source"),g=a.get("target");return d&&joint.util.has(c,"id")&&c.id===b.id?(f=!0,!1):e&&joint.util.has(g,"id")&&g.id===b.id?(f=!0,!1):void 0}),f},disconnectLinks:function(a,b){this.getConnectedLinks(a).forEach(function(c){c.set(c.get("source").id===a.id?"source":"target",{x:0,y:0},b)})},removeLinks:function(a,b){joint.util.invoke(this.getConnectedLinks(a),"remove",b)},findModelsFromPoint:function(a){return this.getElements().filter(function(b){return b.getBBox().containsPoint(a)})},findModelsInArea:function(a,b){a=g.rect(a),b=joint.util.defaults(b||{},{strict:!1});var c=b.strict?"containsRect":"intersect";return this.getElements().filter(function(b){return a[c](b.getBBox())})},findModelsUnderElement:function(a,b){b=joint.util.defaults(b||{},{searchBy:"bbox"});var c=a.getBBox(),d="bbox"===b.searchBy?this.findModelsInArea(c):this.findModelsFromPoint(c[b.searchBy]());return d.filter(function(b){return a.id!==b.id&&!b.isEmbeddedIn(a)})},getBBox:function(a,b){return this.getCellsBBox(a||this.getElements(),b)},getCellsBBox:function(a,b){return joint.util.toArray(a).reduce(function(a,c){return c.isLink()?a:a?a.union(c.getBBox(b)):c.getBBox(b)},null)},translate:function(a,b,c){var d=this.getCells().filter(function(a){return!a.isEmbedded()});return joint.util.invoke(d,"translate",a,b,c),this},resize:function(a,b,c){return this.resizeCells(a,b,this.getCells(),c)},resizeCells:function(a,b,c,d){var e=this.getCellsBBox(c);if(e){var f=Math.max(a/e.width,0),g=Math.max(b/e.height,0);joint.util.invoke(c,"scale",f,g,e.origin(),d)}return this; -},startBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)+1,this.trigger("batch:start",joint.util.assign({},b,{batchName:a}))},stopBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)-1,this.trigger("batch:stop",joint.util.assign({},b,{batchName:a}))},hasActiveBatch:function(a){return a?!!this._batches[a]:joint.util.toArray(this._batches).some(function(a){return a>0})}}),joint.util.wrapWith(joint.dia.Graph.prototype,["resetCells","addCells","removeCells"],"cells"),function(a,b,c,d,e){function f(a){return e.isString(a)&&"%"===a.slice(-1)}function g(a,b){return function(c,d){var e=f(c);c=parseFloat(c),e&&(c/=100);var g={};if(isFinite(c)){var h=e||c>=0&&c<=1?c*d[b]:Math.max(c+d[b],0);g[a]=h}return g}}function h(a,b,d){return function(e,g){var h=f(e);e=parseFloat(e),h&&(e/=100);var i;if(isFinite(e)){var j=g[d]();i=h||e>0&&e<1?j[a]+g[b]*e:j[a]+e}var k=c.Point();return k[a]=i||0,k}}function i(a,b,d){return function(e,g){var h;h="middle"===e?g[b]/2:e===d?g[b]:isFinite(e)?e>-1&&e<1?-g[b]*e:-e:f(e)?g[b]*parseFloat(e)/100:0;var i=c.Point();return i[a]=-(g[a]+h),i}}var j=a.dia.attributes={xlinkHref:{set:"xlink:href"},xlinkShow:{set:"xlink:show"},xlinkRole:{set:"xlink:role"},xlinkType:{set:"xlink:type"},xlinkArcrole:{set:"xlink:arcrole"},xlinkTitle:{set:"xlink:title"},xlinkActuate:{set:"xlink:actuate"},xmlSpace:{set:"xml:space"},xmlBase:{set:"xml:base"},xmlLang:{set:"xml:lang"},preserveAspectRatio:{set:"preserveAspectRatio"},requiredExtension:{set:"requiredExtension"},requiredFeatures:{set:"requiredFeatures"},systemLanguage:{set:"systemLanguage"},externalResourcesRequired:{set:"externalResourceRequired"},filter:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineFilter(a)+")"}},fill:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},stroke:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},sourceMarker:{qualify:e.isPlainObject,set:function(a){return{"marker-start":"url(#"+this.paper.defineMarker(a)+")"}}},targetMarker:{qualify:e.isPlainObject,set:function(a){return a=e.assign({transform:"rotate(180)"},a),{"marker-end":"url(#"+this.paper.defineMarker(a)+")"}}},vertexMarker:{qualify:e.isPlainObject,set:function(a){return{"marker-mid":"url(#"+this.paper.defineMarker(a)+")"}}},text:{set:function(b,c,e,f){var g=d(e),h="joint-text",i=g.data(h),j=a.util.pick(f,"lineHeight","annotations","textPath","x","eol"),k=j.fontSize=f["font-size"]||f.fontSize,l=JSON.stringify([b,j]);void 0!==i&&i===l||(k&&e.setAttribute("font-size",k),V(e).text(""+b,j),g.data(h,l))}},textWrap:{qualify:e.isPlainObject,set:function(b,c,d,e){var g=b.width||0;f(g)?c.width*=parseFloat(g)/100:g<=0?c.width+=g:c.width=g;var h=b.height||0;f(h)?c.height*=parseFloat(h)/100:h<=0?c.height+=h:c.height=h;var i=a.util.breakText(""+b.text,c,{"font-weight":e["font-weight"]||e.fontWeight,"font-size":e["font-size"]||e.fontSize,"font-family":e["font-family"]||e.fontFamily},{svgDocument:this.paper.svg});V(d).text(i)}},lineHeight:{qualify:function(a,b,c){return void 0!==c.text}},textPath:{qualify:function(a,b,c){return void 0!==c.text}},annotations:{qualify:function(a,b,c){return void 0!==c.text}},port:{set:function(a){return null===a||void 0===a.id?a:a.id}},style:{qualify:e.isPlainObject,set:function(a,b,c){d(c).css(a)}},html:{set:function(a,b,c){d(c).html(a+"")}},ref:{},refX:{position:h("x","width","origin")},refY:{position:h("y","height","origin")},refDx:{position:h("x","width","corner")},refDy:{position:h("y","height","corner")},refWidth:{set:g("width","width")},refHeight:{set:g("height","height")},refRx:{set:g("rx","width")},refRy:{set:g("ry","height")},refCx:{set:g("cx","width")},refCy:{set:g("cy","height")},xAlignment:{offset:i("x","width","right")},yAlignment:{offset:i("y","height","bottom")},resetOffset:{offset:function(a,b){return a?{x:-b.x,y:-b.y}:{x:0,y:0}}}};j.refX2=j.refX,j.refY2=j.refY,j["ref-x"]=j.refX,j["ref-y"]=j.refY,j["ref-dy"]=j.refDy,j["ref-dx"]=j.refDx,j["ref-width"]=j.refWidth,j["ref-height"]=j.refHeight,j["x-alignment"]=j.xAlignment,j["y-alignment"]=j.yAlignment}(joint,_,g,$,joint.util),joint.dia.Cell=Backbone.Model.extend({constructor:function(a,b){var c,d=a||{};this.cid=joint.util.uniqueId("c"),this.attributes={},b&&b.collection&&(this.collection=b.collection),b&&b.parse&&(d=this.parse(d,b)||{}),(c=joint.util.result(this,"defaults"))&&(d=joint.util.merge({},c,d)),this.set(d,b),this.changed={},this.initialize.apply(this,arguments)},translate:function(a,b,c){throw new Error("Must define a translate() method.")},toJSON:function(){var a=this.constructor.prototype.defaults.attrs||{},b=this.attributes.attrs,c={};joint.util.forIn(b,function(b,d){var e=a[d];joint.util.forIn(b,function(a,b){joint.util.isObject(a)&&!Array.isArray(a)?joint.util.forIn(a,function(a,f){e&&e[b]&&joint.util.isEqual(e[b][f],a)||(c[d]=c[d]||{},(c[d][b]||(c[d][b]={}))[f]=a)}):e&&joint.util.isEqual(e[b],a)||(c[d]=c[d]||{},c[d][b]=a)})});var d=joint.util.cloneDeep(joint.util.omit(this.attributes,"attrs"));return d.attrs=c,d},initialize:function(a){a&&a.id||this.set("id",joint.util.uuid(),{silent:!0}),this._transitionIds={},this.processPorts(),this.on("change:attrs",this.processPorts,this)},processPorts:function(){var a=this.ports,b={};joint.util.forIn(this.get("attrs"),function(a,c){a&&a.port&&(void 0!==a.port.id?b[a.port.id]=a.port:b[a.port]={id:a.port})});var c={};if(joint.util.forIn(a,function(a,d){b[d]||(c[d]=!0)}),this.graph&&!joint.util.isEmpty(c)){var d=this.graph.getConnectedLinks(this,{inbound:!0});d.forEach(function(a){c[a.get("target").port]&&a.remove()});var e=this.graph.getConnectedLinks(this,{outbound:!0});e.forEach(function(a){c[a.get("source").port]&&a.remove()})}this.ports=b},remove:function(a){a=a||{};var b=this.graph;b&&b.startBatch("remove");var c=this.get("parent");if(c){var d=b&&b.getCell(c);d.unembed(this)}return joint.util.invoke(this.getEmbeddedCells(),"remove",a),this.trigger("remove",this,this.collection,a),b&&b.stopBatch("remove"),this},toFront:function(a){if(this.graph){a=a||{};var b=(this.graph.getLastCell().get("z")||0)+1;if(this.startBatch("to-front").set("z",b,a),a.deep){var c=this.getEmbeddedCells({deep:!0,breadthFirst:!0});c.forEach(function(c){c.set("z",++b,a)})}this.stopBatch("to-front")}return this},toBack:function(a){if(this.graph){a=a||{};var b=(this.graph.getFirstCell().get("z")||0)-1;if(this.startBatch("to-back"),a.deep){var c=this.getEmbeddedCells({deep:!0,breadthFirst:!0});c.reverse().forEach(function(c){c.set("z",b--,a)})}this.set("z",b,a).stopBatch("to-back")}return this},embed:function(a,b){if(this===a||this.isEmbeddedIn(a))throw new Error("Recursive embedding not allowed.");this.startBatch("embed");var c=joint.util.assign([],this.get("embeds"));return c[a.isLink()?"unshift":"push"](a.id),a.set("parent",this.id,b),this.set("embeds",joint.util.uniq(c),b),this.stopBatch("embed"),this},unembed:function(a,b){return this.startBatch("unembed"),a.unset("parent",b),this.set("embeds",joint.util.without(this.get("embeds"),a.id),b),this.stopBatch("unembed"),this},getAncestors:function(){var a=[],b=this.get("parent");if(!this.graph)return a;for(;void 0!==b;){var c=this.graph.getCell(b);if(void 0===c)break;a.push(c),b=c.get("parent")}return a},getEmbeddedCells:function(a){if(a=a||{},this.graph){var b;if(a.deep)if(a.breadthFirst){b=[];for(var c=this.getEmbeddedCells();c.length>0;){var d=c.shift();b.push(d),c.push.apply(c,d.getEmbeddedCells())}}else b=this.getEmbeddedCells(),b.forEach(function(c){b.push.apply(b,c.getEmbeddedCells(a))});else b=joint.util.toArray(this.get("embeds")).map(this.graph.getCell,this.graph);return b}return[]},isEmbeddedIn:function(a,b){var c=joint.util.isString(a)?a:a.id,d=this.get("parent");if(b=joint.util.defaults({deep:!0},b),this.graph&&b.deep){for(;d;){if(d===c)return!0;d=this.graph.getCell(d).get("parent")}return!1}return d===c},isEmbedded:function(){return!!this.get("parent")},clone:function(a){if(a=a||{},a.deep)return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null,[this].concat(this.getEmbeddedCells({deep:!0}))));var b=Backbone.Model.prototype.clone.apply(this,arguments);return b.set("id",joint.util.uuid()),b.unset("embeds"),b.unset("parent"),b},prop:function(a,b,c){var d="/",e=joint.util.isString(a);if(e||Array.isArray(a)){if(arguments.length>1){var f,g;e?(f=a,g=f.split("/")):(f=a.join(d),g=a.slice());var h=g[0],i=g.length;if(c=c||{},c.propertyPath=f,c.propertyValue=b,c.propertyPathArray=g,1===i)return this.set(h,b,c);for(var j={},k=j,l=h,m=1;m0)},getSelector:function(a,b){if(a===this.el)return b;var c;if(a){var d=V(a).index()+1;c=a.tagName+":nth-child("+d+")",b&&(c+=" > "+b),c=this.getSelector(a.parentNode,c)}return c},getAttributeDefinition:function(a){return this.model.constructor.getAttributeDefinition(a)},setNodeAttributes:function(a,b){joint.util.isEmpty(b)||(a instanceof SVGElement?V(a).attr(b):$(a).attr(b))},processNodeAttributes:function(a,b){var c,d,e,f,g,h,i,j,k,l=[];for(c in b)b.hasOwnProperty(c)&&(d=b[c],e=this.getAttributeDefinition(c),!e||joint.util.isFunction(e.qualify)&&!e.qualify.call(this,d,a,b)?(h||(h={}),h[joint.util.toKebabCase(c)]=d):(joint.util.isString(e.set)&&(h||(h={}),h[e.set]=d),null!==d&&l.push(c,e)));for(f=0,g=l.length;f0&&x.height>0){var y=V.transformRect(a.getBBox(),p).scale(1/r,1/s);for(e in m)f=m[e],h=this.getAttributeDefinition(e),t=h.offset.call(this,f,y,a,i),t&&(q.offset(g.Point(t).scale(r,s)),w||(w=!0))}}(void 0!==o||v||w)&&(q.round(1),p.e=q.x,p.f=q.y,a.setAttribute("transform",V.matrixToTransformString(p)))},getNodeScale:function(a,b){var c,d;if(b&&b.contains(a)){var e=b.scale();c=1/e.sx,d=1/e.sy}else c=1,d=1;return{sx:c,sy:d}},findNodesAttributes:function(a,b,c){var d={};for(var e in a)if(a.hasOwnProperty(e))for(var f=c[e]=this.findBySelector(e,b),g=0,h=f.length;g-1?l.splice(t,0,d):l.push(d)}else this.setNodeAttributes(e,i.normal);for(var u=0,v=l.length;u0){this.startBatch("fit-embeds",a),a.deep&&joint.util.invoke(b,"fitEmbeds",a);var c=this.graph.getCellsBBox(b),d=joint.util.normalizeSides(a.padding);c.moveAndExpand({x:-d.left,y:-d.top,width:d.right+d.left,height:d.bottom+d.top}),this.set({position:{x:c.x,y:c.y},size:{width:c.width,height:c.height}},a),this.stopBatch("fit-embeds")}return this},rotate:function(a,b,c,d){if(c){var e=this.getBBox().center(),f=this.get("size"),g=this.get("position");e.rotate(c,this.get("angle")-a);var h=e.x-f.width/2-g.x,i=e.y-f.height/2-g.y;this.startBatch("rotate",{angle:a,absolute:b,origin:c}),this.position(g.x+h,g.y+i,d),this.rotate(a,b,null,d),this.stopBatch("rotate")}else this.set("angle",b?a:(this.get("angle")+a)%360,d);return this},getBBox:function(a){if(a=a||{},a.deep&&this.graph){var b=this.getEmbeddedCells({deep:!0,breadthFirst:!0});return b.push(this),this.graph.getCellsBBox(b)}var c=this.get("position"),d=this.get("size");return g.rect(c.x,c.y,d.width,d.height)}}),joint.dia.ElementView=joint.dia.CellView.extend({_removePorts:function(){},_renderPorts:function(){},className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("element"),a.join(" ")},initialize:function(){joint.dia.CellView.prototype.initialize.apply(this,arguments);var a=this.model;this.listenTo(a,"change:position",this.translate),this.listenTo(a,"change:size",this.resize),this.listenTo(a,"change:angle",this.rotate),this.listenTo(a,"change:markup",this.render),this._initializePorts()},_initializePorts:function(){},update:function(a,b){this._removePorts();var c=this.model,d=c.attr();this.updateDOMSubtreeAttributes(this.el,d,{rootBBox:g.Rect(c.size()),scalableNode:this.scalableNode,rotatableNode:this.rotatableNode,roAttributes:b===d?null:b}),this._renderPorts()},renderMarkup:function(){var a=this.model.get("markup")||this.model.markup;if(!a)throw new Error("properties.markup is missing while the default render() implementation is used.");var b=joint.util.template(a)(),c=V(b);this.vel.append(c)},render:function(){this.$el.empty(),this.renderMarkup(),this.rotatableNode=this.vel.findOne(".rotatable");var a=this.scalableNode=this.vel.findOne(".scalable");return a&&this.update(),this.resize(),this.rotate(),this.translate(),this},resize:function(a,b,c){var d=this.model,e=d.get("size")||{width:1,height:1},f=d.get("angle")||0,g=this.scalableNode;if(!g)return 0!==f&&this.rotate(),void this.update();var h=!1;g.node.getElementsByTagName("path").length>0&&(h=!0);var i=g.getBBox({recursive:h}),j=e.width/(i.width||1),k=e.height/(i.height||1);g.attr("transform","scale("+j+","+k+")");var l=this.rotatableNode,m=l&&l.attr("transform");if(m&&"null"!==m){l.attr("transform",m+" rotate("+-f+","+e.width/2+","+e.height/2+")");var n=g.getBBox({target:this.paper.viewport});d.set("position",{x:n.x,y:n.y},c),this.rotate()}this.update()},translate:function(a,b,c){var d=this.model.get("position")||{x:0,y:0};this.vel.attr("transform","translate("+d.x+","+d.y+")")},rotate:function(){var a=this.rotatableNode;if(a){var b=this.model.get("angle")||0,c=this.model.get("size")||{width:1,height:1},d=c.width/2,e=c.height/2;0!==b?a.attr("transform","rotate("+b+","+d+","+e+")"):a.removeAttr("transform")}},getBBox:function(a){if(a&&a.useModelGeometry){var b=this.model.getBBox().bbox(this.model.get("angle"));return this.paper.localToPaperRect(b)}return joint.dia.CellView.prototype.getBBox.apply(this,arguments)},prepareEmbedding:function(a){a=a||{};var b=a.model||this.model,c=a.paper||this.paper,d=c.model;b.startBatch("to-front",a),b.toFront({deep:!0,ui:!0});var e=d.get("cells").max("z").get("z"),f=d.getConnectedLinks(b,{deep:!0});joint.util.invoke(f,"set","z",e+1,{ui:!0}),b.stopBatch("to-front");var g=b.get("parent");g&&d.getCell(g).unembed(b,{ui:!0})},processEmbedding:function(a){a=a||{};var b=a.model||this.model,c=a.paper||this.paper,d=c.options,e=c.model.findModelsUnderElement(b,{searchBy:d.findParentBy});d.frontParentOnly&&(e=e.slice(-1));for(var f=null,g=this._candidateEmbedView,h=e.length-1;h>=0;h--){var i=e[h];if(g&&g.model.id==i.id){f=g;break}var j=i.findView(c);if(d.validateEmbedding.call(c,this,j)){f=j;break}}f&&f!=g&&(this.clearEmbedding(),this._candidateEmbedView=f.highlight(null,{embedding:!0})),!f&&g&&this.clearEmbedding()},clearEmbedding:function(){var a=this._candidateEmbedView;a&&(a.unhighlight(null,{embedding:!0}),this._candidateEmbedView=null)},finalizeEmbedding:function(a){a=a||{};var b=this._candidateEmbedView,c=a.model||this.model,d=a.paper||this.paper;b&&(b.model.embed(c,{ui:!0}),b.unhighlight(null,{embedding:!0}),delete this._candidateEmbedView),joint.util.invoke(d.model.getConnectedLinks(c,{deep:!0}),"reparent",{ui:!0})},pointerdown:function(a,b,c){var d=this.paper;if(a.target.getAttribute("magnet")&&this.can("addLinkFromMagnet")&&d.options.validateMagnet.call(d,this,a.target)){this.model.startBatch("add-link");var e=d.getDefaultLink(this,a.target);e.set({source:{id:this.model.id,selector:this.getSelector(a.target),port:a.target.getAttribute("port")},target:{x:b,y:c}}),d.model.addCell(e);var f=this._linkView=d.findViewByModel(e);f.pointerdown(a,b,c),f.startArrowheadMove("target",{whenNotAllowed:"remove"})}else this._dx=b,this._dy=c,this.restrictedArea=d.getRestrictedArea(this),joint.dia.CellView.prototype.pointerdown.apply(this,arguments),this.notify("element:pointerdown",a,b,c)},pointermove:function(a,b,c){if(this._linkView)this._linkView.pointermove(a,b,c);else{var d=this.paper.options.gridSize;if(this.can("elementMove")){var e=this.model.get("position"),f=g.snapToGrid(e.x,d)-e.x+g.snapToGrid(b-this._dx,d),h=g.snapToGrid(e.y,d)-e.y+g.snapToGrid(c-this._dy,d);this.model.translate(f,h,{restrictedArea:this.restrictedArea,ui:!0}),this.paper.options.embeddingMode&&(this._inProcessOfEmbedding||(this.prepareEmbedding(),this._inProcessOfEmbedding=!0),this.processEmbedding())}this._dx=g.snapToGrid(b,d),this._dy=g.snapToGrid(c,d),joint.dia.CellView.prototype.pointermove.apply(this,arguments),this.notify("element:pointermove",a,b,c)}},pointerup:function(a,b,c){this._linkView?(this._linkView.pointerup(a,b,c),this._linkView=null,this.model.stopBatch("add-link")):(this._inProcessOfEmbedding&&(this.finalizeEmbedding(),this._inProcessOfEmbedding=!1),this.notify("element:pointerup",a,b,c),joint.dia.CellView.prototype.pointerup.apply(this,arguments))},mouseenter:function(a){joint.dia.CellView.prototype.mouseenter.apply(this,arguments),this.notify("element:mouseenter",a)},mouseleave:function(a){joint.dia.CellView.prototype.mouseleave.apply(this,arguments),this.notify("element:mouseleave",a)}}),joint.dia.Link=joint.dia.Cell.extend({markup:['','','','','','','',''].join(""),labelMarkup:['',"","",""].join(""),toolMarkup:['','','','',"Remove link.","",'','','',"Link options.","",""].join(""),vertexMarkup:['','','','',"Remove vertex.","",""].join(""),arrowheadMarkup:['','',""].join(""),defaults:{type:"link",source:{},target:{}},isLink:function(){return!0},disconnect:function(){return this.set({source:g.point(0,0),target:g.point(0,0)})},label:function(a,b,c){return a=a||0,arguments.length<=1?this.prop(["labels",a]):this.prop(["labels",a],b,c)},translate:function(a,b,c){return c=c||{},c.translateBy=c.translateBy||this.id,c.tx=a,c.ty=b,this.applyToPoints(function(c){return{x:(c.x||0)+a,y:(c.y||0)+b}},c)},scale:function(a,b,c,d){return this.applyToPoints(function(d){return g.point(d).scale(a,b,c).toJSON()},d)},applyToPoints:function(a,b){if(!joint.util.isFunction(a))throw new TypeError("dia.Link: applyToPoints expects its first parameter to be a function.");var c={},d=this.get("source");d.id||(c.source=a(d));var e=this.get("target");e.id||(c.target=a(e));var f=this.get("vertices");return f&&f.length>0&&(c.vertices=f.map(a)),this.set(c,b)},reparent:function(a){var b;if(this.graph){var c=this.graph.getCell(this.get("source").id),d=this.graph.getCell(this.get("target").id),e=this.graph.getCell(this.get("parent"));c&&d&&(b=this.graph.getCommonAncestor(c,d)),!e||b&&b.id===e.id||e.unembed(this,a),b&&b.embed(this,a)}return b},hasLoop:function(a){a=a||{};var b=this.get("source").id,c=this.get("target").id;if(!b||!c)return!1;var d=b===c;if(!d&&a.deep&&this.graph){var e=this.graph.getCell(b),f=this.graph.getCell(c);d=e.isEmbeddedIn(f)||f.isEmbeddedIn(e)}return d},getSourceElement:function(){var a=this.get("source");return a&&a.id&&this.graph&&this.graph.getCell(a.id)||null},getTargetElement:function(){var a=this.get("target"); -return a&&a.id&&this.graph&&this.graph.getCell(a.id)||null},getRelationshipAncestor:function(){var a;if(this.graph){var b=[this,this.getSourceElement(),this.getTargetElement()].filter(function(a){return!!a});a=this.graph.getCommonAncestor.apply(this.graph,b)}return a||null},isRelationshipEmbeddedIn:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a.id,c=this.getRelationshipAncestor();return!!c&&(c.id===b||c.isEmbeddedIn(b))}},{endsEqual:function(a,b){var c=a.port===b.port||!a.port&&!b.port;return a.id===b.id&&c}}),joint.dia.LinkView=joint.dia.CellView.extend({className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("link"),a.join(" ")},options:{shortLinkLength:100,doubleLinkTools:!1,longLinkLength:160,linkToolsOffset:40,doubleLinkToolsOffset:60,sampleInterval:50},_z:null,initialize:function(a){joint.dia.CellView.prototype.initialize.apply(this,arguments),"function"!=typeof this.constructor.prototype.watchSource&&(this.constructor.prototype.watchSource=this.createWatcher("source"),this.constructor.prototype.watchTarget=this.createWatcher("target")),this._labelCache={},this._markerCache={},this.startListening()},startListening:function(){var a=this.model;this.listenTo(a,"change:markup",this.render),this.listenTo(a,"change:smooth change:manhattan change:router change:connector",this.update),this.listenTo(a,"change:toolMarkup",this.onToolsChange),this.listenTo(a,"change:labels change:labelMarkup",this.onLabelsChange),this.listenTo(a,"change:vertices change:vertexMarkup",this.onVerticesChange),this.listenTo(a,"change:source",this.onSourceChange),this.listenTo(a,"change:target",this.onTargetChange)},onSourceChange:function(a,b,c){this.watchSource(a,b),c.translateBy&&this.model.get("target").id||(c.updateConnectionOnly=!0,this.update(this.model,null,c))},onTargetChange:function(a,b,c){this.watchTarget(a,b),c.translateBy||(c.updateConnectionOnly=!0,this.update(this.model,null,c))},onVerticesChange:function(a,b,c){this.renderVertexMarkers(),c.translateBy&&c.translateBy!==this.model.id||(c.updateConnectionOnly=!0,this.update(a,null,c))},onToolsChange:function(){this.renderTools().updateToolsPosition()},onLabelsChange:function(a,b,c){var d=!0,e=this.model.previous("labels");if(e&&"propertyPathArray"in c&&"propertyValue"in c){var f=c.propertyPathArray||[],g=f.length;if(g>1){var h=!!e[f[1]];h&&(2===g?d="markup"in Object(c.propertyValue):"markup"!==f[2]&&(d=!1))}}d?this.renderLabels():this.updateLabels(),this.updateLabelPositions()},render:function(){this.$el.empty();var a=this.model,b=a.get("markup")||a.markup,c=V(b);if(Array.isArray(c)||(c=[c]),this._V={},c.forEach(function(a){var b=a.attr("class");b&&(b=joint.util.removeClassNamePrefix(b),this._V[$.camelCase(b)]=a)},this),!this._V.connection)throw new Error("link: no connection path in the markup");return this.renderTools(),this.renderVertexMarkers(),this.renderArrowheadMarkers(),this.vel.append(c),this.renderLabels(),this.watchSource(a,a.get("source")).watchTarget(a,a.get("target")).update(),this},renderLabels:function(){var a=this._V.labels;if(!a)return this;a.empty();var b=this.model,c=b.get("labels")||[],d=this._labelCache={},e=c.length;if(0===e)return this;for(var f=joint.util.template(b.get("labelMarkup")||b.labelMarkup),g=V(f()),h=0;hd?d:l,l=l<0?d+l:l,l=l>1?l:d*l):l=d/2,h=c.getPointAtLength(l),joint.util.isObject(m))h=g.point(h).offset(m);else if(Number.isFinite(m)){b||(b=this._samples||this._V.connection.sample(this.options.sampleInterval));for(var n,o,p,q=1/0,r=0,s=b.length;r=this.options.longLinkLength){var e=this.options.doubleLinkToolsOffset||b;d=this.getPointAtLength(c-e),this._tool2Cache.attr("transform","translate("+d.x+", "+d.y+") "+a),this._tool2Cache.attr("visibility","visible")}else this.options.doubleLinkTools&&this._tool2Cache.attr("visibility","hidden")}return this},updateArrowheadMarkers:function(){if(!this._V.markerArrowheads)return this;if("none"===$.css(this._V.markerArrowheads.node,"display"))return this;var a=this.getConnectionLength()g);)c=d.slice();return h===-1&&(h=0,c.splice(h,0,a)),this.model.set("vertices",c,{ui:!0}),h},sendToken:function(a,b,c){function d(a,b){return function(){a.remove(),"function"==typeof b&&b()}}var e,f;joint.util.isObject(b)?(e=b.duration,f="reverse"===b.direction):(e=b,f=!1),e=e||1e3;var g={dur:e+"ms",repeatCount:1,calcMode:"linear",fill:"freeze"};f&&(g.keyPoints="1;0",g.keyTimes="0;1");var h=V(a),i=this._V.connection;h.appendTo(this.paper.viewport).animateAlongPath(g,i),setTimeout(d(h,c),e)},findRoute:function(a){var b=joint.routers,c=this.model.get("router"),d=this.paper.options.defaultRouter;if(!c)if(this.model.get("manhattan"))c={name:"orthogonal"};else{if(!d)return a;c=d}var e=c.args||{},f=joint.util.isFunction(c)?c:b[c.name];if(!joint.util.isFunction(f))throw new Error('unknown router: "'+c.name+'"');var g=f.call(this,a||[],e,this);return g},getPathData:function(a){var b=joint.connectors,c=this.model.get("connector"),d=this.paper.options.defaultConnector;c||(c=this.model.get("smooth")?{name:"smooth"}:d||{});var e=joint.util.isFunction(c)?c:b[c.name],f=c.args||{};if(!joint.util.isFunction(e))throw new Error('unknown connector: "'+c.name+'"');var g=e.call(this,this._markerCache.sourcePoint,this._markerCache.targetPoint,a||this.model.get("vertices")||{},f,this);return g},getConnectionPoint:function(a,b,c){var d;if(joint.util.isEmpty(b)&&(b={x:0,y:0}),joint.util.isEmpty(c)&&(c={x:0,y:0}),b.id){var e,f=g.Rect("source"===a?this.sourceBBox:this.targetBBox);if(c.id){var h=g.Rect("source"===a?this.targetBBox:this.sourceBBox);e=h.intersectionWithLineFromCenterToPoint(f.center()),e=e||h.center()}else e=g.Point(c);var i=this.paper.options;if(i.perpendicularLinks||this.options.perpendicular){var j,k=f.origin(),l=f.corner();if(k.y<=e.y&&e.y<=l.y)switch(j=f.sideNearestToPoint(e)){case"left":d=g.Point(k.x,e.y);break;case"right":d=g.Point(l.x,e.y);break;default:d=f.center()}else if(k.x<=e.x&&e.x<=l.x)switch(j=f.sideNearestToPoint(e)){case"top":d=g.Point(e.x,k.y);break;case"bottom":d=g.Point(e.x,l.y);break;default:d=f.center()}else d=f.intersectionWithLineFromCenterToPoint(e),d=d||f.center()}else if(i.linkConnectionPoint){var m="target"===a?this.targetView:this.sourceView,n="target"===a?this.targetMagnet:this.sourceMagnet;d=i.linkConnectionPoint(this,m,n,e,a)}else d=f.intersectionWithLineFromCenterToPoint(e),d=d||f.center()}else d=g.Point(b);return d},getConnectionLength:function(){return this._V.connection.node.getTotalLength()},getPointAtLength:function(a){return this._V.connection.node.getPointAtLength(a)},_beforeArrowheadMove:function(){this._z=this.model.get("z"),this.model.toFront(),this.el.style.pointerEvents="none",this.paper.options.markAvailable&&this._markAvailableMagnets()},_afterArrowheadMove:function(){null!==this._z&&(this.model.set("z",this._z,{ui:!0}),this._z=null),this.el.style.pointerEvents="visiblePainted",this.paper.options.markAvailable&&this._unmarkAvailableMagnets()},_createValidateConnectionArgs:function(a){function b(a,b){return c[f]=a,c[f+1]=a.el===b?void 0:b,c}var c=[];c[4]=a,c[5]=this;var d,e=0,f=0;"source"===a?(e=2,d="target"):(f=2,d="source");var g=this.model.get(d);return g.id&&(c[e]=this.paper.findViewByModel(g.id),c[e+1]=g.selector&&c[e].el.querySelector(g.selector)),b},_markAvailableMagnets:function(){function a(a,b){var c=a.paper,d=c.options.validateConnection;return d.apply(c,this._validateConnectionArgs(a,b))}var b=this.paper,c=b.model.getElements();this._marked={};for(var d=0,e=c.length;d0){for(var i=0,j=h.length;i").addClass(joint.util.addClassNamePrefix("paper-background")),this.options.background&&this.drawBackground(this.options.background),this.$grid=$("
").addClass(joint.util.addClassNamePrefix("paper-grid")),this.options.drawGrid&&this.drawGrid(),this.$el.append(this.$background,this.$grid,this.svg),this},update:function(){return this.options.drawGrid&&this.drawGrid(),this._background&&this.updateBackgroundImage(this._background),this},_viewportMatrix:null,_viewportTransformString:null,matrix:function(a){var b=this.viewport;if(void 0===a){var c=b.getAttribute("transform");return(this._viewportTransformString||null)===c?a=this._viewportMatrix:(a=b.getCTM(),this._viewportMatrix=a,this._viewportTransformString=c),V.createSVGMatrix(a)}return a=V.createSVGMatrix(a),V(b).transform(a,{absolute:!0}),this._viewportMatrix=a,this._viewportTransformString=b.getAttribute("transform"),this},clientMatrix:function(){return V.createSVGMatrix(this.viewport.getScreenCTM())},_onSort:function(){this.model.hasActiveBatch("add")||this.sortViews()},_onBatchStop:function(a){var b=a&&a.batchName;"add"!==b||this.model.hasActiveBatch("add")||this.sortViews()},onRemove:function(){this.removeViews(),this.unbindDocumentEvents()},setDimensions:function(a,b){a=this.options.width=a||this.options.width,b=this.options.height=b||this.options.height,this.$el.css({width:Math.round(a),height:Math.round(b)}),this.trigger("resize",a,b)},setOrigin:function(a,b){return this.translate(a||0,b||0,{absolute:!0})},fitToContent:function(a,b,c,d){joint.util.isObject(a)?(d=a,a=d.gridWidth||1,b=d.gridHeight||1,c=d.padding||0):(d=d||{},a=a||1,b=b||1,c=c||0),c=joint.util.normalizeSides(c);var e=V(this.viewport).getBBox(),f=this.scale(),g=this.translate();e.x*=f.sx,e.y*=f.sy,e.width*=f.sx,e.height*=f.sy;var h=Math.max(Math.ceil((e.width+e.x)/a),1)*a,i=Math.max(Math.ceil((e.height+e.y)/b),1)*b,j=0,k=0;("negative"==d.allowNewOrigin&&e.x<0||"positive"==d.allowNewOrigin&&e.x>=0||"any"==d.allowNewOrigin)&&(j=Math.ceil(-e.x/a)*a,j+=c.left,h+=j),("negative"==d.allowNewOrigin&&e.y<0||"positive"==d.allowNewOrigin&&e.y>=0||"any"==d.allowNewOrigin)&&(k=Math.ceil(-e.y/b)*b,k+=c.top,i+=k),h+=c.right,i+=c.bottom,h=Math.max(h,d.minWidth||0),i=Math.max(i,d.minHeight||0),h=Math.min(h,d.maxWidth||Number.MAX_VALUE),i=Math.min(i,d.maxHeight||Number.MAX_VALUE);var l=h!=this.options.width||i!=this.options.height,m=j!=g.tx||k!=g.ty;m&&this.translate(j,k),l&&this.setDimensions(h,i)},scaleContentToFit:function(a){var b=this.getContentBBox();if(b.width&&b.height){a=a||{},joint.util.defaults(a,{padding:0,preserveAspectRatio:!0,scaleGrid:null,minScale:0,maxScale:Number.MAX_VALUE});var c,d=a.padding,e=a.minScaleX||a.minScale,f=a.maxScaleX||a.maxScale,h=a.minScaleY||a.minScale,i=a.maxScaleY||a.maxScale;if(a.fittingBBox)c=a.fittingBBox;else{var j=this.translate();c={x:j.tx,y:j.ty,width:this.options.width,height:this.options.height}}c=g.rect(c).moveAndExpand({x:d,y:d,width:-2*d,height:-2*d});var k=this.scale(),l=c.width/b.width*k.sx,m=c.height/b.height*k.sy;if(a.preserveAspectRatio&&(l=m=Math.min(l,m)),a.scaleGrid){var n=a.scaleGrid;l=n*Math.floor(l/n),m=n*Math.floor(m/n)}l=Math.min(f,Math.max(e,l)),m=Math.min(i,Math.max(h,m)),this.scale(l,m);var o=this.getContentBBox(),p=c.x-o.x,q=c.y-o.y;this.translate(p,q)}},getContentBBox:function(){var a=this.viewport.getBoundingClientRect(),b=this.clientMatrix(),c=this.translate();return g.rect({x:a.left-b.e+c.tx,y:a.top-b.f+c.ty,width:a.width,height:a.height})},getArea:function(){return this.paperToLocalRect({x:0,y:0,width:this.options.width,height:this.options.height})},getRestrictedArea:function(){var a;return a=joint.util.isFunction(this.options.restrictTranslate)?this.options.restrictTranslate.apply(this,arguments):this.options.restrictTranslate===!0?this.getArea():this.options.restrictTranslate||null},createViewForModel:function(a){var b,c,d=this.options.cellViewNamespace,e=a.get("type")+"View",f=joint.util.getByPath(d,e,".");a.isLink()?(b=this.options.linkView,c=joint.dia.LinkView):(b=this.options.elementView,c=joint.dia.ElementView);var g=b.prototype instanceof Backbone.View?f||b:b.call(this,a)||f||c;return new g({model:a,interactive:this.options.interactive})},onCellAdded:function(a,b,c){if(this.options.async&&c.async!==!1&&joint.util.isNumber(c.position)){if(this._asyncCells=this._asyncCells||[],this._asyncCells.push(a),0==c.position){if(this._frameId)throw new Error("another asynchronous rendering in progress");this.asyncRenderViews(this._asyncCells,c),delete this._asyncCells}}else this.renderView(a)},removeView:function(a){var b=this._views[a.id];return b&&(b.remove(),delete this._views[a.id]),b},renderView:function(a){var b=this._views[a.id]=this.createViewForModel(a);return V(this.viewport).append(b.el),b.paper=this,b.render(),$(b.el).find("image").on("dragstart",function(){return!1}),b},beforeRenderViews:function(a){return a.sort(function(a){return a.isLink()?1:-1}),a},afterRenderViews:function(){this.sortViews()},resetViews:function(a,b){this.removeViews();var c=a.models.slice();if(c=this.beforeRenderViews(c,b)||c,this.cancelRenderViews(),this.options.async)this.asyncRenderViews(c,b);else{for(var d=0,e=c.length;d(e.get("z")||0)?1:-1})},scale:function(a,b,c,d){if(void 0===a)return V.matrixToScale(this.matrix());void 0===b&&(b=a),void 0===c&&(c=0,d=0);var e=this.translate();if(c||d||e.tx||e.ty){var f=e.tx-c*(a-1),g=e.ty-d*(b-1);this.translate(f,g)}var h=this.matrix();return h.a=a||0,h.d=b||0,this.matrix(h),this.trigger("scale",a,b,c,d),this},rotate:function(a,b,c){if(void 0===a)return V.matrixToRotate(this.matrix());if(void 0===b){var d=this.viewport.getBBox();b=d.width/2,c=d.height/2}var e=this.matrix().translate(b,c).rotate(a).translate(-b,-c);return this.matrix(e),this},translate:function(a,b){if(void 0===a)return V.matrixToTranslate(this.matrix());var c=this.matrix();c.e=a||0,c.f=b||0,this.matrix(c);var d=this.translate(),e=this.options.origin;return e.x=d.tx,e.y=d.ty,this.trigger("translate",d.tx,d.ty),this.options.drawGrid&&this.drawGrid(),this},findView:function(a){for(var b=joint.util.isString(a)?this.viewport.querySelector(a):a instanceof $?a[0]:a;b&&b!==this.el&&b!==document;){var c=b.getAttribute("model-id");if(c)return this._views[c];b=b.parentNode}},findViewByModel:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a&&a.id;return this._views[b]},findViewsFromPoint:function(a){a=g.point(a);var b=this.model.getElements().map(this.findViewByModel,this);return b.filter(function(b){return b&&b.vel.getBBox({target:this.viewport}).containsPoint(a)},this)},findViewsInArea:function(a,b){b=joint.util.defaults(b||{},{strict:!1}),a=g.rect(a);var c=this.model.getElements().map(this.findViewByModel,this),d=b.strict?"containsRect":"intersect";return c.filter(function(b){return b&&a[d](b.vel.getBBox({target:this.viewport}))},this)},getModelById:function(a){return this.model.getCell(a)},snapToGrid:function(a,b){return this.clientToLocalPoint(a,b).snapToGrid(this.options.gridSize)},localToPaperPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix());return g.Point(d)},localToPaperRect:function(a,b,c,d){var e=g.Rect(a,b),f=V.transformRect(e,this.matrix()); -return g.Rect(f)},paperToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix().inverse());return g.Point(d)},paperToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.matrix().inverse());return g.Rect(f)},localToClientPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix());return g.Point(d)},localToClientRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix());return g.Rect(f)},clientToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix().inverse());return g.Point(d)},clientToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix().inverse());return g.Rect(f)},localToPagePoint:function(a,b){return this.localToPaperPoint(a,b).offset(this.pageOffset())},localToPageRect:function(a,b,c,d){return this.localToPaperRect(a,b,c,d).moveAndExpand(this.pageOffset())},pageToLocalPoint:function(a,b){var c=g.Point(a,b),d=c.difference(this.pageOffset());return this.paperToLocalPoint(d)},pageToLocalRect:function(a,b,c,d){var e=this.pageOffset(),f=g.Rect(a,b,c,d);return f.x-=e.x,f.y-=e.y,this.paperToLocalRect(f)},clientOffset:function(){var a=this.svg.getBoundingClientRect();return g.Point(a.left,a.top)},pageOffset:function(){return this.clientOffset().offset(window.scrollX,window.scrollY)},linkAllowed:function(a){var b;if(a instanceof joint.dia.Link)b=a;else{if(!(a instanceof joint.dia.LinkView))throw new Error("Must provide link model or view.");b=a.model}if(!this.options.multiLinks){var c=b.get("source"),d=b.get("target");if(c.id&&d.id){var e=b.getSourceElement();if(e){var f=this.model.getConnectedLinks(e,{outbound:!0,inbound:!1}),g=f.filter(function(a){var b=a.get("source"),e=a.get("target");return b&&b.id===c.id&&(!b.port||b.port===c.port)&&e&&e.id===d.id&&(!e.port||e.port===d.port)}).length;if(g>1)return!1}}}return!!(this.options.linkPinning||joint.util.has(b.get("source"),"id")&&joint.util.has(b.get("target"),"id"))},getDefaultLink:function(a,b){return joint.util.isFunction(this.options.defaultLink)?this.options.defaultLink.call(this,a,b):this.options.defaultLink.clone()},resolveHighlighter:function(a){a=a||{};var b=a.highlighter,c=this.options;if(void 0===b){var d=["embedding","connecting","magnetAvailability","elementAvailability"].find(function(b){return!!a[b]});b=d&&c.highlighting[d]||c.highlighting.default}if(!b)return!1;joint.util.isString(b)&&(b={name:b});var e=b.name,f=c.highlighterNamespace[e];if(!f)throw new Error('Unknown highlighter ("'+e+'")');if("function"!=typeof f.highlight)throw new Error('Highlighter ("'+e+'") is missing required highlight() method');if("function"!=typeof f.unhighlight)throw new Error('Highlighter ("'+e+'") is missing required unhighlight() method');return{highlighter:f,options:b.options||{},name:e}},onCellHighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){b.id||(b.id=V.uniqueId());var d=c.name+b.id+JSON.stringify(c.options);if(!this._highlights[d]){var e=c.highlighter;e.highlight(a,b,joint.util.assign({},c.options)),this._highlights[d]={cellView:a,magnetEl:b,opt:c.options,highlighter:e}}}},onCellUnhighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){var d=c.name+b.id+JSON.stringify(c.options),e=this._highlights[d];e&&(e.highlighter.unhighlight(e.cellView,e.magnetEl,e.opt),this._highlights[d]=null)}},mousedblclick:function(a){a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerdblclick(a,c.x,c.y):this.trigger("blank:pointerdblclick",a,c.x,c.y)}},mouseclick:function(a){if(this._mousemoved<=this.options.clickThreshold){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(this.guard(a,b))return;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerclick(a,c.x,c.y):this.trigger("blank:pointerclick",a,c.x,c.y)}},guard:function(a,b){return!(!this.options.guard||!this.options.guard(a,b))||(a.data&&void 0!==a.data.guarded?a.data.guarded:!(b&&b.model&&b.model instanceof joint.dia.Cell)&&(this.svg!==a.target&&this.el!==a.target&&!$.contains(this.svg,a.target)))},contextmenu:function(a){a=joint.util.normalizeEvent(a),this.options.preventContextMenu&&a.preventDefault();var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.contextmenu(a,c.x,c.y):this.trigger("blank:contextmenu",a,c.x,c.y)}},pointerdown:function(a){this.bindDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){this._mousemoved=0;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?(a.preventDefault(),this.sourceView=b,b.pointerdown(a,c.x,c.y)):(this.options.preventDefaultBlankAction&&a.preventDefault(),this.trigger("blank:pointerdown",a,c.x,c.y))}},pointermove:function(a){var b=this.sourceView;if(b){a.preventDefault();var c=++this._mousemoved;if(c>this.options.moveThreshold){a=joint.util.normalizeEvent(a);var d=this.snapToGrid({x:a.clientX,y:a.clientY});b.pointermove(a,d.x,d.y)}}},pointerup:function(a){this.unbindDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.snapToGrid({x:a.clientX,y:a.clientY});this.sourceView?(this.sourceView.pointerup(a,b.x,b.y),this.sourceView=null):this.trigger("blank:pointerup",a,b.x,b.y)},mousewheel:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=a.originalEvent,d=this.snapToGrid({x:c.clientX,y:c.clientY}),e=Math.max(-1,Math.min(1,c.wheelDelta||-c.detail));b?b.mousewheel(a,d.x,d.y,e):this.trigger("blank:mousewheel",a,d.x,d.y,e)}},cellMouseover:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(b){if(this.guard(a,b))return;b.mouseover(a)}},cellMouseout:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(b){if(this.guard(a,b))return;b.mouseout(a)}},cellMouseenter:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);b&&!this.guard(a,b)&&b.mouseenter(a)},cellMouseleave:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);b&&!this.guard(a,b)&&b.mouseleave(a)},cellEvent:function(a){a=joint.util.normalizeEvent(a);var b=a.currentTarget,c=b.getAttribute("event");if(c){var d=this.findView(b);if(d&&!this.guard(a,d)){var e=this.snapToGrid({x:a.clientX,y:a.clientY});d.event(a,c,e.x,e.y)}}},setGridSize:function(a){return this.options.gridSize=a,this.options.drawGrid&&this.drawGrid(),this},clearGrid:function(){return this.$grid&&this.$grid.css("backgroundImage","none"),this},_getGriRefs:function(){return this._gridCache||(this._gridCache={root:V("svg",{width:"100%",height:"100%"},V("defs")),patterns:{},add:function(a,b){V(this.root.node.childNodes[0]).append(b),this.patterns[a]=b,this.root.append(V("rect",{width:"100%",height:"100%",fill:"url(#"+a+")"}))},get:function(a){return this.patterns[a]},exist:function(a){return void 0!==this.patterns[a]}}),this._gridCache},setGrid:function(a){this.clearGrid(),this._gridCache=null,this._gridSettings=[];var b=Array.isArray(a)?a:[a||{}];return b.forEach(function(a){this._gridSettings.push.apply(this._gridSettings,this._resolveDrawGridOption(a))},this),this},_resolveDrawGridOption:function(a){var b=this.constructor.gridPatterns;if(joint.util.isString(a)&&Array.isArray(b[a]))return b[a].map(function(a){return joint.util.assign({},a)});var c=a||{args:[{}]},d=Array.isArray(c),e=c.name;if(d||e||c.markup||(e="dot"),e&&Array.isArray(b[e])){var f=b[e].map(function(a){return joint.util.assign({},a)}),g=Array.isArray(c.args)?c.args:[c.args||{}];joint.util.defaults(g[0],joint.util.omit(a,"args"));for(var h=0;h'),f=joint.util.toArray(d).map(function(a){return e({offset:a.offset,color:a.color,opacity:Number.isFinite(a.opacity)?a.opacity:1})}),g=["<"+c+">",f.join(""),""].join(""),h=joint.util.assign({id:b},a.attrs);V(g,h).appendTo(this.defs)}return b},defineMarker:function(a){if(!joint.util.isObject(a))throw new TypeError("dia.Paper: defineMarker() requires 1. argument to be an object.");var b=a.id;if(b||(b=this.svg.id+joint.util.hashCode(JSON.stringify(a))),!this.isDefined(b)){var c=joint.util.omit(a,"type","userSpaceOnUse"),d=V("marker",{id:b,orient:"auto",overflow:"visible",markerUnits:a.markerUnits||"userSpaceOnUse"},[V(a.type||"path",c)]);d.appendTo(this.defs)}return b}},{backgroundPatterns:{flipXy:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,-1,b.width,b.height),e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,1,b.width,0),e.drawImage(a,0,0,c,d),e.setTransform(1,0,0,-1,0,b.height),e.drawImage(a,0,0,c,d),b},flipX:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(2*c,0),e.scale(-1,1),e.drawImage(a,0,0,c,d),b},flipY:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(0,2*d),e.scale(1,-1),e.drawImage(a,0,0,c,d),b},watermark:function(a,b){b=b||{};var c=a.width,d=a.height,e=document.createElement("canvas");e.width=3*c,e.height=3*d;for(var f=e.getContext("2d"),h=joint.util.isNumber(b.watermarkAngle)?-b.watermarkAngle:-20,i=g.toRad(h),j=e.width/4,k=e.height/4,l=0;l<4;l++)for(var m=0;m<4;m++)(l+m)%2>0&&(f.setTransform(1,0,0,1,(2*l-1)*j,(2*m-1)*k),f.rotate(i),f.drawImage(a,-c/2,-d/2,c,d));return e}},gridPatterns:{dot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){V(a).attr({width:b.thickness*b.sx,height:b.thickness*b.sy,fill:b.color})}}],fixedDot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){var c=b.sx<=1?b.thickness*b.sx:b.thickness;V(a).attr({width:c,height:c,fill:b.color})}}],mesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}],doubleMesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}},{color:"#000000",thickness:3,scaleFactor:4,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}]}}),function(a,b,c){var d=function(b){var d=c.cloneDeep(b)||{};this.ports=[],this.groups={},this.portLayoutNamespace=a.layout.Port,this.portLabelLayoutNamespace=a.layout.PortLabel,this._init(d)};d.prototype={getPorts:function(){return this.ports},getGroup:function(a){return this.groups[a]||{}},getPortsByGroup:function(a){return this.ports.filter(function(b){return b.group===a})},getGroupPortsMetrics:function(a,b){var d=this.getGroup(a),e=this.getPortsByGroup(a),f=d.position||{},h=f.name,i=this.portLayoutNamespace;i[h]||(h="left");var j=f.args||{},k=e.map(function(a){return a&&a.position&&a.position.args}),l=i[h](k,b,j),m={ports:e,result:[]};return c.toArray(l).reduce(function(a,c,d){var e=a.ports[d];return a.result.push({portId:e.id,portTransformation:c,labelTransformation:this._getPortLabelLayout(e,g.Point(c),b),portAttrs:e.attrs,portSize:e.size,labelSize:e.label.size}),a}.bind(this),m),m.result},_getPortLabelLayout:function(a,b,c){var d=this.portLabelLayoutNamespace,e=a.label.position.name||"left";return d[e]?d[e](b,c,a.label.position.args):null},_init:function(a){if(c.isObject(a.groups))for(var b=Object.keys(a.groups),d=0,e=b.length;d0},hasPort:function(a){return this.getPortIndex(a)!==-1},getPorts:function(){return c.cloneDeep(this.prop("ports/items"))||[]},getPort:function(a){return c.cloneDeep(c.toArray(this.prop("ports/items")).find(function(b){return b.id&&b.id===a}))},getPortsPositions:function(a){var b=this._portSettingsData.getGroupPortsMetrics(a,g.Rect(this.size()));return b.reduce(function(a,b){var c=b.portTransformation;return a[b.portId]={x:c.x,y:c.y,angle:c.angle},a},{})},getPortIndex:function(a){var b=c.isObject(a)?a.id:a;return this._isValidPortId(b)?c.toArray(this.prop("ports/items")).findIndex(function(a){return a.id===b}):-1},addPort:function(a,b){if(!c.isObject(a)||Array.isArray(a))throw new Error("Element: addPort requires an object.");var d=c.assign([],this.prop("ports/items"));return d.push(a),this.prop("ports/items",d,b),this},portProp:function(a,b,d,e){var f=this.getPortIndex(a);if(f===-1)throw new Error("Element: unable to find port with id "+a);var g=Array.prototype.slice.call(arguments,1);return Array.isArray(b)?g[0]=["ports","items",f].concat(b):c.isString(b)?g[0]=["ports/items/",f,"/",b].join(""):(g=["ports/items/"+f],c.isPlainObject(b)&&(g.push(b),g.push(d))),this.prop.apply(this,g)},_validatePorts:function(){var b=this.get("ports")||{},d=[];b=b||{};var e=c.toArray(b.items);return e.forEach(function(a){"object"!=typeof a&&d.push("Element: invalid port ",a),this._isValidPortId(a.id)||(a.id=c.uuid())},this),a.util.uniq(e,"id").length!==e.length&&d.push("Element: found id duplicities in ports."),d},_isValidPortId:function(a){return null!==a&&void 0!==a&&!c.isObject(a)},addPorts:function(a,b){return a.length&&this.prop("ports/items",c.assign([],this.prop("ports/items")).concat(a),b),this},removePort:function(a,b){var d=b||{},e=c.assign([],this.prop("ports/items")),f=this.getPortIndex(a);return f!==-1&&(e.splice(f,1),d.rewrite=!0,this.prop("ports/items",e,d)),this},_createPortData:function(){var a=this._validatePorts();if(a.length>0)throw this.set("ports",this.previous("ports")),new Error(a.join(" "));var b;this._portSettingsData&&(b=this._portSettingsData.getPorts()),this._portSettingsData=new d(this.get("ports"));var c=this._portSettingsData.getPorts();if(b){var e=c.filter(function(a){if(!b.find(function(b){return b.id===a.id}))return a}),f=b.filter(function(a){if(!c.find(function(b){return b.id===a.id}))return a});f.length>0&&this.trigger("ports:remove",this,f),e.length>0&&this.trigger("ports:add",this,e)}}}),c.assign(a.dia.ElementView.prototype,{portContainerMarkup:'',portMarkup:'',portLabelMarkup:'',_portElementsCache:null,_initializePorts:function(){this._portElementsCache={},this.listenTo(this.model,"change:ports",function(){this._refreshPorts()})},_refreshPorts:function(){this._removePorts(),this._portElementsCache={},this._renderPorts()},_renderPorts:function(){for(var a=[],b=this._getContainerElement(),d=0,e=b.node.childNodes.length;d1)throw new Error("ElementView: Invalid port markup - multiple roots.");b.attr({port:a.id,"port-group":a.group});var d=V(this.portContainerMarkup).append(b).append(c);return this._portElementsCache[a.id]={portElement:d,portLabelElement:c},d},_updatePortGroup:function(a){for(var b=g.Rect(this.model.size()),c=this.model._portSettingsData.getGroupPortsMetrics(a,b),d=0,e=c.length;d'}),joint.shapes.basic.TextView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"change:attrs",this.resize)}}),joint.shapes.basic.Generic.define("basic.Text",{attrs:{text:{"font-size":18,fill:"#000000"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Circle",{size:{width:60,height:60},attrs:{circle:{fill:"#ffffff",stroke:"#000000",r:30,cx:30,cy:30},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Ellipse",{size:{width:60,height:40},attrs:{ellipse:{fill:"#ffffff",stroke:"#000000",rx:30,ry:20,cx:30,cy:20},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polygon",{size:{width:60,height:40},attrs:{polygon:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polyline",{size:{width:60,height:40},attrs:{polyline:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Image",{attrs:{text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Path",{size:{width:60,height:60},attrs:{path:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle",ref:"path","ref-x":.5,"ref-dy":10,fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Path.define("basic.Rhombus",{attrs:{path:{d:"M 30 0 L 60 30 30 60 0 30 z"},text:{"ref-y":.5,"ref-dy":null,"y-alignment":"middle"}}}),joint.shapes.basic.PortsModelInterface={initialize:function(){this.updatePortsAttrs(),this.on("change:inPorts change:outPorts",this.updatePortsAttrs,this),this.constructor.__super__.constructor.__super__.initialize.apply(this,arguments)},updatePortsAttrs:function(a){if(this._portSelectors){var b=joint.util.omit(this.get("attrs"),this._portSelectors);this.set("attrs",b,{silent:!0})}this._portSelectors=[];var c={};joint.util.toArray(this.get("inPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".inPorts","in");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),joint.util.toArray("outPorts").forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".outPorts","out");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),this.attr(c,{silent:!0}),this.processPorts(),this.trigger("process:ports")},getPortSelector:function(a){var b=".inPorts",c=this.get("inPorts").indexOf(a);if(c<0&&(b=".outPorts",c=this.get("outPorts").indexOf(a),c<0))throw new Error("getPortSelector(): Port doesn't exist.");return b+">g:nth-child("+(c+1)+")>.port-body"}},joint.shapes.basic.PortsViewInterface={initialize:function(){this.listenTo(this.model,"process:ports",this.update),joint.dia.ElementView.prototype.initialize.apply(this,arguments)},update:function(){this.renderPorts(),joint.dia.ElementView.prototype.update.apply(this,arguments)},renderPorts:function(){var a=this.$(".inPorts").empty(),b=this.$(".outPorts").empty(),c=joint.util.template(this.model.portMarkup),d=this.model.ports||[];d.filter(function(a){return"in"===a.type}).forEach(function(b,d){a.append(V(c({id:d,port:b})).node)}),d.filter(function(a){return"out"===a.type}).forEach(function(a,d){b.append(V(c({id:d,port:a})).node)})}},joint.shapes.basic.Generic.define("basic.TextBlock",{attrs:{rect:{fill:"#ffffff",stroke:"#000000",width:80,height:100},text:{fill:"#000000","font-size":14,"font-family":"Arial, helvetica, sans-serif"},".content":{text:"","ref-x":.5,"ref-y":.5,"y-alignment":"middle","x-alignment":"middle"}},content:""},{markup:['','',joint.env.test("svgforeignobject")?'
':'',""].join(""),initialize:function(){this.listenTo(this,"change:size",this.updateSize),this.listenTo(this,"change:content",this.updateContent),this.updateSize(this,this.get("size")),this.updateContent(this,this.get("content")),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateSize:function(a,b){this.attr({".fobj":joint.util.assign({},b),div:{style:joint.util.assign({},b)}})},updateContent:function(a,b){joint.env.test("svgforeignobject")?this.attr({".content":{html:b}}):this.attr({".content":{text:b}})},setForeignObjectSize:function(){this.updateSize.apply(this,arguments)},setDivContent:function(){this.updateContent.apply(this,arguments)}}),joint.shapes.basic.TextBlockView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.noSVGForeignObjectElement=!joint.env.test("svgforeignobject"),joint.env.test("svgforeignobject")||this.listenTo(this.model,"change:content change:size",function(a){this.updateContent(a)})},update:function(a,b){var c=this.model;if(joint.env.test("svgforeignobject"))joint.dia.ElementView.prototype.update.call(this,c,b);else{var d=joint.util.omit(b||c.get("attrs"),".content");joint.dia.ElementView.prototype.update.call(this,c,d),b&&!joint.util.has(b,".content")||this.updateContent(c,b)}},updateContent:function(a,b){var c=joint.util.merge({},(b||a.get("attrs"))[".content"]);c=joint.util.omit(c,"text");var d=joint.util.breakText(a.get("content"),a.get("size"),c,{svgDocument:this.paper.svg}),e=joint.util.setByPath({},".content",c,"/");e[".content"].text=d,joint.dia.ElementView.prototype.update.call(this,a,e)}}),joint.routers.manhattan=function(a,b,c,d){"use strict";function e(a){this.map={},this.options=a,this.mapGridSize=100}function f(){this.items=[],this.hash={},this.values={},this.OPEN=1,this.CLOSE=2}function g(b){return a.point(0===b.x?0:Math.abs(b.x)/b.x,0===b.y?0:Math.abs(b.y)/b.y)}function h(b,c,d,e){for(var f,h=[],i=g(e.difference(c)),j=c;f=b[j];){var k=g(j.difference(f));k.equals(i)||(h.unshift(j),i=k),j=f}var l=g(a.point(j).difference(d));return l.equals(i)||h.unshift(j),h}function i(a,b,c){var e=c.step,f=a.center(),g=d.isObject(c.directionMap)?Object.keys(c.directionMap):[],h=d.toArray(b);return g.reduce(function(b,d){if(h.includes(d)){var g=c.directionMap[d],i=g.x*a.width/2,j=g.y*a.height/2,k=f.clone().offset(i,j);a.containsPoint(k)&&k.offset(g.x*e,g.y*e),b.push(k.snapToGrid(e))}return b},[])}function j(b,c,d){var e=360/d;return Math.floor(a.normalizeAngle(b.theta(c)+e/2)/e)*e}function k(a,b){var c=Math.abs(a-b);return c>180?360-c:c}function l(a,b){for(var c=1/0,d=0,e=b.length;d0&&n.length>0){for(var r=new f,s={},t={},u=0,v=m.length;u0;){var E=r.pop(),F=a.point(E),G=t[E],H=I,I=s[E]?j(s[E],F,B):null!=g.previousDirAngle?g.previousDirAngle:j(o,F,B);if(D.indexOf(E)>=0&&(z=k(I,j(F,p,B)),F.equals(p)||z<180))return g.previousDirAngle=I,h(s,F,o,p);for(u=0;ug.maxAllowedDirectionChange)){var J=F.clone().offset(y.offsetX,y.offsetY),K=J.toString();if(!r.isClose(K)&&e.isPointAccessible(J)){var L=G+y.cost+g.penalties[z];(!r.isOpen(K)||L90){var h=e;e=f,f=h}var i=d%90<45?e:f,j=g.line(a,i),k=90*Math.ceil(d/90),l=g.point.fromPolar(j.squaredLength(),g.toRad(k+135),i),m=g.line(b,l),n=j.intersection(m);return n?[n.round(),b]:[b]}};return function(c,d,e){return joint.routers.manhattan(c,a.assign({},b,d),e)}}(joint.util),joint.routers.normal=function(a,b,c){return a},joint.routers.oneSide=function(a,b,c){var d,e,f,g=b.side||"bottom",h=b.padding||40,i=c.sourceBBox,j=c.targetBBox,k=i.center(),l=j.center();switch(g){case"bottom":f=1,d="y",e="height";break;case"top":f=-1,d="y",e="height";break;case"left":f=-1,d="x",e="width";break;case"right":f=1,d="x",e="width";break;default:throw new Error("Router: invalid side")}return k[d]+=f*(i[e]/2+h),l[d]+=f*(j[e]/2+h),f*(k[d]-l[d])>0?l[d]=k[d]:k[d]=l[d],[k].concat(a,l)},joint.routers.orthogonal=function(a){function b(a,b){return a.x==b.x?a.y>b.y?"N":"S":a.y==b.y?a.x>b.x?"W":"E":null}function c(a,b){return a["W"==b||"E"==b?"width":"height"]}function d(a,b){return g.rect(a).moveAndExpand({x:-b,y:-b,width:2*b,height:2*b})}function e(a){return g.rect(a.x,a.y,0,0)}function f(a,b){var c=Math.min(a.x,b.x),d=Math.min(a.y,b.y),e=Math.max(a.x+a.width,b.x+b.width),f=Math.max(a.y+a.height,b.y+b.height);return g.rect(c,d,e-c,f-d)}function h(a,b,c){var d=g.point(a.x,b.y);return c.containsPoint(d)&&(d=g.point(b.x,a.y)),d}function i(a,c,d){var e=g.point(a.x,c.y),f=g.point(c.x,a.y),h=b(a,e),i=b(a,f),j=o[d],k=h==d||h!=j&&(i==j||i!=d)?e:f;return{points:[k],direction:b(k,c)}}function j(a,c,d){var e=h(a,c,d);return{points:[e],direction:b(e,c)}}function k(d,e,f,i){var j,k={},l=[g.point(d.x,e.y),g.point(e.x,d.y)],m=l.filter(function(a){return!f.containsPoint(a)}),n=m.filter(function(a){return b(a,d)!=i});if(n.length>0)j=n.filter(function(a){return b(d,a)==i}).pop(),j=j||n[0],k.points=[j],k.direction=b(j,e);else{j=a.difference(l,m)[0];var o=g.point(e).move(j,-c(f,i)/2),p=h(o,d,f);k.points=[p,o],k.direction=b(o,e)}return k}function l(a,d,e,f){var h=j(d,a,f),k=h.points[0];if(e.containsPoint(k)){h=j(a,d,e);var l=h.points[0];if(f.containsPoint(l)){var m=g.point(a).move(l,-c(e,b(a,l))/2),n=g.point(d).move(k,-c(f,b(d,k))/2),o=g.line(m,n).midpoint(),p=j(a,o,e),q=i(o,d,p.direction);h.points=[p.points[0],q.points[0]],h.direction=q.direction}}return h}function m(a,c,e,i,j){var k,l,m,n={},o=d(f(e,i),1),q=o.center().distance(c)>o.center().distance(a),r=q?c:a,s=q?a:c;return j?(k=g.point.fromPolar(o.width+o.height,p[j],r),k=o.pointNearestToPoint(k).move(k,-1)):k=o.pointNearestToPoint(r).move(r,1),l=h(k,s,o),k.round().equals(l.round())?(l=g.point.fromPolar(o.width+o.height,g.toRad(k.theta(r))+Math.PI/2,s),l=o.pointNearestToPoint(l).move(s,1).round(),m=h(k,l,o),n.points=q?[l,m,k]:[k,m,l]):n.points=q?[l,k]:[k,l],n.direction=q?b(k,c):b(l,c),n}function n(c,f,h){var n=f.elementPadding||20,o=[],p=d(h.sourceBBox,n),q=d(h.targetBBox,n);c=a.toArray(c).map(g.point),c.unshift(p.center()),c.push(q.center());for(var r,s=0,t=c.length-1;sv)||"jumpover"!==d.name)}),y=x.map(function(a){return r.findViewByModel(a)}),z=d(a,b,f),A=y.map(function(a){return null==a?[]:a===this?z:d(a.sourcePoint,a.targetPoint,a.route)},this),B=z.reduce(function(a,b){var c=x.reduce(function(a,c,d){if(c!==u){var e=g(b,A[d]);a.push.apply(a,e)}return a},[]).sort(function(a,c){return h(b.start,a)-h(b.start,c)});return c.length>0?a.push.apply(a,i(b,c,o)):a.push(b),a},[]);return j(B,o,p)}}(_,g,joint.util),function(a,b,c,d){function e(a,b,d){var e=a.toJSON();return e.angle=b||0,c.util.defaults({},d,e)}function f(a,c,d){return a.map(function(a,b,c){var d=this.pointAt((b+.5)/c.length);return(a.dx||a.dy)&&d.offset(a.dx||0,a.dy||0),e(d.round(),0,a)},b.line(c,d))}function g(a,c,d,f){var g=c.center(),h=c.width/c.height,i=c.topMiddle(),j=b.Ellipse.fromRect(c);return a.map(function(a,b,c){var k=d+f(b,c.length),l=i.clone().rotate(g,-k).scale(h,1,g),m=a.compensateRotation?-j.tangentTheta(l):0;return(a.dx||a.dy)&&l.offset(a.dx||0,a.dy||0),a.dr&&l.move(g,a.dr),e(l.round(),m,a)})}function h(a,c){var e=c.x;d.isString(e)&&(e=parseFloat(e)/100*a.width);var f=c.y;return d.isString(f)&&(f=parseFloat(f)/100*a.height),b.point(e||0,f||0)}c.layout.Port={absolute:function(a,b,c){return a.map(h.bind(null,b))},fn:function(a,b,c){return c.fn(a,b,c)},line:function(a,b,c){var d=h(b,c.start||b.origin()),e=h(b,c.end||b.corner());return f(a,d,e)},left:function(a,b,c){return f(a,b.origin(),b.bottomLeft())},right:function(a,b,c){return f(a,b.topRight(),b.corner())},top:function(a,b,c){return f(a,b.origin(),b.topRight())},bottom:function(a,b,c){return f(a,b.bottomLeft(),b.corner())},ellipseSpread:function(a,b,c){var d=c.startAngle||0,e=c.step||360/a.length;return g(a,b,d,function(a){return a*e})},ellipse:function(a,b,c){var d=c.startAngle||0,e=c.step||20;return g(a,b,d,function(a,b){return(a+.5-b/2)*e})}}}(_,g,joint,joint.util),function(a,b,c,d){function e(a,b){return d.defaultsDeep({},a,b,{x:0,y:0,angle:0,attrs:{".":{y:"0","text-anchor":"start"}}})}function f(a,b,c,f){f=d.defaults({},f,{offset:15});var h,i,j,k,l=b.center().theta(a),m=g(b),n=f.offset,o=0;lm[2]?(j=".3em",h=n,i=0,k="start"):lo[2]?(k=".3em",i=-m,j=0,l="end"):h-270&&i<-90?(g="start",j=i-180):g="end";var m=Math.round;return e({x:m(k.x),y:m(k.y),angle:c?j:0,attrs:{".":{y:l,"text-anchor":g}}})}c.layout.PortLabel={manual:function(a,b,c){return e(c,a)},left:function(a,b,c){return e(c,{x:-15,attrs:{".":{y:".3em","text-anchor":"end"}}})},right:function(a,b,c){return e(c,{x:15,attrs:{".":{y:".3em","text-anchor":"start"}}})},top:function(a,b,c){return e(c,{y:-15,attrs:{".":{"text-anchor":"middle"}}})},bottom:function(a,b,c){return e(c,{y:15,attrs:{".":{y:".6em","text-anchor":"middle"}}})},outsideOriented:function(a,b,c){return f(a,b,!0,c)},outside:function(a,b,c){return f(a,b,!1,c)},insideOriented:function(a,b,c){return h(a,b,!0,c)},inside:function(a,b,c){return h(a,b,!1,c)},radial:function(a,b,c){return i(a.difference(b.center()),!1,c)},radialOriented:function(a,b,c){return i(a.difference(b.center()),!0,c)}}}(_,g,joint,joint.util),joint.highlighters.addClass={className:joint.util.addClassNamePrefix("highlighted"),highlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).addClass(e)},unhighlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).removeClass(e)}},joint.highlighters.opacity={highlight:function(a,b){V(b).addClass(joint.util.addClassNamePrefix("highlight-opacity"))},unhighlight:function(a,b){V(b).removeClass(joint.util.addClassNamePrefix("highlight-opacity"))}},joint.highlighters.stroke={defaultOptions:{padding:3,rx:0,ry:0,attrs:{"stroke-width":3,stroke:"#FEB663"}},_views:{},getHighlighterId:function(a,b){return a.id+JSON.stringify(b)},removeHighlighter:function(a){this._views[a]&&(this._views[a].remove(),this._views[a]=null)},highlight:function(a,b,c){var d=this.getHighlighterId(b,c);if(!this._views[d]){var e,f=joint.util.defaults(c||{},this.defaultOptions),g=V(b);try{var h=g.convertToPathData()}catch(a){e=g.bbox(!0),h=V.rectToPath(joint.util.assign({},f,e))}var i=V("path").attr({d:h,"pointer-events":"none","vector-effect":"non-scaling-stroke",fill:"none"}).attr(f.attrs),j=g.getTransformToElement(a.el),k=f.padding;if(k){e||(e=g.bbox(!0));var l=e.x+e.width/2,m=e.y+e.height/2;e=V.transformRect(e,j);var n=Math.max(e.width,1),o=Math.max(e.height,1),p=(n+k)/n,q=(o+k)/o,r=V.createSVGMatrix({a:p,b:0,c:0,d:q,e:l-p*l,f:m-q*m});j=j.multiply(r)}i.transform(j);var s=this._views[d]=new joint.mvc.View({svgElement:!0,className:"highlight-stroke",el:i.node}),t=this.removeHighlighter.bind(this,d),u=a.model;s.listenTo(u,"remove",t),s.listenTo(u.graph,"reset",t),a.vel.append(i)}},unhighlight:function(a,b,c){this.removeHighlighter(this.getHighlighterId(b,c))}}; +var g=function(){function a(a,b){return b.unshift(null),new(Function.prototype.bind.apply(a,b))}function b(a){var b,c,d=[];for(c=arguments.length,b=1;b=1)return[new q(b,c,d,e),new q(e,e,e,e)];var f=this.getSkeletonPoints(a),g=f.startControlPoint1,h=f.startControlPoint2,i=f.divider,j=f.dividerControlPoint1,k=f.dividerControlPoint2;return[new q(b,g,h,i),new q(i,j,k,e)]},endpointDistance:function(){return this.start.distance(this.end)},equals:function(a){return!!a&&this.start.x===a.start.x&&this.start.y===a.start.y&&this.controlPoint1.x===a.controlPoint1.x&&this.controlPoint1.y===a.controlPoint1.y&&this.controlPoint2.x===a.controlPoint2.x&&this.controlPoint2.y===a.controlPoint2.y&&this.end.x===a.end.x&&this.end.y===a.end.y},getSkeletonPoints:function(a){var b=this.start,c=this.controlPoint1,d=this.controlPoint2,e=this.end;if(a<=0)return{startControlPoint1:b.clone(),startControlPoint2:b.clone(),divider:b.clone(),dividerControlPoint1:c.clone(),dividerControlPoint2:d.clone()};if(a>=1)return{startControlPoint1:c.clone(),startControlPoint2:d.clone(),divider:e.clone(),dividerControlPoint1:e.clone(),dividerControlPoint2:e.clone()};var f=new s(b,c).pointAt(a),g=new s(c,d).pointAt(a),h=new s(d,e).pointAt(a),i=new s(f,g).pointAt(a),j=new s(g,h).pointAt(a),k=new s(i,j).pointAt(a),l={startControlPoint1:f,startControlPoint2:i,divider:k,dividerControlPoint1:j,dividerControlPoint2:h};return l},getSubdivisions:function(a){a=a||{};var b=void 0===a.precision?this.PRECISION:a.precision,c=[new q(this.start,this.controlPoint1,this.controlPoint2,this.end)];if(0===b)return c;for(var d=this.endpointDistance(),e=p(10,-b),f=0;;){f+=1;for(var g=[],h=c.length,i=0;i1&&r=1)return this.end.clone();var c=this.tAt(a,b);return this.pointAtT(c)},pointAtLength:function(a,b){var c=this.tAtLength(a,b);return this.pointAtT(c)},pointAtT:function(a){return a<=0?this.start.clone():a>=1?this.end.clone():this.getSkeletonPoints(a).divider},PRECISION:3,scale:function(a,b,c){return this.start.scale(a,b,c),this.controlPoint1.scale(a,b,c),this.controlPoint2.scale(a,b,c),this.end.scale(a,b,c),this},tangentAt:function(a,b){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var c=this.tAt(a,b);return this.tangentAtT(c)},tangentAtLength:function(a,b){if(!this.isDifferentiable())return null;var c=this.tAtLength(a,b);return this.tangentAtT(c)},tangentAtT:function(a){if(!this.isDifferentiable())return null;a<0?a=0:a>1&&(a=1);var b=this.getSkeletonPoints(a),c=b.startControlPoint2,d=b.dividerControlPoint1,e=b.divider,f=new s(c,d);return f.translate(e.x-c.x,e.y-c.y),f},tAt:function(a,b){if(a<=0)return 0;if(a>=1)return 1;b=b||{};var c=void 0===b.precision?this.PRECISION:b.precision,d=void 0===b.subdivisions?this.getSubdivisions({precision:c}):b.subdivisions,e={precision:c,subdivisions:d},f=this.length(e),g=f*a;return this.tAtLength(g,e)},tAtLength:function(a,b){var c=!0;a<0&&(c=!1,a=-a),b=b||{};for(var d,e,f,g,h,i=void 0===b.precision?this.PRECISION:b.precision,j=void 0===b.subdivisions?this.getSubdivisions({precision:i}):b.subdivisions,k={precision:i,subdivisions:j},l=0,m=j.length,n=1/m,o=c?0:m-1;c?o=0;c?o++:o--){var q=j[o],r=q.endpointDistance();if(a<=l+r){d=q,e=o*n,f=(o+1)*n,g=c?a-l:r+l-a,h=c?r+l-a:a-l;break}l+=r}if(!d)return c?1:0;for(var s=this.length(k),t=p(10,-i);;){var u;if(u=0!==s?g/s:0,ui.x+g/2,m=ei.x?f-d:f+d,c=g*g/(e-j)-g*g*(f-k)*(b-k)/(h*h*(e-j))+j):(c=f>i.y?e+d:e-d,b=h*h/(f-k)-h*h*(e-j)*(c-j)/(g*g*(f-k))+k),new u(c,b).theta(a)},equals:function(a){return!!a&&a.x===this.x&&a.y===this.y&&a.a===this.a&&a.b===this.b},intersectionWithLine:function(a){var b=[],c=a.start,d=a.end,e=this.a,f=this.b,g=a.vector(),i=c.difference(new u(this)),j=new u(g.x/(e*e),g.y/(f*f)),k=new u(i.x/(e*e),i.y/(f*f)),l=g.dot(j),m=g.dot(k),n=i.dot(k)-1,o=m*m-l*n;if(o<0)return null;if(o>0){var p=h(o),q=(-m-p)/l,r=(-m+p)/l;if((q<0||10){if(f>d||g>d)return null}else if(f=1?c.clone():b.lerp(c,a)},pointAtLength:function(a){var b=this.start,c=this.end;fromStart=!0,a<0&&(fromStart=!1,a=-a);var d=this.length();return a>=d?fromStart?c.clone():b.clone():this.pointAt((fromStart?a:d-a)/d)},pointOffset:function(a){a=new c.Point(a);var b=this.start,d=this.end,e=(d.x-b.x)*(a.y-b.y)-(d.y-b.y)*(a.x-b.x);return e/this.length()},rotate:function(a,b){return this.start.rotate(a,b),this.end.rotate(a,b),this},round:function(a){var b=p(10,a||0);return this.start.x=l(this.start.x*b)/b,this.start.y=l(this.start.y*b)/b,this.end.x=l(this.end.x*b)/b,this.end.y=l(this.end.y*b)/b,this},scale:function(a,b,c){return this.start.scale(a,b,c),this.end.scale(a,b,c),this},setLength:function(a){var b=this.length();if(!b)return this;var c=a/b;return this.scale(c,c,this.start)},squaredLength:function(){var a=this.start.x,b=this.start.y,c=this.end.x,d=this.end.y;return(a-=c)*a+(b-=d)*b},tangentAt:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAt(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},tangentAtLength:function(a){if(!this.isDifferentiable())return null;var b=this.start,c=this.end,d=this.pointAtLength(a),e=new s(b,c);return e.translate(d.x-b.x,d.y-b.y),e},translate:function(a,b){return this.start.translate(a,b),this.end.translate(a,b),this},vector:function(){return new u(this.end.x-this.start.x,this.end.y-this.start.y)},toString:function(){return this.start.toString()+" "+this.end.toString()}},s.prototype.intersection=s.prototype.intersect;var t=c.Path=function(a){if(!(this instanceof t))return new t(a);if("string"==typeof a)return new t.parse(a);this.segments=[];var b,c;if(a){if(Array.isArray(a)&&0!==a.length)if(c=a.length,a[0].isSegment)for(b=0;b=c||a<0)throw new Error("Index out of range.");return b[a]},getSegmentSubdivisions:function(a){var b=this.segments,c=b.length;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=[],f=0;fd||a<0)throw new Error("Index out of range.");var e,f=null,g=null;if(0!==d&&(a>=1?(f=c[a-1],g=f.nextSegment):g=c[0]),Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");for(var h=b.length,i=0;i=d?(e=d-1,f=1):f<0?f=0:f>1&&(f=1),b=b||{};for(var g,h=void 0===b.precision?this.PRECISION:b.precision,i=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:h}):b.segmentSubdivisions,j=0,k=0;k=1)return this.end.clone();b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.pointAtLength(i,g)},pointAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;if(0===a)return this.start.clone();var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isVisible){if(a<=i+m)return k.pointAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f)return e?f.end:f.start;var n=c[d-1];return n.end.clone()},pointAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].pointAtT(0);if(d>=c)return b[c-1].pointAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].pointAtT(e)},prepareSegment:function(a,b,c){a.previousSegment=b,a.nextSegment=c,b&&(b.nextSegment=a),c&&(c.previousSegment=a);var d=a;return a.isSubpathStart&&(a.subpathStartSegment=a,d=c),d&&this.updateSubpathStartSegment(d),a},PRECISION:3,removeSegment:function(a){var b=this.segments,c=b.length;if(0===c)throw new Error("Path has no segments.");if(a<0&&(a=c+a),a>=c||a<0)throw new Error("Index out of range.");var d=b.splice(a,1)[0],e=d.previousSegment,f=d.nextSegment;e&&(e.nextSegment=f),f&&(f.previousSegment=e),d.isSubpathStart&&f&&this.updateSubpathStartSegment(f)},replaceSegment:function(a,b){var c=this.segments,d=c.length;if(0===d)throw new Error("Path has no segments.");if(a<0&&(a=d+a),a>=d||a<0)throw new Error("Index out of range.");var e,f=c[a],g=f.previousSegment,h=f.nextSegment,i=f.isSubpathStart;if(Array.isArray(b)){if(!b[0].isSegment)throw new Error("Segments required.");c.splice(a,1);for(var j=b.length,k=0;k1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.segmentIndexAtLength(i,g)},toPoints:function(a){var b=this.segments,c=b.length;if(0===c)return null;a=a||{};for(var d=void 0===a.precision?this.PRECISION:a.precision,e=void 0===a.segmentSubdivisions?this.getSegmentSubdivisions({precision:d}):a.segmentSubdivisions,f=[],g=[],h=0;h0){var k=j.map(function(a){return a.start});Array.prototype.push.apply(g,k)}else g.push(i.start)}else g.length>0&&(g.push(b[h-1].end),f.push(g),g=[])}return g.length>0&&(g.push(this.end),f.push(g)),f},toPolylines:function(a){var b=[],c=this.toPoints(a);if(!c)return null;for(var d=0,e=c.length;d=0;e?j++:j--){var k=c[j],l=g[j],m=k.length({precision:f,subdivisions:l});if(k.isVisible){if(a<=i+m)return j;h=j}i+=m}return h},tangentAt:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;a<0&&(a=0),a>1&&(a=1),b=b||{};var e=void 0===b.precision?this.PRECISION:b.precision,f=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:e}):b.segmentSubdivisions,g={precision:e,segmentSubdivisions:f},h=this.length(g),i=h*a;return this.tangentAtLength(i,g)},tangentAtLength:function(a,b){var c=this.segments,d=c.length;if(0===d)return null;var e=!0;a<0&&(e=!1,a=-a),b=b||{};for(var f,g=void 0===b.precision?this.PRECISION:b.precision,h=void 0===b.segmentSubdivisions?this.getSegmentSubdivisions({precision:g}):b.segmentSubdivisions,i=0,j=e?0:d-1;e?j=0;e?j++:j--){var k=c[j],l=h[j],m=k.length({precision:g,subdivisions:l});if(k.isDifferentiable()){if(a<=i+m)return k.tangentAtLength((e?1:-1)*(a-i),{precision:g,subdivisions:l});f=k}i+=m}if(f){var n=e?1:0;return f.tangentAtT(n)}return null},tangentAtT:function(a){var b=this.segments,c=b.length;if(0===c)return null;var d=a.segmentIndex;if(d<0)return b[0].tangentAtT(0);if(d>=c)return b[c-1].tangentAtT(1);var e=a.value;return e<0?e=0:e>1&&(e=1),b[d].tangentAtT(e)},translate:function(a,b){for(var c=this.segments,d=c.length,e=0;e=0;c--){var d=a[c];if(d.isVisible)return d.end}return a[b-1].end}});var u=c.Point=function(a,b){if(!(this instanceof u))return new u(a,b);if("string"==typeof a){var c=a.split(a.indexOf("@")===-1?" ":"@");a=parseFloat(c[0]),b=parseFloat(c[1])}else Object(a)===a&&(b=a.y,a=a.x);this.x=void 0===a?0:a,this.y=void 0===b?0:b};u.fromPolar=function(a,b,c){c=c&&new u(c)||new u(0,0);var d=e(a*f(b)),h=e(a*g(b)),i=x(z(b));return i<90?h=-h:i<180?(d=-d,h=-h):i<270&&(d=-d),new u(c.x+d,c.y+h)},u.random=function(a,b,c,d){return new u(m(o()*(b-a+1)+a),m(o()*(d-c+1)+c))},u.prototype={adhereToRect:function(a){return a.containsPoint(this)?this:(this.x=i(j(this.x,a.x),a.x+a.width),this.y=i(j(this.y,a.y),a.y+a.height),this)},bearing:function(a){return new s(this,a).bearing()},changeInAngle:function(a,b,c){return this.clone().offset(-a,-b).theta(c)-this.theta(c)},clone:function(){return new u(this)},difference:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),new u(this.x-(a||0),this.y-(b||0))},distance:function(a){return new s(this,a).length()},squaredDistance:function(a){return new s(this,a).squaredLength()},equals:function(a){return!!a&&this.x===a.x&&this.y===a.y},magnitude:function(){return h(this.x*this.x+this.y*this.y)||.01},manhattanDistance:function(a){return e(a.x-this.x)+e(a.y-this.y)},move:function(a,b){var c=A(new u(a).theta(this)),d=this.offset(f(c)*b,-g(c)*b);return d},normalize:function(a){var b=(a||1)/this.magnitude();return this.scale(b,b)},offset:function(a,b){return Object(a)===a&&(b=a.y,a=a.x),this.x+=a||0,this.y+=b||0,this},reflection:function(a){return new u(a).move(this,this.distance(a))},rotate:function(a,b){a=a||new c.Point(0,0),b=A(x(-b));var d=f(b),e=g(b),h=d*(this.x-a.x)-e*(this.y-a.y)+a.x,i=e*(this.x-a.x)+d*(this.y-a.y)+a.y;return this.x=h,this.y=i,this},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this},scale:function(a,b,c){return c=c&&new u(c)||new u(0,0),this.x=c.x+a*(this.x-c.x),this.y=c.y+b*(this.y-c.y),this},snapToGrid:function(a,b){return this.x=y(this.x,a),this.y=y(this.y,b||a),this},theta:function(a){a=new u(a);var b=-(a.y-this.y),c=a.x-this.x,d=k(b,c);return d<0&&(d=2*n+d),180*d/n},angleBetween:function(a,b){var c=this.equals(a)||this.equals(b)?NaN:this.theta(b)-this.theta(a);return c<0&&(c+=360),c},vectorAngle:function(a){var b=new u(0,0);return b.angleBetween(this,a)},toJSON:function(){return{x:this.x,y:this.y}},toPolar:function(a){a=a&&new u(a)||new u(0,0);var b=this.x,c=this.y;return this.x=h((b-a.x)*(b-a.x)+(c-a.y)*(c-a.y)),this.y=A(a.theta(new u(b,c))),this},toString:function(){return this.x+"@"+this.y},update:function(a,b){return this.x=a||0,this.y=b||0,this},dot:function(a){return a?this.x*a.x+this.y*a.y:NaN},cross:function(a,b){return a&&b?(b.x-this.x)*(a.y-this.y)-(b.y-this.y)*(a.x-this.x):NaN},lerp:function(a,b){var c=this.x,d=this.y;return new u((1-b)*c+b*a.x,(1-b)*d+b*a.y)}},u.prototype.translate=u.prototype.offset;var v=c.Rect=function(a,b,c,d){return this instanceof v?(Object(a)===a&&(b=a.y,c=a.width,d=a.height,a=a.x),this.x=void 0===a?0:a,this.y=void 0===b?0:b,this.width=void 0===c?0:c,void(this.height=void 0===d?0:d)):new v(a,b,c,d)};v.fromEllipse=function(a){return a=new r(a),new v(a.x-a.a,a.y-a.b,2*a.a,2*a.b)},v.prototype={bbox:function(a){if(!a)return this.clone();var b=A(a||0),c=e(g(b)),d=e(f(b)),h=this.width*d+this.height*c,i=this.width*c+this.height*d;return new v(this.x+(this.width-h)/2,this.y+(this.height-i)/2,h,i)},bottomLeft:function(){return new u(this.x,this.y+this.height)},bottomLine:function(){return new s(this.bottomLeft(),this.bottomRight())},bottomMiddle:function(){ +return new u(this.x+this.width/2,this.y+this.height)},center:function(){return new u(this.x+this.width/2,this.y+this.height/2)},clone:function(){return new v(this)},containsPoint:function(a){return a=new u(a),a.x>=this.x&&a.x<=this.x+this.width&&a.y>=this.y&&a.y<=this.y+this.height},containsRect:function(a){var b=new v(this).normalize(),c=new v(a).normalize(),d=b.width,e=b.height,f=c.width,g=c.height;if(!(d&&e&&f&&g))return!1;var h=b.x,i=b.y,j=c.x,k=c.y;return f+=j,d+=h,g+=k,e+=i,h<=j&&f<=d&&i<=k&&g<=e},corner:function(){return new u(this.x+this.width,this.y+this.height)},equals:function(a){var b=new v(this).normalize(),c=new v(a).normalize();return b.x===c.x&&b.y===c.y&&b.width===c.width&&b.height===c.height},intersect:function(a){var b=this.origin(),c=this.corner(),d=a.origin(),e=a.corner();if(e.x<=b.x||e.y<=b.y||d.x>=c.x||d.y>=c.y)return null;var f=j(b.x,d.x),g=j(b.y,d.y);return new v(f,g,i(c.x,e.x)-f,i(c.y,e.y)-g)},intersectionWithLine:function(a){var b,c,d=this,e=[d.topLine(),d.rightLine(),d.bottomLine(),d.leftLine()],f=[],g=[],h=e.length;for(c=0;c0?f:null},intersectionWithLineFromCenterToPoint:function(a,b){a=new u(a);var c,d=new u(this.x+this.width/2,this.y+this.height/2);b&&a.rotate(d,b);for(var e=[this.topLine(),this.rightLine(),this.bottomLine(),this.leftLine()],f=new s(d,a),g=e.length-1;g>=0;--g){var h=e[g].intersection(f);if(null!==h){c=h;break}}return c&&b&&c.rotate(d,-b),c},leftLine:function(){return new s(this.topLeft(),this.bottomLeft())},leftMiddle:function(){return new u(this.x,this.y+this.height/2)},moveAndExpand:function(a){return this.x+=a.x||0,this.y+=a.y||0,this.width+=a.width||0,this.height+=a.height||0,this},offset:function(a,b){return u.prototype.offset.call(this,a,b)},inflate:function(a,b){return void 0===a&&(a=0),void 0===b&&(b=a),this.x-=a,this.y-=b,this.width+=2*a,this.height+=2*b,this},normalize:function(){var a=this.x,b=this.y,c=this.width,d=this.height;return this.width<0&&(a=this.x+this.width,c=-this.width),this.height<0&&(b=this.y+this.height,d=-this.height),this.x=a,this.y=b,this.width=c,this.height=d,this},origin:function(){return new u(this.x,this.y)},pointNearestToPoint:function(a){if(a=new u(a),this.containsPoint(a)){var b=this.sideNearestToPoint(a);switch(b){case"right":return new u(this.x+this.width,a.y);case"left":return new u(this.x,a.y);case"bottom":return new u(a.x,this.y+this.height);case"top":return new u(a.x,this.y)}}return a.adhereToRect(this)},rightLine:function(){return new s(this.topRight(),this.bottomRight())},rightMiddle:function(){return new u(this.x+this.width,this.y+this.height/2)},round:function(a){var b=p(10,a||0);return this.x=l(this.x*b)/b,this.y=l(this.y*b)/b,this.width=l(this.width*b)/b,this.height=l(this.height*b)/b,this},scale:function(a,b,c){return c=this.origin().scale(a,b,c),this.x=c.x,this.y=c.y,this.width*=a,this.height*=b,this},maxRectScaleToFit:function(a,b){a=new v(a),b||(b=a.center());var c,d,e,f,g,h,j,k,l=b.x,m=b.y;c=d=e=f=g=h=j=k=1/0;var n=a.topLeft();n.xl&&(d=(this.x+this.width-l)/(o.x-l)),o.y>m&&(h=(this.y+this.height-m)/(o.y-m));var p=a.topRight();p.x>l&&(e=(this.x+this.width-l)/(p.x-l)),p.ym&&(k=(this.y+this.height-m)/(q.y-m)),{sx:i(c,d,e,f),sy:i(g,h,j,k)}},maxRectUniformScaleToFit:function(a,b){var c=this.maxRectScaleToFit(a,b);return i(c.sx,c.sy)},sideNearestToPoint:function(a){a=new u(a);var b=a.x-this.x,c=this.x+this.width-a.x,d=a.y-this.y,e=this.y+this.height-a.y,f=b,g="left";return cb&&(b=i),jd&&(d=j)}return new v(a,c,b-a,d-c)},clone:function(){var a=this.points,b=a.length;if(0===b)return new w;for(var c=[],d=0;df.x&&(f=c[a]);var g=[];for(a=0;a2){var j=g[g.length-1];g.unshift(j)}for(var k,l,m,n,o,p,q={},r=[];0!==g.length;)if(k=g.pop(),l=k[0],!q.hasOwnProperty(k[0]+"@@"+k[1]))for(var s=!1;!s;)if(r.length<2)r.push(k),s=!0;else{m=r.pop(),n=m[0],o=r.pop(),p=o[0];var t=p.cross(n,l);if(t<0)r.push(o),r.push(m),r.push(k),s=!0;else if(0===t){var u=1e-10,v=n.angleBetween(p,l);e(v-180)2&&r.pop();var x,y=-1;for(b=r.length,a=0;a0){var B=r.slice(y),C=r.slice(0,y);A=B.concat(C)}else A=r;var D=[];for(b=A.length,a=0;a=1)return b[c-1].clone();var d=this.length(),e=d*a;return this.pointAtLength(e)},pointAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return b[0].clone();var d=!0;a<0&&(d=!1,a=-a);for(var e=0,f=c-1,g=d?0:f-1;d?g=0;d?g++:g--){var h=b[g],i=b[g+1],j=new s(h,i),k=h.distance(i);if(a<=e+k)return j.pointAtLength((d?1:-1)*(a-e));e+=k}var l=d?b[c-1]:b[0];return l.clone()},scale:function(a,b,c){var d=this.points,e=d.length;if(0===e)return this;for(var f=0;f1&&(a=1);var d=this.length(),e=d*a;return this.tangentAtLength(e)},tangentAtLength:function(a){var b=this.points,c=b.length;if(0===c)return null;if(1===c)return null;var d=!0;a<0&&(d=!1,a=-a);for(var e,f=0,g=c-1,h=d?0:g-1;d?h=0;d?h++:h--){var i=b[h],j=b[h+1],k=new s(i,j),l=i.distance(j);if(k.isDifferentiable()){if(a<=f+l)return k.tangentAtLength((d?1:-1)*(a-f));e=k}f+=l}if(e){var m=d?1:0;return e.tangentAt(m)}return null},intersectionWithLine:function(a){for(var b=new s(a),c=[],d=this.points,e=0,f=d.length-1;e0?c:null},translate:function(a,b){var c=this.points,d=c.length;if(0===d)return this;for(var e=0;e=1?b:b*a},pointAtT:function(a){if(this.pointAt)return this.pointAt(a);throw new Error("Neither pointAtT() nor pointAt() function is implemented.")},previousSegment:null,subpathStartSegment:null,tangentAtT:function(a){if(this.tangentAt)return this.tangentAt(a);throw new Error("Neither tangentAtT() nor tangentAt() function is implemented.")},bbox:function(){throw new Error("Declaration missing for virtual function.")},clone:function(){throw new Error("Declaration missing for virtual function.")},closestPoint:function(){throw new Error("Declaration missing for virtual function.")},closestPointLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointNormalizedLength:function(){throw new Error("Declaration missing for virtual function.")},closestPointTangent:function(){throw new Error("Declaration missing for virtual function.")},equals:function(){throw new Error("Declaration missing for virtual function.")},getSubdivisions:function(){throw new Error("Declaration missing for virtual function.")},isDifferentiable:function(){throw new Error("Declaration missing for virtual function.")},length:function(){throw new Error("Declaration missing for virtual function.")},pointAt:function(){throw new Error("Declaration missing for virtual function.")},pointAtLength:function(){throw new Error("Declaration missing for virtual function.")},scale:function(){throw new Error("Declaration missing for virtual function.")},tangentAt:function(){throw new Error("Declaration missing for virtual function.")},tangentAtLength:function(){throw new Error("Declaration missing for virtual function.")},translate:function(){throw new Error("Declaration missing for virtual function.")},serialize:function(){throw new Error("Declaration missing for virtual function.")},toString:function(){throw new Error("Declaration missing for virtual function.")}},C=function(){for(var b=[],c=arguments.length,d=0;d0)throw new Error("Closepath constructor expects no arguments.");return this},J={clone:function(){return new I},getSubdivisions:function(){return[]},isDifferentiable:function(){return!(!this.previousSegment||!this.subpathStartSegment)&&!this.start.equals(this.end)},scale:function(){return this},translate:function(){return this},type:"Z",serialize:function(){return this.type},toString:function(){return this.type+" "+this.start+" "+this.end}};Object.defineProperty(J,"start",{configurable:!0,enumerable:!0,get:function(){if(!this.previousSegment)throw new Error("Missing previous segment. (This segment cannot be the first segment of a path; OR segment has not yet been added to a path.)");return this.previousSegment.end}}),Object.defineProperty(J,"end",{configurable:!0,enumerable:!0,get:function(){if(!this.subpathStartSegment)throw new Error("Missing subpath start segment. (This segment needs a subpath start segment (e.g. Moveto); OR segment has not yet been added to a path.)");return this.subpathStartSegment.end}}),I.prototype=b(B,s.prototype,J);var K=t.segmentTypes={L:C,C:E,M:G,Z:I,z:I};return t.regexSupportedData=new RegExp("^[\\s\\d"+Object.keys(K).join("")+",.]*$"),t.isDataSupported=function(a){return"string"==typeof a&&this.regexSupportedData.test(a)},c}(); +var V,Vectorizer;V=Vectorizer=function(){"use strict";function a(a,b){a||(a={});var c=q("textPath"),d=a.d;if(d&&void 0===a["xlink:href"]){var e=q("path").attr("d",d).appendTo(b.defs());c.attr("xlink:href","#"+e.id)}return q.isObject(a)&&c.attr(a),c.node}function b(a,b,c){c||(c={});for(var d=c.includeAnnotationIndices,e=c.eol,f=c.lineHeight,g=c.baseSize,h=0,i={},j=b.length-1,k=0;k<=j;k++){var l=b[k],m=null;if(q.isObject(l)){var n=l.attrs,o=q("tspan",n),p=o.node,r=l.t;e&&k===j&&(r+=e),p.textContent=r;var s=n.class;s&&o.addClass(s),d&&o.attr("annotations",l.annotations),m=parseFloat(n["font-size"]),void 0===m&&(m=g),m&&m>h&&(h=m)}else e&&k===j&&(l+=e),p=document.createTextNode(l||" "),g&&g>h&&(h=g);a.appendChild(p)}return h&&(i.maxFontSize=h),f?i.lineHeight=f:h&&(i.lineHeight=1.2*h),i}function c(a,b){var c=parseFloat(a);return s.test(a)?c*b:c}function d(a,b,d,e){if(!Array.isArray(b))return 0;var f=b.length;if(!f)return 0;for(var g=b[0],h=c(g.maxFontSize,d)||d,i=0,j=c(e,d),k=1;k1){var e,g,h=[];for(e=0,g=d.childNodes.length;e0&&D.setAttribute("dy",B),(y>0||g)&&D.setAttribute("x",j),D.className.baseVal=C,r.appendChild(D),v+=E.length+1}if(i)if(l)B=d(h,x,p,o);else if("top"===h)B="0.8em";else{var I;switch(z>0?(I=parseFloat(o)||1,I*=z,s.test(o)||(I/=p)):I=0,h){case"middle":B=.3-I/2+"em";break;case"bottom":B=-I-.3+"em"}}else 0===h?B="0em":h?B=h:(B=0,null===this.attr("y")&&this.attr("y",u||"0.8em"));return r.firstChild.setAttribute("dy",B),this.append(r),this},r.removeAttr=function(a){var b=q.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},r.attr=function(a,b){if(q.isUndefined(a)){for(var c=this.node.attributes,d={},e=0;e1&&k.push(k[0]),new g.Polyline(k);case"PATH":return l=this.attr("d"),g.Path.isDataSupported(l)||(l=q.normalizePathData(l)),new g.Path(l);case"LINE":return x1=parseFloat(this.attr("x1"))||0,y1=parseFloat(this.attr("y1"))||0,x2=parseFloat(this.attr("x2"))||0,y2=parseFloat(this.attr("y2"))||0,new g.Line({x:x1,y:y1},{x:x2,y:y2})}return this.getBBox()},r.findIntersection=function(a,b){var c=this.svg().node;b=b||c;var d=this.getBBox({target:b}),e=d.center();if(d.intersectionWithLineFromCenterToPoint(a)){var f,h=this.tagName();if("RECT"===h){var i=new g.Rect(parseFloat(this.attr("x")||0),parseFloat(this.attr("y")||0),parseFloat(this.attr("width")),parseFloat(this.attr("height"))),j=this.getTransformToElement(b),k=q.decomposeMatrix(j),l=c.createSVGTransform();l.setRotate(-k.rotation,e.x,e.y);var m=q.transformRect(i,l.matrix.multiply(j));f=new g.Rect(m).intersectionWithLineFromCenterToPoint(a,k.rotation)}else if("PATH"===h||"POLYGON"===h||"POLYLINE"===h||"CIRCLE"===h||"ELLIPSE"===h){var n,o,p,r,s,t,u="PATH"===h?this:this.convertToPath(),v=u.sample(),w=1/0,x=[];for(n=0;n'+(a||"")+"",c=q.parseXML(b,{async:!1});return c.documentElement},q.idCounter=0,q.uniqueId=function(){return"v-"+ ++q.idCounter},q.toNode=function(a){return q.isV(a)?a.node:a.nodeName&&a||a[0]},q.ensureId=function(a){return a=q.toNode(a),a.id||(a.id=q.uniqueId())},q.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},q.isUndefined=function(a){return"undefined"==typeof a},q.isString=function(a){return"string"==typeof a},q.isObject=function(a){return a&&"object"==typeof a},q.isArray=Array.isArray,q.parseXML=function(a,b){b=b||{};var c;try{var d=new DOMParser;q.isUndefined(b.async)||(d.async=b.async),c=d.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},q.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var b=a.split(":");return{ns:f[b[0]],local:b[1]}}return{ns:null,local:a}},q.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,q.transformSeparatorRegex=/[ ,]+/,q.transformationListRegex=/^(\w+)\((.*)\)/,q.transformStringToMatrix=function(a){var b=q.createSVGMatrix(),c=a&&a.match(q.transformRegex);if(!c)return b;for(var d=0,e=c.length;d=0){var f=q.transformStringToMatrix(a),g=q.decomposeMatrix(f);b=[g.translateX,g.translateY],d=[g.scaleX,g.scaleY],c=[g.rotation];var h=[];0===b[0]&&0===b[0]||h.push("translate("+b+")"),1===d[0]&&1===d[1]||h.push("scale("+d+")"),0!==c[0]&&h.push("rotate("+c+")"),a=h.join(" ")}else{var i=a.match(/translate\((.*?)\)/);i&&(b=i[1].split(e));var j=a.match(/rotate\((.*?)\)/);j&&(c=j[1].split(e));var k=a.match(/scale\((.*?)\)/);k&&(d=k[1].split(e))}}var l=d&&d[0]?parseFloat(d[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:l,sy:d&&d[1]?parseFloat(d[1]):l}}},q.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},q.decomposeMatrix=function(a){var b=q.deltaTransformPoint(a,{x:0,y:1}),c=q.deltaTransformPoint(a,{x:1,y:0}),d=180/j*k(b.y,b.x)-90,e=180/j*k(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:l(a.a*a.a+a.b*a.b),scaleY:l(a.c*a.c+a.d*a.d),skewX:d,skewY:e,rotation:d}},q.matrixToScale=function(a){var b,c,d,e;return a?(b=q.isUndefined(a.a)?1:a.a,e=q.isUndefined(a.d)?1:a.d,c=a.b,d=a.c):b=e=1,{sx:c?l(b*b+c*c):b,sy:d?l(d*d+e*e):e}},q.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=q.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(k(b.y,b.x))-90)}},q.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},q.isV=function(a){return a instanceof q},q.isVElement=q.isV;var t=q("svg").node;return q.createSVGMatrix=function(a){var b=t.createSVGMatrix();for(var c in a)b[c]=a[c];return b},q.createSVGTransform=function(a){return q.isUndefined(a)?t.createSVGTransform():(a instanceof SVGMatrix||(a=q.createSVGMatrix(a)),t.createSVGTransformFromMatrix(a))},q.createSVGPoint=function(a,b){var c=t.createSVGPoint();return c.x=a,c.y=b,c},q.transformRect=function(a,b){var c=t.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var e=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var f=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var h=c.matrixTransform(b),i=m(d.x,e.x,f.x,h.x),j=n(d.x,e.x,f.x,h.x),k=m(d.y,e.y,f.y,h.y),l=n(d.y,e.y,f.y,h.y);return new g.Rect(i,k,j-i,l-k)},q.transformPoint=function(a,b){return new g.Point(q.createSVGPoint(a.x,a.y).matrixTransform(b))},q.transformLine=function(a,b){return new g.Line(q.transformPoint(a.start,b),q.transformPoint(a.end,b))},q.transformPolyline=function(a,b){var c=a instanceof g.Polyline?a.points:a;q.isArray(c)||(c=[]);for(var d=[],e=0,f=c.length;e=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L"+f*q+","+f*r+"A"+f+","+f+" 0 "+l+",0 "+f*m+","+f*n+"Z":"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L0,0Z"},q.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?q.isObject(a[c])&&q.isObject(b[c])?a[c]=q.mergeAttrs(a[c],b[c]):q.isObject(a[c])?a[c]=q.mergeAttrs(a[c],q.styleToObject(b[c])):q.isObject(b[c])?a[c]=q.mergeAttrs(q.styleToObject(a[c]),b[c]):a[c]=q.mergeAttrs(q.styleToObject(a[c]),q.styleToObject(b[c])):a[c]=b[c];return a},q.annotateString=function(a,b,c){b=b||[],c=c||{};for(var d,e,f,g=c.offset||0,h=[],i=[],j=0;j=m&&j=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},q.convertLineToPathData=function(a){a=q(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},q.convertPolygonToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)+" Z"},q.convertPolylineToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)},q.svgPointsToPath=function(a){for(var b=0,c=a.length;b1&&(z=o(z),d*=z,e*=z);var A=d*d,B=e*e,C=(g==h?-1:1)*o(p((A*B-A*y*y-B*x*x)/(A*y*y+B*x*x))),D=C*d*y/e+(a+i)/2,E=C*-e*x/d+(c+q)/2,F=n(((c-E)/e).toFixed(9)),G=n(((q-E)/e).toFixed(9));F=aG&&(F-=2*j),(!h&&G)>F&&(G-=2*j)}var H=G-F;if(p(H)>t){var I=G,J=i,K=q;G=F+t*((h&&G)>F?1:-1),i=D+d*l(G),q=E+e*k(G),v=b(i,q,d,e,f,0,h,J,K,[G,I,D,E])}H=G-F;var L=l(F),M=k(F),N=l(G),O=k(G),P=m(H/4),Q=4/3*(d*P),R=4/3*(e*P),S=[a,c],T=[a+Q*M,c-R*L],U=[i+Q*O,q-R*N],V=[i,q];if(T[0]=2*S[0]-T[0],T[1]=2*S[1]-T[1],r)return[T,U,V].concat(v);v=[T,U,V].concat(v).join().split(",");for(var W=[],X=v.length,Y=0;Y2&&(c.push([d].concat(f.splice(0,2))),g="l",d="m"===d?"l":"L");f.length>=b[g]&&(c.push([d].concat(f.splice(0,b[g]))),b[g]););}),c}function d(a){if(Array.isArray(a)&&Array.isArray(a&&a[0])||(a=c(a)),!a||!a.length)return[["M",0,0]];for(var b,d=[],e=0,f=0,g=0,h=0,i=0,j=a.length,k=i;k7){a[b].shift();for(var c=a[b];c.length;)i[b]="A",a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),l=g.length}}for(var g=d(c),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i=[],j="",k="",l=g.length,m=0;m0&&(k=i[m-1])),g[m]=e(g[m],h,k),"A"!==i[m]&&"C"===j&&(i[m]="C"),f(g,m);var n=g[m],o=n.length;h.x=n[o-2],h.y=n[o-1],h.bx=parseFloat(n[o-4])||h.x,h.by=parseFloat(n[o-3])||h.y}return g[0][0]&&"M"===g[0][0]||g.unshift(["M",0,0]),g}var f="\t\n\v\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029",g=new RegExp("([a-z])["+f+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+f+"]*,?["+f+"]*)+)","ig"),h=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+f+"]*,?["+f+"]*","ig"),i=Math,j=i.PI,k=i.sin,l=i.cos,m=i.tan,n=i.asin,o=i.sqrt,p=i.abs;return function(a){return e(a).join(",").split(",").join(" ")}}(),q.namespace=f,q}(); +var joint={version:"2.1.0",config:{classNamePrefix:"joint-",defaultTheme:"default"},dia:{},ui:{},layout:{},shapes:{},format:{},connectors:{},highlighters:{},routers:{},anchors:{},connectionPoints:{},connectionStrategies:{},linkTools:{},mvc:{views:{}},setTheme:function(a,b){b=b||{},joint.util.invoke(joint.mvc.views,"setTheme",a,b),joint.mvc.View.prototype.defaultTheme=a},env:{_results:{},_tests:{svgforeignobject:function(){return!!document.createElementNS&&/SVGForeignObject/.test({}.toString.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))}},addTest:function(a,b){return joint.env._tests[a]=b},test:function(a){var b=joint.env._tests[a];if(!b)throw new Error('Test not defined ("'+a+'"). Use `joint.env.addTest(name, fn) to add a new test.`');var c=joint.env._results[a];if("undefined"!=typeof c)return c;try{c=b()}catch(a){c=!1}return joint.env._results[a]=c,c}},util:{hashCode:function(a){var b=0;if(0==a.length)return b;for(var c=0;c0){var f=joint.util.getByPath(a,d,c);f&&delete f[e]}else delete a[e];return a},flattenObject:function(a,b,c){b=b||"/";var d={};for(var e in a)if(a.hasOwnProperty(e)){var f="object"==typeof a[e];if(f&&c&&c(a[e])&&(f=!1),f){var g=this.flattenObject(a[e],b,c);for(var h in g)g.hasOwnProperty(h)&&(d[e+b+h]=g[h])}else d[e]=a[e]}return d},uuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"==a?b:3&b|8;return c.toString(16)})},guid:function(a){return this.guid.id=this.guid.id||1,a.id=void 0===a.id?"j_"+this.guid.id++:a.id,a.id},toKebabCase:function(a){return a.replace(/[A-Z]/g,"-$&").toLowerCase()},mixin:_.assign,supplement:_.defaults,deepMixin:_.mixin,deepSupplement:_.defaultsDeep,normalizeEvent:function(a){var b=a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0];if(b){for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return a},nextFrame:function(){var a;if("undefined"!=typeof window&&(a=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame),!a){var b=0;a=function(a){var c=(new Date).getTime(),d=Math.max(0,16-(c-b)),e=setTimeout(function(){a(c+d)},d);return b=c+d,e}}return function(b,c){return a(c?b.bind(c):b)}}(),cancelFrame:function(){var a,b="undefined"!=typeof window;return b&&(a=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame),a=a||clearTimeout,b?a.bind(window):a}(),shapePerimeterConnectionPoint:function(a,b,c,d){var e,f;if(!c){var g=b.$(".scalable")[0],h=b.$(".rotatable")[0];g&&g.firstChild?c=g.firstChild:h&&h.firstChild&&(c=h.firstChild)}return c?(f=V(c).findIntersection(d,a.paper.viewport),f||(e=V(c).getBBox({target:a.paper.viewport}))):(e=b.model.getBBox(),f=e.intersectionWithLineFromCenterToPoint(d)),f||e.center()},isPercentage:function(a){return joint.util.isString(a)&&"%"===a.slice(-1)},parseCssNumeric:function(a,b){b=b||[];var c={value:parseFloat(a)};if(Number.isNaN(c.value))return null;var d=b.join("|");if(joint.util.isString(a)){var e=new RegExp("(\\d+)("+d+")$").exec(a);if(!e)return null;e[2]&&(c.unit=e[2])}return c},breakText:function(a,b,c,d){d=d||{},c=c||{};var e=b.width,f=b.height,g=d.svgDocument||V("svg").node,h=V("tspan").node,i=V("text").attr(c).append(h).node,j=document.createTextNode("");i.style.opacity=0,i.style.display="block",h.style.display="block",h.appendChild(j),g.appendChild(i),d.svgDocument||document.body.appendChild(g);for(var k,l,m=d.separator||" ",n=d.eol||"\n",o=a.split(m),p=[],q=[],r=0,s=0,t=o.length;r=0)if(u.length>1){for(var v=u.split(n),w=0,x=v.length-1;wf){q.splice(Math.floor(f/l));break}}}}return d.svgDocument?g.removeChild(i):document.body.removeChild(g),q.join(n)},sanitizeHTML:function(a){var b=$($.parseHTML("
"+a+"
",null,!1));return b.find("*").each(function(){var a=this;$.each(a.attributes,function(){var b=this,c=b.name,d=b.value;0!==c.indexOf("on")&&0!==d.indexOf("javascript:")||$(a).removeAttr(c)})}),b.html()},downloadBlob:function(a,b){if(window.navigator.msSaveBlob)window.navigator.msSaveBlob(a,b);else{var c=window.URL.createObjectURL(a),d=document.createElement("a");d.href=c,d.download=b,document.body.appendChild(d),d.click(),document.body.removeChild(d),window.URL.revokeObjectURL(c)}},downloadDataUri:function(a,b){var c=joint.util.dataUriToBlob(a);joint.util.downloadBlob(c,b)},dataUriToBlob:function(a){a=a.replace(/\s/g,""),a=decodeURIComponent(a);var b,c=a.indexOf(","),d=a.slice(0,c),e=d.split(":")[1].split(";")[0],f=a.slice(c+1);b=d.indexOf("base64")>=0?atob(f):unescape(encodeURIComponent(f));for(var g=new window.Uint8Array(b.length),h=0;h=1)return 1;var b=a*a,c=b*a;return 4*(a<.5?c:3*(a-b)+c-.75)},exponential:function(a){return Math.pow(2,10*(a-1))},bounce:function(a){for(var b=0,c=1;1;b+=c,c/=2)if(a>=(7-4*b)/11){var d=(11-6*b-11*a)/4;return-d*d+c*c}},reverse:function(a){return function(b){return 1-a(1-b)}},reflect:function(a){return function(b){return.5*(b<.5?a(2*b):2-a(2-2*b))}},clamp:function(a,b,c){return b=b||0,c=c||1,function(d){var e=a(d);return ec?c:e}},back:function(a){return a||(a=1.70158),function(b){return b*b*((a+1)*b-a)}},elastic:function(a){return a||(a=1.5),function(b){return Math.pow(2,10*(b-1))*Math.cos(20*Math.PI*a/3*b)}}},interpolate:{number:function(a,b){var c=b-a;return function(b){return a+c*b}},object:function(a,b){var c=Object.keys(a);return function(d){var e,f,g={};for(e=c.length-1;e!=-1;e--)f=c[e],g[f]=a[f]+(b[f]-a[f])*d;return g}},hexColor:function(a,b){var c=parseInt(a.slice(1),16),d=parseInt(b.slice(1),16),e=255&c,f=(255&d)-e,g=65280&c,h=(65280&d)-g,i=16711680&c,j=(16711680&d)-i;return function(a){var b=e+f*a&255,c=g+h*a&65280,d=i+j*a&16711680;return"#"+(1<<24|b|c|d).toString(16).slice(1)}},unit:function(a,b){var c=/(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/,d=c.exec(a),e=c.exec(b),f=e[1].indexOf("."),g=f>0?e[1].length-f-1:0;a=+d[1];var h=+e[1]-a,i=d[2];return function(b){return(a+h*b).toFixed(g)+i}}},filter:{outline:function(a){var b='',c=Number.isFinite(a.margin)?a.margin:2,d=Number.isFinite(a.width)?a.width:1;return joint.util.template(b)({color:a.color||"blue",opacity:Number.isFinite(a.opacity)?a.opacity:1,outerRadius:c+d,innerRadius:c})},highlight:function(a){var b='';return joint.util.template(b)({color:a.color||"red",width:Number.isFinite(a.width)?a.width:1,blur:Number.isFinite(a.blur)?a.blur:0,opacity:Number.isFinite(a.opacity)?a.opacity:1})},blur:function(a){var b=Number.isFinite(a.x)?a.x:2;return joint.util.template('')({stdDeviation:Number.isFinite(a.y)?[b,a.y]:b})},dropShadow:function(a){var b="SVGFEDropShadowElement"in window?'':'';return joint.util.template(b)({dx:a.dx||0,dy:a.dy||0,opacity:Number.isFinite(a.opacity)?a.opacity:1,color:a.color||"black",blur:Number.isFinite(a.blur)?a.blur:4})},grayscale:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},sepia:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},saturate:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:1-b})},hueRotate:function(a){return joint.util.template('')({angle:a.angle||0})},invert:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:1-b})},brightness:function(a){return joint.util.template('')({amount:Number.isFinite(a.amount)?a.amount:1})},contrast:function(a){var b=Number.isFinite(a.amount)?a.amount:1;return joint.util.template('')({amount:b,amount2:.5-b/2})}},format:{number:function(a,b,c){function d(a){for(var b=a.length,d=[],e=0,f=c.grouping[0];b>0&&f>0;)d.push(a.substring(b-=f,b+f)),f=c.grouping[e=(e+1)%c.grouping.length];return d.reverse().join(c.thousands)}c=c||{currency:["$",""],decimal:".",thousands:",",grouping:[3]};var e=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,f=e.exec(a),g=f[1]||" ",h=f[2]||">",i=f[3]||"",j=f[4]||"",k=f[5],l=+f[6],m=f[7],n=f[8],o=f[9],p=1,q="",r="",s=!1;switch(n&&(n=+n.substring(1)),(k||"0"===g&&"="===h)&&(k=g="0",h="=",m&&(l-=Math.floor((l-1)/4))),o){case"n":m=!0,o="g";break;case"%":p=100,r="%",o="f";break;case"p":p=100,r="%",o="r";break;case"b":case"o":case"x":case"X":"#"===j&&(q="0"+o.toLowerCase());break;case"c":case"d":s=!0,n=0;break;case"s":p=-1,o="r"}"$"===j&&(q=c.currency[0],r=c.currency[1]),"r"!=o||n||(o="g"),null!=n&&("g"==o?n=Math.max(1,Math.min(21,n)):"e"!=o&&"f"!=o||(n=Math.max(0,Math.min(20,n))));var t=k&&m;if(s&&b%1)return"";var u=b<0||0===b&&1/b<0?(b=-b,"-"):i,v=r;if(p<0){var w=this.prefix(b,n);b=w.scale(b),v=w.symbol+r}else b*=p;b=this.convert(o,b,n);var x=b.lastIndexOf("."),y=x<0?b:b.substring(0,x),z=x<0?"":c.decimal+b.substring(x+1);!k&&m&&c.grouping&&(y=d(y));var A=q.length+y.length+z.length+(t?0:u.length),B=A"===h?B+u+b:"^"===h?B.substring(0,A>>=1)+u+b+B.substring(A):u+(t?b:B+b))+v},string:function(a,b){for(var c,d="{",e=!1,f=[];(c=a.indexOf(d))!==-1;){var g,h,i;if(g=a.slice(0,c),e){h=g.split(":"),i=h.shift().split("."),g=b;for(var j=0;j8?function(a){return a/c}:function(a){return a*c},symbol:a}}),d=0;return a&&(a<0&&(a*=-1),b&&(a=this.round(a,this.precision(a,b))),d=1+Math.floor(1e-12+Math.log(a)/Math.LN10),d=Math.max(-24,Math.min(24,3*Math.floor((d<=0?d+1:d-1)/3)))),c[8+d/3]}},template:function(a){var b=/<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g;return function(c){return c=c||{},a.replace(b,function(a){for(var b=Array.from(arguments),d=b.slice(1,4).find(function(a){return!!a}),e=d.split("."),f=c[e.shift()];void 0!==f&&e.length;)f=f[e.shift()];return void 0!==f?f:""})}},toggleFullScreen:function(a){function b(a,b){for(var c=["webkit","moz","ms","o",""],d=0;d0&&b[0]||[],e=c>1&&b[c-1]||{};return Array.isArray(d)||(e instanceof joint.dia.Cell?d=b:d instanceof joint.dia.Cell&&(b.length>1&&b.pop(),d=b)),e instanceof joint.dia.Cell&&(e={}),a.call(this,d,e)}}},parseDOMJSON:function(a,b){for(var c={},d=V.namespace.xmlns,e=b||d,f=document.createDocumentFragment(),g=[a,f,e];g.length>0;){e=g.pop();for(var h=g.pop(),i=g.pop(),j=0,k=i.length;j0);return this.stopBatch("clear"),this},_prepareCell:function(a,b){var c;if(a instanceof Backbone.Model?(c=a.attributes,a.graph||b&&b.dry||(a.graph=this)):c=a,!joint.util.isString(c.type))throw new TypeError("dia.Graph: cell type must be a string.");return a},minZIndex:function(){var a=this.get("cells").first();return a?a.get("z")||0:0},maxZIndex:function(){var a=this.get("cells").last();return a?a.get("z")||0:0},addCell:function(a,b){return Array.isArray(a)?this.addCells(a,b):(a instanceof Backbone.Model?a.has("z")||a.set("z",this.maxZIndex()+1):void 0===a.z&&(a.z=this.maxZIndex()+1),this.get("cells").add(this._prepareCell(a,b),b||{}),this)},addCells:function(a,b){return a.length&&(a=joint.util.flattenDeep(a),b.position=a.length,this.startBatch("add"),a.forEach(function(a){b.position--,this.addCell(a,b)},this),this.stopBatch("add")),this},resetCells:function(a,b){var c=joint.util.toArray(a).map(function(a){return this._prepareCell(a,b)},this);return this.get("cells").reset(c,b),this},removeCells:function(a,b){return a.length&&(this.startBatch("remove"),joint.util.invoke(a,"remove",b),this.stopBatch("remove")),this},_removeCell:function(a,b,c){c=c||{},c.clear||(c.disconnectLinks?this.disconnectLinks(a,c):this.removeLinks(a,c)),this.get("cells").remove(a,{silent:!0}),a.graph===this&&(a.graph=null)},getCell:function(a){return this.get("cells").get(a)},getCells:function(){return this.get("cells").toArray()},getElements:function(){return Object.keys(this._nodes).map(this.getCell,this)},getLinks:function(){return Object.keys(this._edges).map(this.getCell,this)},getFirstCell:function(){return this.get("cells").first()},getLastCell:function(){return this.get("cells").last()},getConnectedLinks:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=[],f={};if(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),b.deep){var g=a.getEmbeddedCells({deep:!0}),h={};g.forEach(function(a){a.isLink()&&(h[a.id]=!0)}),g.forEach(function(a){a.isLink()||(d&&joint.util.forIn(this.getOutboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)),c&&joint.util.forIn(this.getInboundEdges(a.id),function(a,b){f[b]||h[b]||(e.push(this.getCell(b)),f[b]=!0)}.bind(this)))},this)}return e},getNeighbors:function(a,b){b=b||{};var c=b.inbound,d=b.outbound;void 0===c&&void 0===d&&(c=d=!0);var e=this.getConnectedLinks(a,b).reduce(function(e,f){var g=f.get("source"),h=f.get("target"),i=f.hasLoop(b);if(c&&joint.util.has(g,"id")&&!e[g.id]){var j=this.getCell(g.id);!i&&(!j||j===a||b.deep&&j.isEmbeddedIn(a))||(e[g.id]=j)}if(d&&joint.util.has(h,"id")&&!e[h.id]){var k=this.getCell(h.id);!i&&(!k||k===a||b.deep&&k.isEmbeddedIn(a))||(e[h.id]=k)}return e}.bind(this),{});return joint.util.toArray(e)},getCommonAncestor:function(){var a=Array.from(arguments).map(function(a){for(var b=[],c=a.get("parent");c;)b.push(c),c=this.getCell(c).get("parent");return b},this);a=a.sort(function(a,b){return a.length-b.length});var b=joint.util.toArray(a.shift()).find(function(b){return a.every(function(a){return a.includes(b)})});return this.getCell(b)},getSuccessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{outbound:!0})),c},cloneCells:function(a){a=joint.util.uniq(a);var b=joint.util.toArray(a).reduce(function(a,b){return a[b.id]=b.clone(),a},{});return joint.util.toArray(a).forEach(function(a){var c=b[a.id];if(c.isLink()){var d=c.get("source"),e=c.get("target");d.id&&b[d.id]&&c.prop("source/id",b[d.id].id),e.id&&b[e.id]&&c.prop("target/id",b[e.id].id)}var f=a.get("parent");f&&b[f]&&c.set("parent",b[f].id);var g=joint.util.toArray(a.get("embeds")).reduce(function(a,c){return b[c]&&a.push(b[c].id),a},[]);joint.util.isEmpty(g)||c.set("embeds",g)}),b},cloneSubgraph:function(a,b){var c=this.getSubgraph(a,b);return this.cloneCells(c)},getSubgraph:function(a,b){b=b||{};var c=[],d={},e=[],f=[];return joint.util.toArray(a).forEach(function(a){if(d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a)),b.deep){var g=a.getEmbeddedCells({deep:!0});g.forEach(function(a){d[a.id]||(c.push(a),d[a.id]=a,a.isLink()?f.push(a):e.push(a))})}}),f.forEach(function(a){var b=a.get("source"),f=a.get("target");if(b.id&&!d[b.id]){var g=this.getCell(b.id);c.push(g),d[g.id]=g,e.push(g)}if(f.id&&!d[f.id]){var h=this.getCell(f.id);c.push(this.getCell(f.id)), +d[h.id]=h,e.push(h)}},this),e.forEach(function(a){var e=this.getConnectedLinks(a,b);e.forEach(function(a){var b=a.get("source"),e=a.get("target");!d[a.id]&&b.id&&d[b.id]&&e.id&&d[e.id]&&(c.push(a),d[a.id]=a)})},this),c},getPredecessors:function(a,b){b=b||{};var c=[];return this.search(a,function(b){b!==a&&c.push(b)},joint.util.assign({},b,{inbound:!0})),c},search:function(a,b,c){c=c||{},c.breadthFirst?this.bfs(a,b,c):this.dfs(a,b,c)},bfs:function(a,b,c){c=c||{};var d={},e={},f=[];for(f.push(a),e[a.id]=0;f.length>0;){var g=f.shift();if(!d[g.id]){if(d[g.id]=!0,b(g,e[g.id])===!1)return;this.getNeighbors(g,c).forEach(function(a){e[a.id]=e[g.id]+1,f.push(a)})}}},dfs:function(a,b,c,d,e){c=c||{};var f=d||{},g=e||0;b(a,g)!==!1&&(f[a.id]=!0,this.getNeighbors(a,c).forEach(function(a){f[a.id]||this.dfs(a,b,c,f,g+1)},this))},getSources:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._in[c]&&!joint.util.isEmpty(this._in[c])||a.push(this.getCell(c))}.bind(this)),a},getSinks:function(){var a=[];return joint.util.forIn(this._nodes,function(b,c){this._out[c]&&!joint.util.isEmpty(this._out[c])||a.push(this.getCell(c))}.bind(this)),a},isSource:function(a){return!this._in[a.id]||joint.util.isEmpty(this._in[a.id])},isSink:function(a){return!this._out[a.id]||joint.util.isEmpty(this._out[a.id])},isSuccessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{outbound:!0}),c},isPredecessor:function(a,b){var c=!1;return this.search(a,function(d){if(d===b&&d!==a)return c=!0,!1},{inbound:!0}),c},isNeighbor:function(a,b,c){c=c||{};var d=c.inbound,e=c.outbound;void 0===d&&void 0===e&&(d=e=!0);var f=!1;return this.getConnectedLinks(a,c).forEach(function(a){var c=a.get("source"),g=a.get("target");return d&&joint.util.has(c,"id")&&c.id===b.id?(f=!0,!1):e&&joint.util.has(g,"id")&&g.id===b.id?(f=!0,!1):void 0}),f},disconnectLinks:function(a,b){this.getConnectedLinks(a).forEach(function(c){c.set(c.get("source").id===a.id?"source":"target",{x:0,y:0},b)})},removeLinks:function(a,b){joint.util.invoke(this.getConnectedLinks(a),"remove",b)},findModelsFromPoint:function(a){return this.getElements().filter(function(b){return b.getBBox().containsPoint(a)})},findModelsInArea:function(a,b){a=g.rect(a),b=joint.util.defaults(b||{},{strict:!1});var c=b.strict?"containsRect":"intersect";return this.getElements().filter(function(b){return a[c](b.getBBox())})},findModelsUnderElement:function(a,b){b=joint.util.defaults(b||{},{searchBy:"bbox"});var c=a.getBBox(),d="bbox"===b.searchBy?this.findModelsInArea(c):this.findModelsFromPoint(c[b.searchBy]());return d.filter(function(b){return a.id!==b.id&&!b.isEmbeddedIn(a)})},getBBox:function(a,b){return this.getCellsBBox(a||this.getElements(),b)},getCellsBBox:function(a,b){return joint.util.toArray(a).reduce(function(a,c){return c.isLink()?a:a?a.union(c.getBBox(b)):c.getBBox(b)},null)},translate:function(a,b,c){var d=this.getCells().filter(function(a){return!a.isEmbedded()});return joint.util.invoke(d,"translate",a,b,c),this},resize:function(a,b,c){return this.resizeCells(a,b,this.getCells(),c)},resizeCells:function(a,b,c,d){var e=this.getCellsBBox(c);if(e){var f=Math.max(a/e.width,0),g=Math.max(b/e.height,0);joint.util.invoke(c,"scale",f,g,e.origin(),d)}return this},startBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)+1,this.trigger("batch:start",joint.util.assign({},b,{batchName:a}))},stopBatch:function(a,b){return b=b||{},this._batches[a]=(this._batches[a]||0)-1,this.trigger("batch:stop",joint.util.assign({},b,{batchName:a}))},hasActiveBatch:function(a){return 0===arguments.length?joint.util.toArray(this._batches).some(function(a){return a>0}):Array.isArray(a)?a.some(function(a){return!!this._batches[a]},this):!!this._batches[a]}},{validations:{multiLinks:function(a,b){var c=b.get("source"),d=b.get("target");if(c.id&&d.id){var e=b.getSourceElement();if(e){var f=a.getConnectedLinks(e,{outbound:!0}),g=f.filter(function(a){var b=a.get("source"),e=a.get("target");return b&&b.id===c.id&&(!b.port||b.port===c.port)&&e&&e.id===d.id&&(!e.port||e.port===d.port)});if(g.length>1)return!1}}return!0},linkPinning:function(a,b){return b.source().id&&b.target().id}}}),joint.util.wrapWith(joint.dia.Graph.prototype,["resetCells","addCells","removeCells"],"cells"),function(a,b,c,d,e){function f(a,b){return function(c,d){var f=e.isPercentage(c);c=parseFloat(c),f&&(c/=100);var g={};if(isFinite(c)){var h=f||c>=0&&c<=1?c*d[b]:Math.max(c+d[b],0);g[a]=h}return g}}function g(a,b,d){return function(f,g){var h=e.isPercentage(f);f=parseFloat(f),h&&(f/=100);var i;if(isFinite(f)){var j=g[d]();i=h||f>0&&f<1?j[a]+g[b]*f:j[a]+f}var k=c.Point();return k[a]=i||0,k}}function h(a,b,d){return function(f,g){var h;h="middle"===f?g[b]/2:f===d?g[b]:isFinite(f)?f>-1&&f<1?-g[b]*f:-f:e.isPercentage(f)?g[b]*parseFloat(f)/100:0;var i=c.Point();return i[a]=-(g[a]+h),i}}function i(a,b){var c="joint-shape",e=b&&b.resetOffset;return function(b,f,g){var h=d(g),i=h.data(c);if(!i||i.value!==b){var j=a(b);i={value:b,shape:j,shapeBBox:j.bbox()},h.data(c,i)}var k=i.shape.clone(),l=i.shapeBBox.clone(),m=l.origin(),n=f.origin();l.x=n.x,l.y=n.y;var o=f.maxRectScaleToFit(l,n),p=0===l.width||0===f.width?1:o.sx,q=0===l.height||0===f.height?1:o.sy;return k.scale(p,q,m),e&&k.translate(-m.x,-m.y),k}}function j(a){function d(a){return new c.Path(b.normalizePathData(a))}var e=i(d,a);return function(a,b,c){var d=e(a,b,c);return{d:d.serialize()}}}function k(a){var b=i(c.Polyline,a);return function(a,c,d){var e=b(a,c,d);return{points:e.serialize()}}}function l(a,b){var d=new c.Point(1,0);return function(c){var e,f,g=this[a](c);return g?(f=b.rotate?g.vector().vectorAngle(d):0,e=g.start):(e=this.path.start,f=0),0===f?{transform:"translate("+e.x+","+e.y+")"}:{transform:"translate("+e.x+","+e.y+") rotate("+f+")"}}}function m(a,b,c){return void 0!==c.text}function n(){return this instanceof a.dia.LinkView}function o(a){var b={},c=a.stroke;"string"==typeof c&&(b.stroke=c,b.fill=c);var d=a.strokeOpacity;return void 0===d&&(d=a["stroke-opacity"]),void 0===d&&(d=a.opacity),void 0!==d&&(b["stroke-opacity"]=d,b["fill-opacity"]=d),b}var p=a.dia.attributes={xlinkHref:{set:"xlink:href"},xlinkShow:{set:"xlink:show"},xlinkRole:{set:"xlink:role"},xlinkType:{set:"xlink:type"},xlinkArcrole:{set:"xlink:arcrole"},xlinkTitle:{set:"xlink:title"},xlinkActuate:{set:"xlink:actuate"},xmlSpace:{set:"xml:space"},xmlBase:{set:"xml:base"},xmlLang:{set:"xml:lang"},preserveAspectRatio:{set:"preserveAspectRatio"},requiredExtension:{set:"requiredExtension"},requiredFeatures:{set:"requiredFeatures"},systemLanguage:{set:"systemLanguage"},externalResourcesRequired:{set:"externalResourceRequired"},filter:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineFilter(a)+")"}},fill:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},stroke:{qualify:e.isPlainObject,set:function(a){return"url(#"+this.paper.defineGradient(a)+")"}},sourceMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),a),{"marker-start":"url(#"+this.paper.defineMarker(a)+")"}}},targetMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),{transform:"rotate(180)"},a),{"marker-end":"url(#"+this.paper.defineMarker(a)+")"}}},vertexMarker:{qualify:e.isPlainObject,set:function(a,b,c,d){return a=e.assign(o(d),a),{"marker-mid":"url(#"+this.paper.defineMarker(a)+")"}}},text:{qualify:function(a,b,c){return!c.textWrap||!e.isPlainObject(c.textWrap)},set:function(c,f,g,h){var i=d(g),j="joint-text",k=i.data(j),l=a.util.pick(h,"lineHeight","annotations","textPath","x","textVerticalAnchor","eol"),m=l.fontSize=h["font-size"]||h.fontSize,n=JSON.stringify([c,l]);if(void 0===k||k!==n){m&&g.setAttribute("font-size",m);var o=l.textPath;if(e.isObject(o)){var p=o.selector;if("string"==typeof p){var q=this.findBySelector(p)[0];q instanceof SVGPathElement&&(l.textPath=e.assign({"xlink:href":"#"+q.id},o))}}b(g).text(""+c,l),i.data(j,n)}}},textWrap:{qualify:e.isPlainObject,set:function(b,c,d,f){var g=b.width||0;e.isPercentage(g)?c.width*=parseFloat(g)/100:g<=0?c.width+=g:c.width=g;var h=b.height||0;e.isPercentage(h)?c.height*=parseFloat(h)/100:h<=0?c.height+=h:c.height=h;var i=b.text;if(void 0===i&&(i=attr.text),void 0!==i)var j=a.util.breakText(""+i,c,{"font-weight":f["font-weight"]||f.fontWeight,"font-size":f["font-size"]||f.fontSize,"font-family":f["font-family"]||f.fontFamily,lineHeight:f.lineHeight},{svgDocument:this.paper.svg});a.dia.attributes.text.set.call(this,j,c,d,f)}},title:{qualify:function(a,b){return b instanceof SVGElement},set:function(a,b,c){var e=d(c),f="joint-title",g=e.data(f);if(void 0===g||g!==a){e.data(f,a);var h=c.firstChild;if(h&&"TITLE"===h.tagName.toUpperCase())h.textContent=a;else{var i=document.createElementNS(c.namespaceURI,"title");i.textContent=a,c.insertBefore(i,h)}}}},lineHeight:{qualify:m},textVerticalAnchor:{qualify:m},textPath:{qualify:m},annotations:{qualify:m},port:{set:function(a){return null===a||void 0===a.id?a:a.id}},style:{qualify:e.isPlainObject,set:function(a,b,c){d(c).css(a)}},html:{set:function(a,b,c){d(c).html(a+"")}},ref:{},refX:{position:g("x","width","origin")},refY:{position:g("y","height","origin")},refDx:{position:g("x","width","corner")},refDy:{position:g("y","height","corner")},refWidth:{set:f("width","width")},refHeight:{set:f("height","height")},refRx:{set:f("rx","width")},refRy:{set:f("ry","height")},refRInscribed:{set:function(a){var b=f(a,"width"),c=f(a,"height");return function(a,d){var e=d.height>d.width?b:c;return e(a,d)}}("r")},refRCircumscribed:{set:function(a,b){var c=e.isPercentage(a);a=parseFloat(a),c&&(a/=100);var d,f=Math.sqrt(b.height*b.height+b.width*b.width);return isFinite(a)&&(d=c||a>=0&&a<=1?a*f:Math.max(a+f,0)),{r:d}}},refCx:{set:f("cx","width")},refCy:{set:f("cy","height")},xAlignment:{offset:h("x","width","right")},yAlignment:{offset:h("y","height","bottom")},resetOffset:{offset:function(a,b){return a?{x:-b.x,y:-b.y}:{x:0,y:0}}},refDResetOffset:{set:j({resetOffset:!0})},refDKeepOffset:{set:j({resetOffset:!1})},refPointsResetOffset:{set:k({resetOffset:!0})},refPointsKeepOffset:{set:k({resetOffset:!1})},connection:{qualify:n,set:function(){return{d:this.getSerializedConnection()}}},atConnectionLengthKeepGradient:{qualify:n,set:l("getTangentAtLength",{rotate:!0})},atConnectionLengthIgnoreGradient:{qualify:n,set:l("getTangentAtLength",{rotate:!1})},atConnectionRatioKeepGradient:{qualify:n,set:l("getTangentAtRatio",{rotate:!0})},atConnectionRatioIgnoreGradient:{qualify:n,set:l("getTangentAtRatio",{rotate:!1})}};p.refR=p.refRInscribed,p.refD=p.refDResetOffset,p.refPoints=p.refPointsResetOffset,p.atConnectionLength=p.atConnectionLengthKeepGradient,p.atConnectionRatio=p.atConnectionRatioKeepGradient,p.refX2=p.refX,p.refY2=p.refY,p["ref-x"]=p.refX,p["ref-y"]=p.refY,p["ref-dy"]=p.refDy,p["ref-dx"]=p.refDx,p["ref-width"]=p.refWidth,p["ref-height"]=p.refHeight,p["x-alignment"]=p.xAlignment,p["y-alignment"]=p.yAlignment}(joint,V,g,$,joint.util),function(a,b){var c=a.mvc.View.extend({name:null,tagName:"g",className:"tool",svgElement:!0,_visible:!0,init:function(){var a=this.name;a&&this.vel.attr("data-tool-name",a)},configure:function(a,b){return this.relatedView=a,this.paper=a.paper,this.parentView=b,this.simulateRelatedView(this.el),this},simulateRelatedView:function(a){a&&a.setAttribute("model-id",this.relatedView.model.id)},getName:function(){return this.name},show:function(){this.el.style.display="",this._visible=!0},hide:function(){this.el.style.display="none",this._visible=!1},isVisible:function(){return!!this._visible},focus:function(){var a=this.options.focusOpacity;isFinite(a)&&(this.el.style.opacity=a),this.parentView.focusTool(this)},blur:function(){this.el.style.opacity="",this.parentView.blurTool(this)},update:function(){}}),d=a.mvc.View.extend({tagName:"g",className:"tools",svgElement:!0,tools:null,options:{tools:null,relatedView:null,name:null,component:!1},configure:function(d){d=b.assign(this.options,d);var e=d.tools;if(!Array.isArray(e))return this;var f=d.relatedView;if(!(f instanceof a.dia.CellView))return this;for(var g=this.tools=[],h=0,i=e.length;h0;){var d=c.shift();b.push(d),c.push.apply(c,d.getEmbeddedCells())}}else b=this.getEmbeddedCells(),b.forEach(function(c){b.push.apply(b,c.getEmbeddedCells(a))});else b=joint.util.toArray(this.get("embeds")).map(this.graph.getCell,this.graph);return b}return[]},isEmbeddedIn:function(a,b){var c=joint.util.isString(a)?a:a.id,d=this.parent();if(b=joint.util.defaults({deep:!0},b),this.graph&&b.deep){for(;d;){if(d===c)return!0;d=this.graph.getCell(d).parent()}return!1}return d===c},isEmbedded:function(){return!!this.parent()},clone:function(a){if(a=a||{},a.deep)return joint.util.toArray(joint.dia.Graph.prototype.cloneCells.call(null,[this].concat(this.getEmbeddedCells({deep:!0}))));var b=Backbone.Model.prototype.clone.apply(this,arguments);return b.set("id",joint.util.uuid()),b.unset("embeds"),b.unset("parent"),b},prop:function(a,b,c){var d="/",e=joint.util.isString(a);if(e||Array.isArray(a)){if(arguments.length>1){var f,g;e?(f=a,g=f.split("/")):(f=a.join(d),g=a.slice());var h=g[0],i=g.length;if(c=c||{},c.propertyPath=f,c.propertyValue=b,c.propertyPathArray=g,1===i)return this.set(h,b,c);for(var j={},k=j,l=h,m=1;m0)},getSelector:function(a,b){if(a===this.el)return b;var c;if(a){var d=V(a).index()+1;c=a.tagName+":nth-child("+d+")",b&&(c+=" > "+b),c=this.getSelector(a.parentNode,c)}return c},getLinkEnd:function(a,b,c,d,e){var f=this.model,h=f.id,i=this.findAttribute("port",a),j=a.getAttribute("joint-selector"),k={id:h};null!=j&&(k.magnet=j),null!=i?(k.port=i,f.hasPort(i)||j||(k.selector=this.getSelector(a))):null==j&&this.el!==a&&(k.selector=this.getSelector(a));var l=this.paper,m=l.options.connectionStrategy;if("function"==typeof m){var n=m.call(l,k,this,a,new g.Point(b,c),d,e);n&&(k=n)}return k},getMagnetFromLinkEnd:function(a){var b,c=this.el,d=a.port,e=a.magnet;return b=null!=d&&this.model.hasPort(d)?this.findPortNode(d,e)||c:this.findBySelector(e||a.selector,c,this.selectors)[0]},findAttribute:function(a,b){if(!b)return null;var c=b.getAttribute(a);if(null===c){if(b===this.el)return null;for(var d=b.parentNode;d&&d!==this.el&&1===d.nodeType&&(c=d.getAttribute(a),null===c);)d=d.parentNode}return c},getAttributeDefinition:function(a){return this.model.constructor.getAttributeDefinition(a)},setNodeAttributes:function(a,b){joint.util.isEmpty(b)||(a instanceof SVGElement?V(a).attr(b):$(a).attr(b))},processNodeAttributes:function(a,b){var c,d,e,f,g,h,i,j,k,l=[];for(c in b)b.hasOwnProperty(c)&&(d=b[c],e=this.getAttributeDefinition(c),!e||joint.util.isFunction(e.qualify)&&!e.qualify.call(this,d,a,b)?(h||(h={}),h[joint.util.toKebabCase(c)]=d):(joint.util.isString(e.set)&&(h||(h={}),h[e.set]=d),null!==d&&l.push(c,e)));for(f=0,g=l.length;f0&&x.height>0){var y=V.transformRect(a.getBBox(),p).scale(1/r,1/s);for(e in m)f=m[e],h=this.getAttributeDefinition(e),t=h.offset.call(this,f,y,a,i),t&&(q.offset(g.Point(t).scale(r,s)),w||(w=!0))}}(void 0!==o||v||w)&&(q.round(1),p.e=q.x,p.f=q.y,a.setAttribute("transform",V.matrixToTransformString(p)))},getNodeScale:function(a,b){var c,d;if(b&&b.contains(a)){var e=b.scale();c=1/e.sx,d=1/e.sy}else c=1,d=1;return{sx:c,sy:d}},findNodesAttributes:function(a,b,c,d){var e={};for(var f in a)if(a.hasOwnProperty(f))for(var g=c[f]=this.findBySelector(f,b,d),h=0,i=g.length;h-1?l.splice(t,0,d):l.push(d)}else this.setNodeAttributes(e,i.normal);for(var u=0,v=l.length;u0){this.startBatch("fit-embeds",a),a.deep&&joint.util.invoke(b,"fitEmbeds",a);var c=this.graph.getCellsBBox(b),d=joint.util.normalizeSides(a.padding);c.moveAndExpand({x:-d.left,y:-d.top,width:d.right+d.left,height:d.bottom+d.top}),this.set({position:{x:c.x,y:c.y},size:{width:c.width,height:c.height}},a),this.stopBatch("fit-embeds")}return this},rotate:function(a,b,c,d){if(c){var e=this.getBBox().center(),f=this.get("size"),g=this.get("position");e.rotate(c,this.get("angle")-a);var h=e.x-f.width/2-g.x,i=e.y-f.height/2-g.y;this.startBatch("rotate",{angle:a,absolute:b,origin:c}),this.position(g.x+h,g.y+i,d),this.rotate(a,b,null,d),this.stopBatch("rotate")}else this.set("angle",b?a:(this.get("angle")+a)%360,d);return this},angle:function(){return g.normalizeAngle(this.get("angle")||0)},getBBox:function(a){if(a=a||{},a.deep&&this.graph){var b=this.getEmbeddedCells({deep:!0,breadthFirst:!0});return b.push(this),this.graph.getCellsBBox(b)}var c=this.get("position"),d=this.get("size");return new g.Rect(c.x,c.y,d.width,d.height)}}),joint.dia.ElementView=joint.dia.CellView.extend({_removePorts:function(){},_renderPorts:function(){},className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("element"),a.join(" ")},metrics:null,initialize:function(){joint.dia.CellView.prototype.initialize.apply(this,arguments);var a=this.model;this.listenTo(a,"change:position",this.translate),this.listenTo(a,"change:size",this.resize),this.listenTo(a,"change:angle",this.rotate),this.listenTo(a,"change:markup",this.render),this._initializePorts(),this.metrics={}},_initializePorts:function(){},update:function(a,b){this.metrics={},this._removePorts();var c=this.model,d=c.attr();this.updateDOMSubtreeAttributes(this.el,d,{rootBBox:new g.Rect(c.size()),selectors:this.selectors,scalableNode:this.scalableNode,rotatableNode:this.rotatableNode,roAttributes:b===d?null:b}),this._renderPorts()},rotatableSelector:"rotatable",scalableSelector:"scalable",scalableNode:null,rotatableNode:null,renderMarkup:function(){var a=this.model,b=a.get("markup")||a.markup;if(!b)throw new Error("dia.ElementView: markup required");if(Array.isArray(b))return this.renderJSONMarkup(b);if("string"==typeof b)return this.renderStringMarkup(b);throw new Error("dia.ElementView: invalid markup")},renderJSONMarkup:function(a){var b=joint.util.parseDOMJSON(a),c=this.selectors=b.selectors,d=this.selector;if(c[d])throw new Error("dia.ElementView: ambiguous root selector.");c[d]=this.el,this.rotatableNode=V(c[this.rotatableSelector])||null,this.scalableNode=V(c[this.scalableSelector])||null,this.vel.append(b.fragment)},renderStringMarkup:function(a){var b=this.vel;b.append(V(a)),this.rotatableNode=b.findOne(".rotatable"),this.scalableNode=b.findOne(".scalable");var c=this.selectors={};c[this.selector]=this.el},render:function(){return this.vel.empty(),this.renderMarkup(),this.scalableNode&&this.update(),this.resize(),this.rotatableNode?(this.rotate(),this.translate(),this):(this.updateTransformation(),this)},resize:function(){return this.scalableNode?this.sgResize.apply(this,arguments):(this.model.attributes.angle&&this.rotate(),void this.update())},translate:function(){return this.rotatableNode?this.rgTranslate():void this.updateTransformation()},rotate:function(){return this.rotatableNode?this.rgRotate():void this.updateTransformation()},updateTransformation:function(){var a=this.getTranslateString(),b=this.getRotateString();b&&(a+=" "+b),this.vel.attr("transform",a)},getTranslateString:function(){var a=this.model.attributes.position;return"translate("+a.x+","+a.y+")"},getRotateString:function(){var a=this.model.attributes,b=a.angle;if(!b)return null;var c=a.size;return"rotate("+b+","+c.width/2+","+c.height/2+")"},getBBox:function(a){var b;if(a&&a.useModelGeometry){var c=this.model;b=c.getBBox().bbox(c.angle())}else b=this.getNodeBBox(this.el);return this.paper.localToPaperRect(b)},nodeCache:function(a){var b=V.ensureId(a),c=this.metrics[b];return c||(c=this.metrics[b]={}),c},getNodeData:function(a){var b=this.nodeCache(a);return b.data||(b.data={}),b.data},getNodeBBox:function(a){var b=this.getNodeBoundingRect(a),c=this.getNodeMatrix(a),d=this.getRootTranslateMatrix(),e=this.getRootRotateMatrix();return V.transformRect(b,d.multiply(e).multiply(c))},getNodeBoundingRect:function(a){var b=this.nodeCache(a);return void 0===b.boundingRect&&(b.boundingRect=V(a).getBBox()),new g.Rect(b.boundingRect)},getNodeUnrotatedBBox:function(a){var b=this.getNodeBoundingRect(a),c=this.getNodeMatrix(a),d=this.getRootTranslateMatrix();return V.transformRect(b,d.multiply(c))},getNodeShape:function(a){var b=this.nodeCache(a);return void 0===b.geometryShape&&(b.geometryShape=V(a).toGeometryShape()),b.geometryShape.clone()},getNodeMatrix:function(a){var b=this.nodeCache(a);if(void 0===b.magnetMatrix){var c=this.rotatableNode||this.el;b.magnetMatrix=V(a).getTransformToElement(c)}return V.createSVGMatrix(b.magnetMatrix)},getRootTranslateMatrix:function(){var a=this.model,b=a.position(),c=V.createSVGMatrix().translate(b.x,b.y);return c},getRootRotateMatrix:function(){var a=V.createSVGMatrix(),b=this.model,c=b.angle();if(c){var d=b.getBBox(),e=d.width/2,f=d.height/2;a=a.translate(e,f).rotate(c).translate(-e,-f)}return a},rgRotate:function(){this.rotatableNode.attr("transform",this.getRotateString())},rgTranslate:function(){this.vel.attr("transform",this.getTranslateString())},sgResize:function(a,b,c){var d=this.model,e=d.get("angle")||0,f=d.get("size")||{width:1,height:1},g=this.scalableNode,h=!1;g.node.getElementsByTagName("path").length>0&&(h=!0);var i=g.getBBox({recursive:h}),j=f.width/(i.width||1),k=f.height/(i.height||1);g.attr("transform","scale("+j+","+k+")");var l=this.rotatableNode,m=l&&l.attr("transform");if(m&&null!==m){l.attr("transform",m+" rotate("+-e+","+f.width/2+","+f.height/2+")");var n=g.getBBox({target:this.paper.viewport});d.set("position",{x:n.x,y:n.y},c),this.rotate()}this.update()},prepareEmbedding:function(a){a||(a={});var b=a.model||this.model,c=a.paper||this.paper,d=c.model;b.startBatch("to-front"),b.toFront({deep:!0,ui:!0});var e=d.get("cells").max("z").get("z"),f=d.getConnectedLinks(b,{deep:!0});joint.util.invoke(f,"set","z",e+1,{ui:!0}),b.stopBatch("to-front");var g=b.parent();g&&d.getCell(g).unembed(b,{ui:!0})},processEmbedding:function(a){a||(a={});var b=a.model||this.model,c=a.paper||this.paper,d=c.options,e=[];if(joint.util.isFunction(d.findParentBy)){var f=joint.util.toArray(d.findParentBy.call(c.model,this));e=f.filter(function(a){return a instanceof joint.dia.Cell&&this.model.id!==a.id&&!a.isEmbeddedIn(this.model)}.bind(this))}else e=c.model.findModelsUnderElement(b,{searchBy:d.findParentBy});d.frontParentOnly&&(e=e.slice(-1));for(var g=null,h=a.candidateEmbedView,i=e.length-1;i>=0;i--){var j=e[i];if(h&&h.model.id==j.id){g=h;break}var k=j.findView(c);if(d.validateEmbedding.call(c,this,k)){g=k;break}}g&&g!=h&&(this.clearEmbedding(a),a.candidateEmbedView=g.highlight(null,{embedding:!0})),!g&&h&&this.clearEmbedding(a)},clearEmbedding:function(a){a||(a={});var b=a.candidateEmbedView;b&&(b.unhighlight(null,{embedding:!0}),a.candidateEmbedView=null)},finalizeEmbedding:function(a){a||(a={});var b=a.candidateEmbedView,c=a.model||this.model,d=a.paper||this.paper;b&&(b.model.embed(c,{ui:!0}),b.unhighlight(null,{embedding:!0}),a.candidateEmbedView=null),joint.util.invoke(d.model.getConnectedLinks(c,{deep:!0}),"reparent",{ui:!0})},pointerdblclick:function(a,b,c){joint.dia.CellView.prototype.pointerdblclick.apply(this,arguments),this.notify("element:pointerdblclick",a,b,c)},pointerclick:function(a,b,c){joint.dia.CellView.prototype.pointerclick.apply(this,arguments),this.notify("element:pointerclick",a,b,c)},contextmenu:function(a,b,c){joint.dia.CellView.prototype.contextmenu.apply(this,arguments),this.notify("element:contextmenu",a,b,c)},pointerdown:function(a,b,c){joint.dia.CellView.prototype.pointerdown.apply(this,arguments),this.notify("element:pointerdown",a,b,c),this.dragStart(a,b,c)},pointermove:function(a,b,c){var d=this.eventData(a);switch(d.action){case"move":this.drag(a,b,c);break;case"magnet":this.dragMagnet(a,b,c)}d.stopPropagation||(joint.dia.CellView.prototype.pointermove.apply(this,arguments),this.notify("element:pointermove",a,b,c)),this.eventData(a,d)},pointerup:function(a,b,c){var d=this.eventData(a);switch(d.action){case"move":this.dragEnd(a,b,c);break;case"magnet":return void this.dragMagnetEnd(a,b,c)}d.stopPropagation||(this.notify("element:pointerup",a,b,c),joint.dia.CellView.prototype.pointerup.apply(this,arguments))},mouseover:function(a){joint.dia.CellView.prototype.mouseover.apply(this,arguments),this.notify("element:mouseover",a)},mouseout:function(a){joint.dia.CellView.prototype.mouseout.apply(this,arguments),this.notify("element:mouseout",a)},mouseenter:function(a){joint.dia.CellView.prototype.mouseenter.apply(this,arguments),this.notify("element:mouseenter",a)},mouseleave:function(a){joint.dia.CellView.prototype.mouseleave.apply(this,arguments),this.notify("element:mouseleave",a)},mousewheel:function(a,b,c,d){joint.dia.CellView.prototype.mousewheel.apply(this,arguments),this.notify("element:mousewheel",a,b,c,d)},onmagnet:function(a,b,c){this.dragMagnetStart(a,b,c);var d=this.eventData(a).stopPropagation;d&&a.stopPropagation()},dragStart:function(a,b,c){this.can("elementMove")&&this.eventData(a,{action:"move",x:b,y:c,restrictedArea:this.paper.getRestrictedArea(this)})},dragMagnetStart:function(a,b,c){if(this.can("addLinkFromMagnet")){this.model.startBatch("add-link");var d=this.paper,e=d.model,f=a.target,g=d.getDefaultLink(this,f),h=this.getLinkEnd(f,b,c,g,"source"),i={x:b,y:c};g.set({source:h,target:i}),g.addTo(e,{async:!1,ui:!0});var j=g.findView(d);joint.dia.CellView.prototype.pointerdown.apply(j,arguments),j.notify("link:pointerdown",a,b,c);var k=j.startArrowheadMove("target",{whenNotAllowed:"remove"});j.eventData(a,k),this.eventData(a,{action:"magnet",linkView:j,stopPropagation:!0}),this.paper.delegateDragEvents(this,a.data)}},drag:function(a,b,c){var d=this.paper,e=d.options.gridSize,f=this.model,h=f.position(),i=this.eventData(a),j=g.snapToGrid(h.x,e)-h.x+g.snapToGrid(b-i.x,e),k=g.snapToGrid(h.y,e)-h.y+g.snapToGrid(c-i.y,e);f.translate(j,k,{restrictedArea:i.restrictedArea,ui:!0});var l=!!i.embedding;d.options.embeddingMode&&(l||(this.prepareEmbedding(i),l=!0),this.processEmbedding(i)),this.eventData(a,{x:g.snapToGrid(b,e),y:g.snapToGrid(c,e),embedding:l})},dragMagnet:function(a,b,c){var d=this.eventData(a),e=d.linkView;e&&e.pointermove(a,b,c)},dragEnd:function(a,b,c){var d=this.eventData(a);d.embedding&&this.finalizeEmbedding(d)},dragMagnetEnd:function(a,b,c){var d=this.eventData(a),e=d.linkView;e&&e.pointerup(a,b,c),this.model.stopBatch("add-link")}}),joint.dia.Link=joint.dia.Cell.extend({markup:['','','','','','','',''].join(""),toolMarkup:['','','','',"Remove link.","",'','','',"Link options.","",""].join(""),doubleToolMarkup:void 0,vertexMarkup:['','','','',"Remove vertex.","",""].join(""),arrowheadMarkup:['','',""].join(""),defaultLabel:void 0,labelMarkup:void 0,_builtins:{defaultLabel:{markup:[{tagName:"rect",selector:"rect"},{tagName:"text",selector:"text"}],attrs:{text:{fill:"#000000",fontSize:14,textAnchor:"middle",yAlignment:"middle",pointerEvents:"none"},rect:{ref:"text",fill:"#ffffff",rx:3,ry:3,refWidth:1,refHeight:1,refX:0,refY:0}},position:{distance:.5}}},defaults:{type:"link",source:{},target:{}},isLink:function(){return!0},disconnect:function(a){return this.set({source:{x:0,y:0},target:{x:0,y:0}},a)},source:function(a,b,c){if(void 0===a)return joint.util.clone(this.get("source"));var d,e,f=a instanceof joint.dia.Cell;if(f)return d=joint.util.clone(b)||{},d.id=a.id,e=c,this.set("source",d,e);var h=a instanceof g.Point;return h?(d=joint.util.clone(b)||{},d.x=a.x,d.y=a.y,e=c,this.set("source",d,e)):(d=a,e=b,this.set("source",d,e))},target:function(a,b,c){if(void 0===a)return joint.util.clone(this.get("target"));var d,e,f=a instanceof joint.dia.Cell;if(f)return d=joint.util.clone(b)||{},d.id=a.id,e=c,this.set("target",d,e);var h=a instanceof g.Point;return h?(d=joint.util.clone(b)||{},d.x=a.x,d.y=a.y,e=c,this.set("target",d,e)):(d=a,e=b,this.set("target",d,e))},router:function(a,b,c){if(void 0===a)return router=this.get("router"),router?"object"==typeof router?joint.util.clone(router):router:this.get("manhattan")?{name:"orthogonal"}:null;var d="object"==typeof a||"function"==typeof a,e=d?a:{name:a,args:b},f=d?b:c;return this.set("router",e,f)},connector:function(a,b,c){if(void 0===a)return connector=this.get("connector"),connector?"object"==typeof connector?joint.util.clone(connector):connector:this.get("smooth")?{name:"smooth"}:null;var d="object"==typeof a||"function"==typeof a,e=d?a:{name:a,args:b},f=d?b:c;return this.set("connector",e,f)},label:function(a,b,c){var d=this.labels();return a=isFinite(a)&&null!==a?0|a:0,a<0&&(a=d.length+a),arguments.length<=1?this.prop(["labels",a]):this.prop(["labels",a],b,c)},labels:function(a,b){return 0===arguments.length?(a=this.get("labels"),Array.isArray(a)?a.slice():[]):(Array.isArray(a)||(a=[]),this.set("labels",a,b))},insertLabel:function(a,b,c){if(!b)throw new Error("dia.Link: no label provided");var d=this.labels(),e=d.length;return a=isFinite(a)&&null!==a?0|a:e,a<0&&(a=e+a+1),d.splice(a,0,b),this.labels(d,c)},appendLabel:function(a,b){return this.insertLabel(-1,a,b)},removeLabel:function(a,b){var c=this.labels();return a=isFinite(a)&&null!==a?0|a:-1,c.splice(a,1),this.labels(c,b)},vertex:function(a,b,c){var d=this.vertices();return a=isFinite(a)&&null!==a?0|a:0,a<0&&(a=d.length+a),arguments.length<=1?this.prop(["vertices",a]):this.prop(["vertices",a],b,c)},vertices:function(a,b){return 0===arguments.length?(a=this.get("vertices"),Array.isArray(a)?a.slice():[]):(Array.isArray(a)||(a=[]),this.set("vertices",a,b))},insertVertex:function(a,b,c){if(!b)throw new Error("dia.Link: no vertex provided");var d=this.vertices(),e=d.length;return a=isFinite(a)&&null!==a?0|a:e,a<0&&(a=e+a+1),d.splice(a,0,b),this.vertices(d,c)},removeVertex:function(a,b){var c=this.vertices();return a=isFinite(a)&&null!==a?0|a:-1,c.splice(a,1),this.vertices(c,b)},translate:function(a,b,c){return c=c||{},c.translateBy=c.translateBy||this.id,c.tx=a,c.ty=b,this.applyToPoints(function(c){return{x:(c.x||0)+a,y:(c.y||0)+b}},c)},scale:function(a,b,c,d){return this.applyToPoints(function(d){return g.point(d).scale(a,b,c).toJSON()},d)},applyToPoints:function(a,b){if(!joint.util.isFunction(a))throw new TypeError("dia.Link: applyToPoints expects its first parameter to be a function.");var c={},d=this.source();d.id||(c.source=a(d));var e=this.target();e.id||(c.target=a(e));var f=this.vertices();return f.length>0&&(c.vertices=f.map(a)),this.set(c,b)},reparent:function(a){var b;if(this.graph){var c=this.getSourceElement(),d=this.getTargetElement(),e=this.getParentCell();c&&d&&(b=this.graph.getCommonAncestor(c,d)),!e||b&&b.id===e.id||e.unembed(this,a),b&&b.embed(this,a)}return b},hasLoop:function(a){a=a||{};var b=this.source().id,c=this.target().id;if(!b||!c)return!1;var d=b===c;if(!d&&a.deep&&this.graph){var e=this.getSourceElement(),f=this.getTargetElement();d=e.isEmbeddedIn(f)||f.isEmbeddedIn(e)}return d},getSourceElement:function(){var a=this.source(),b=this.graph;return a&&a.id&&b&&b.getCell(a.id)||null},getTargetElement:function(){var a=this.target(),b=this.graph;return a&&a.id&&b&&b.getCell(a.id)||null},getRelationshipAncestor:function(){var a;if(this.graph){var b=[this,this.getSourceElement(),this.getTargetElement()].filter(function(a){return!!a});a=this.graph.getCommonAncestor.apply(this.graph,b)}return a||null},isRelationshipEmbeddedIn:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a.id,c=this.getRelationshipAncestor();return!!c&&(c.id===b||c.isEmbeddedIn(b))},_getDefaultLabel:function(){var a=this.get("defaultLabel")||this.defaultLabel||{},b={};return b.markup=a.markup||this.get("labelMarkup")||this.labelMarkup,b.position=a.position,b.attrs=a.attrs,b.size=a.size,b}},{endsEqual:function(a,b){var c=a.port===b.port||!a.port&&!b.port;return a.id===b.id&&c}}),joint.dia.LinkView=joint.dia.CellView.extend({className:function(){var a=joint.dia.CellView.prototype.className.apply(this).split(" ");return a.push("link"),a.join(" ")},options:{shortLinkLength:105,doubleLinkTools:!1,longLinkLength:155,linkToolsOffset:40,doubleLinkToolsOffset:65,sampleInterval:50},_labelCache:null,_labelSelectors:null,_markerCache:null,_V:null,_dragData:null,metrics:null,decimalsRounding:2,initialize:function(a){joint.dia.CellView.prototype.initialize.apply(this,arguments),"function"!=typeof this.constructor.prototype.watchSource&&(this.constructor.prototype.watchSource=this.createWatcher("source"),this.constructor.prototype.watchTarget=this.createWatcher("target")),this._labelCache={},this._labelSelectors={},this._markerCache={},this._V={},this.metrics={},this.startListening()},startListening:function(){var a=this.model;this.listenTo(a,"change:markup",this.render),this.listenTo(a,"change:smooth change:manhattan change:router change:connector",this.update),this.listenTo(a,"change:toolMarkup",this.onToolsChange),this.listenTo(a,"change:labels change:labelMarkup",this.onLabelsChange),this.listenTo(a,"change:vertices change:vertexMarkup",this.onVerticesChange),this.listenTo(a,"change:source",this.onSourceChange),this.listenTo(a,"change:target",this.onTargetChange)},onSourceChange:function(a,b,c){this.watchSource(a,b);var d=this.model;c.translateBy&&d.get("target").id&&b.id||this.update(d,null,c)},onTargetChange:function(a,b,c){this.watchTarget(a,b);var d=this.model;(!c.translateBy||d.get("source").id&&!b.id&&joint.util.isEmpty(d.get("vertices")))&&this.update(d,null,c)},onVerticesChange:function(a,b,c){this.renderVertexMarkers(),c.translateBy&&c.translateBy!==this.model.id||this.update(a,null,c)},onToolsChange:function(){this.renderTools().updateToolsPosition()},onLabelsChange:function(a,b,c){var d=!0,e=this.model.previous("labels");if(e&&"propertyPathArray"in c&&"propertyValue"in c){var f=c.propertyPathArray||[],g=f.length;if(g>1){var h=!!e[f[1]];h&&(2===g?d="markup"in Object(c.propertyValue):"markup"!==f[2]&&(d=!1))}}d?this.renderLabels():this.updateLabels(),this.updateLabelPositions()},render:function(){this.vel.empty(),this._V={},this.renderMarkup(),this.renderLabels();var a=this.model;return this.watchSource(a,a.source()).watchTarget(a,a.target()).update(),this},renderMarkup:function(){var a=this.model,b=a.get("markup")||a.markup;if(!b)throw new Error("dia.LinkView: markup required");if(Array.isArray(b))return this.renderJSONMarkup(b);if("string"==typeof b)return this.renderStringMarkup(b);throw new Error("dia.LinkView: invalid markup")},renderJSONMarkup:function(a){var b=joint.util.parseDOMJSON(a),c=this.selectors=b.selectors,d=this.selector;if(c[d])throw new Error("dia.LinkView: ambiguous root selector.");c[d]=this.el,this.vel.append(b.fragment)},renderStringMarkup:function(a){var b=V(a);Array.isArray(b)||(b=[b]);for(var c=this._V,d=0,e=b.length;d1||"G"!==d[0].nodeName.toUpperCase()?(c=V("g"),c.append(b),c.addClass("label")):(c=V(d[0]),c.addClass("label")),{node:c.node,selectors:a.selectors}}},renderLabels:function(){var a=this._V,b=a.labels,c=this._labelCache={},d=this._labelSelectors={};b&&b.empty();var e=this.model,f=e.get("labels")||[],g=f.length;if(0===g)return this;b||(b=a.labels=V("g").addClass("labels").appendTo(this.el));for(var h=0;h=this.options.longLinkLength){var e=this.options.doubleLinkToolsOffset||b;d=this.getPointAtLength(c-e),this._tool2Cache.attr("transform","translate("+d.x+", "+d.y+") "+a),this._tool2Cache.attr("visibility","visible")}else this.options.doubleLinkTools&&this._tool2Cache.attr("visibility","hidden")}return this},updateArrowheadMarkers:function(){if(!this._V.markerArrowheads)return this;if("none"===$.css(this._V.markerArrowheads.node,"display"))return this;var a=this.getConnectionLength()0&&b<=1,d=0,e={x:0,y:0};if(a.offset){var f=a.offset;"number"==typeof f&&(d=f),f.x&&(e.x=f.x),f.y&&(e.y=f.y)}var g,h=0!==e.x||0!==e.y||0===d,i=this.path,j={segmentSubdivisions:this.getConnectionSubdivisions()},k=c?b*this.getConnectionLength():b;if(h)g=i.pointAtLength(k,j),g.offset(e);else{var l=i.tangentAtLength(k,j);l?(l.rotate(l.start,-90),l.setLength(d),g=l.end):g=i.start}return g},getVertexIndex:function(a,b){for(var c=this.model,d=c.vertices(),e=this.getClosestPointLength(new g.Point(a,b)),f=0,h=d.length;f0){for(var j=0,k=i.length;j").addClass(joint.util.addClassNamePrefix("paper-background")),this.options.background&&this.drawBackground(this.options.background),this.$grid=$("
").addClass(joint.util.addClassNamePrefix("paper-grid")),this.options.drawGrid&&this.drawGrid(),this.$el.append(this.$background,this.$grid,this.svg),this},update:function(){return this.options.drawGrid&&this.drawGrid(),this._background&&this.updateBackgroundImage(this._background),this},_viewportMatrix:null,_viewportTransformString:null,matrix:function(a){var b=this.viewport;if(void 0===a){var c=b.getAttribute("transform");return(this._viewportTransformString||null)===c?a=this._viewportMatrix:(a=b.getCTM(),this._viewportMatrix=a,this._viewportTransformString=c),V.createSVGMatrix(a)}return a=V.createSVGMatrix(a),ctmString=V.matrixToTransformString(a),b.setAttribute("transform",ctmString),this.tools.setAttribute("transform",ctmString),this._viewportMatrix=a,this._viewportTransformString=b.getAttribute("transform"),this},clientMatrix:function(){return V.createSVGMatrix(this.viewport.getScreenCTM())},_sortDelayingBatches:["add","to-front","to-back"],_onSort:function(){this.model.hasActiveBatch(this._sortDelayingBatches)||this.sortViews()},_onBatchStop:function(a){var b=a&&a.batchName;this._sortDelayingBatches.includes(b)&&!this.model.hasActiveBatch(this._sortDelayingBatches)&&this.sortViews()},onRemove:function(){this.removeViews()},setDimensions:function(a,b){a=this.options.width=a||this.options.width,b=this.options.height=b||this.options.height,this.$el.css({width:Math.round(a),height:Math.round(b)}),this.trigger("resize",a,b)},setOrigin:function(a,b){return this.translate(a||0,b||0,{absolute:!0})},fitToContent:function(a,b,c,d){joint.util.isObject(a)?(d=a,a=d.gridWidth||1,b=d.gridHeight||1,c=d.padding||0):(d=d||{},a=a||1,b=b||1,c=c||0),c=joint.util.normalizeSides(c);var e=V(this.viewport).getBBox(),f=this.scale(),g=this.translate();e.x*=f.sx,e.y*=f.sy,e.width*=f.sx,e.height*=f.sy;var h=Math.max(Math.ceil((e.width+e.x)/a),1)*a,i=Math.max(Math.ceil((e.height+e.y)/b),1)*b,j=0,k=0;("negative"==d.allowNewOrigin&&e.x<0||"positive"==d.allowNewOrigin&&e.x>=0||"any"==d.allowNewOrigin)&&(j=Math.ceil(-e.x/a)*a,j+=c.left,h+=j),("negative"==d.allowNewOrigin&&e.y<0||"positive"==d.allowNewOrigin&&e.y>=0||"any"==d.allowNewOrigin)&&(k=Math.ceil(-e.y/b)*b,k+=c.top,i+=k),h+=c.right,i+=c.bottom,h=Math.max(h,d.minWidth||0),i=Math.max(i,d.minHeight||0),h=Math.min(h,d.maxWidth||Number.MAX_VALUE),i=Math.min(i,d.maxHeight||Number.MAX_VALUE);var l=h!=this.options.width||i!=this.options.height,m=j!=g.tx||k!=g.ty;m&&this.translate(j,k),l&&this.setDimensions(h,i)},scaleContentToFit:function(a){var b=this.getContentBBox();if(b.width&&b.height){a=a||{},joint.util.defaults(a,{padding:0,preserveAspectRatio:!0,scaleGrid:null,minScale:0,maxScale:Number.MAX_VALUE});var c,d=a.padding,e=a.minScaleX||a.minScale,f=a.maxScaleX||a.maxScale,h=a.minScaleY||a.minScale,i=a.maxScaleY||a.maxScale;if(a.fittingBBox)c=a.fittingBBox;else{var j=this.translate();c={x:j.tx,y:j.ty,width:this.options.width,height:this.options.height}}c=g.rect(c).moveAndExpand({x:d,y:d,width:-2*d,height:-2*d});var k=this.scale(),l=c.width/b.width*k.sx,m=c.height/b.height*k.sy;if(a.preserveAspectRatio&&(l=m=Math.min(l,m)),a.scaleGrid){var n=a.scaleGrid;l=n*Math.floor(l/n),m=n*Math.floor(m/n)}l=Math.min(f,Math.max(e,l)),m=Math.min(i,Math.max(h,m)),this.scale(l,m);var o=this.getContentBBox(),p=c.x-o.x,q=c.y-o.y;this.translate(p,q)}},getContentArea:function(){return V(this.viewport).getBBox()},getContentBBox:function(){var a=this.viewport.getBoundingClientRect(),b=this.clientMatrix(),c=this.translate();return g.rect({x:a.left-b.e+c.tx,y:a.top-b.f+c.ty,width:a.width,height:a.height})},getArea:function(){return this.paperToLocalRect({x:0,y:0,width:this.options.width,height:this.options.height})},getRestrictedArea:function(){var a;return a=joint.util.isFunction(this.options.restrictTranslate)?this.options.restrictTranslate.apply(this,arguments):this.options.restrictTranslate===!0?this.getArea():this.options.restrictTranslate||null},createViewForModel:function(a){var b,c,d=this.options.cellViewNamespace,e=a.get("type")+"View",f=joint.util.getByPath(d,e,".");a.isLink()?(b=this.options.linkView,c=joint.dia.LinkView):(b=this.options.elementView,c=joint.dia.ElementView);var g=b.prototype instanceof Backbone.View?f||b:b.call(this,a)||f||c;return new g({model:a,interactive:this.options.interactive})},onCellAdded:function(a,b,c){if(this.options.async&&c.async!==!1&&joint.util.isNumber(c.position)){if(this._asyncCells=this._asyncCells||[],this._asyncCells.push(a),0==c.position){if(this._frameId)throw new Error("another asynchronous rendering in progress");this.asyncRenderViews(this._asyncCells,c),delete this._asyncCells}}else this.renderView(a)},removeView:function(a){var b=this._views[a.id];return b&&(b.remove(),delete this._views[a.id]),b},renderView:function(a){var b=this._views[a.id]=this.createViewForModel(a);return V(this.viewport).append(b.el),b.paper=this,b.render(),b},onImageDragStart:function(){return!1},beforeRenderViews:function(a){return a.sort(function(a){return a.isLink()?1:-1}),a},afterRenderViews:function(){this.sortViews()},resetViews:function(a,b){this.removeViews();var c=a.models.slice();if(c=this.beforeRenderViews(c,b)||c,this.cancelRenderViews(),this.options.async)this.asyncRenderViews(c,b);else{for(var d=0,e=c.length;d(e.get("z")||0)?1:-1})},scale:function(a,b,c,d){if(void 0===a)return V.matrixToScale(this.matrix());void 0===b&&(b=a),void 0===c&&(c=0,d=0);var e=this.translate();if(c||d||e.tx||e.ty){var f=e.tx-c*(a-1),g=e.ty-d*(b-1);this.translate(f,g)}var h=this.matrix();return h.a=a||0,h.d=b||0,this.matrix(h),this.trigger("scale",a,b,c,d),this},rotate:function(a,b,c){if(void 0===a)return V.matrixToRotate(this.matrix());if(void 0===b){var d=this.viewport.getBBox();b=d.width/2,c=d.height/2}var e=this.matrix().translate(b,c).rotate(a).translate(-b,-c);return this.matrix(e),this},translate:function(a,b){if(void 0===a)return V.matrixToTranslate(this.matrix());var c=this.matrix();c.e=a||0,c.f=b||0,this.matrix(c);var d=this.translate(),e=this.options.origin;return e.x=d.tx,e.y=d.ty,this.trigger("translate",d.tx,d.ty),this.options.drawGrid&&this.drawGrid(),this},findView:function(a){for(var b=joint.util.isString(a)?this.viewport.querySelector(a):a instanceof $?a[0]:a;b&&b!==this.el&&b!==document;){var c=b.getAttribute("model-id");if(c)return this._views[c];b=b.parentNode}},findViewByModel:function(a){var b=joint.util.isString(a)||joint.util.isNumber(a)?a:a&&a.id;return this._views[b]},findViewsFromPoint:function(a){a=g.point(a);var b=this.model.getElements().map(this.findViewByModel,this);return b.filter(function(b){return b&&b.vel.getBBox({target:this.viewport}).containsPoint(a)},this)},findViewsInArea:function(a,b){b=joint.util.defaults(b||{},{strict:!1}),a=g.rect(a);var c=this.model.getElements().map(this.findViewByModel,this),d=b.strict?"containsRect":"intersect";return c.filter(function(b){return b&&a[d](b.vel.getBBox({target:this.viewport}))},this)},removeTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"remove"),this},hideTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"hide"),this},showTools:function(){return joint.dia.CellView.dispatchToolsEvent(this,"show"),this},getModelById:function(a){return this.model.getCell(a)},snapToGrid:function(a,b){return this.clientToLocalPoint(a,b).snapToGrid(this.options.gridSize)},localToPaperPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix());return g.Point(d)},localToPaperRect:function(a,b,c,d){var e=g.Rect(a,b),f=V.transformRect(e,this.matrix());return g.Rect(f)},paperToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.matrix().inverse());return g.Point(d)},paperToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.matrix().inverse());return g.Rect(f)},localToClientPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix());return g.Point(d)},localToClientRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix());return g.Rect(f)},clientToLocalPoint:function(a,b){var c=g.Point(a,b),d=V.transformPoint(c,this.clientMatrix().inverse());return g.Point(d)},clientToLocalRect:function(a,b,c,d){var e=g.Rect(a,b,c,d),f=V.transformRect(e,this.clientMatrix().inverse());return g.Rect(f)},localToPagePoint:function(a,b){return this.localToPaperPoint(a,b).offset(this.pageOffset())},localToPageRect:function(a,b,c,d){return this.localToPaperRect(a,b,c,d).moveAndExpand(this.pageOffset())},pageToLocalPoint:function(a,b){var c=g.Point(a,b),d=c.difference(this.pageOffset());return this.paperToLocalPoint(d)},pageToLocalRect:function(a,b,c,d){var e=this.pageOffset(),f=g.Rect(a,b,c,d);return f.x-=e.x,f.y-=e.y,this.paperToLocalRect(f)},clientOffset:function(){var a=this.svg.getBoundingClientRect();return g.Point(a.left,a.top)},pageOffset:function(){return this.clientOffset().offset(window.scrollX,window.scrollY)},linkAllowed:function(a){if(!(a instanceof joint.dia.LinkView))throw new Error("Must provide a linkView.");var b=a.model,c=this.options,d=this.model,e=d.constructor.validations;return!(!c.multiLinks&&!e.multiLinks.call(this,d,b))&&(!(!c.linkPinning&&!e.linkPinning.call(this,d,b))&&!("function"==typeof c.allowLink&&!c.allowLink.call(this,a,this)))},getDefaultLink:function(a,b){return joint.util.isFunction(this.options.defaultLink)?this.options.defaultLink.call(this,a,b):this.options.defaultLink.clone()},resolveHighlighter:function(a){a=a||{};var b=a.highlighter,c=this.options;if(void 0===b){var d=["embedding","connecting","magnetAvailability","elementAvailability"].find(function(b){return!!a[b]});b=d&&c.highlighting[d]||c.highlighting.default}if(!b)return!1;joint.util.isString(b)&&(b={name:b});var e=b.name,f=c.highlighterNamespace[e];if(!f)throw new Error('Unknown highlighter ("'+e+'")');if("function"!=typeof f.highlight)throw new Error('Highlighter ("'+e+'") is missing required highlight() method');if("function"!=typeof f.unhighlight)throw new Error('Highlighter ("'+e+'") is missing required unhighlight() method');return{highlighter:f,options:b.options||{},name:e}},onCellHighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){b.id||(b.id=V.uniqueId());var d=c.name+b.id+JSON.stringify(c.options);if(!this._highlights[d]){var e=c.highlighter;e.highlight(a,b,joint.util.assign({},c.options)),this._highlights[d]={cellView:a,magnetEl:b,opt:c.options,highlighter:e}}}},onCellUnhighlight:function(a,b,c){if(c=this.resolveHighlighter(c)){var d=c.name+b.id+JSON.stringify(c.options),e=this._highlights[d];e&&(e.highlighter.unhighlight(e.cellView,e.magnetEl,e.opt),this._highlights[d]=null)}},pointerdblclick:function(a){a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerdblclick(a,c.x,c.y):this.trigger("blank:pointerdblclick",a,c.x,c.y)}},pointerclick:function(a){if(this._mousemoved<=this.options.clickThreshold){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(this.guard(a,b))return;var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.pointerclick(a,c.x,c.y):this.trigger("blank:pointerclick",a,c.x,c.y)}},contextmenu:function(a){ +this.options.preventContextMenu&&a.preventDefault(),a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?b.contextmenu(a,c.x,c.y):this.trigger("blank:contextmenu",a,c.x,c.y)}},pointerdown:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.snapToGrid({x:a.clientX,y:a.clientY});b?(a.preventDefault(),b.pointerdown(a,c.x,c.y)):(this.options.preventDefaultBlankAction&&a.preventDefault(),this.trigger("blank:pointerdown",a,c.x,c.y)),this.delegateDragEvents(b,a.data)}},pointermove:function(a){a.preventDefault();var b=this.eventData(a);b.mousemoved||(b.mousemoved=0);var c=++b.mousemoved;if(!(c<=this.options.moveThreshold)){a=joint.util.normalizeEvent(a);var d=this.snapToGrid({x:a.clientX,y:a.clientY}),e=b.sourceView;e?e.pointermove(a,d.x,d.y):this.trigger("blank:pointermove",a,d.x,d.y),this.eventData(a,b)}},pointerup:function(a){this.undelegateDocumentEvents(),a=joint.util.normalizeEvent(a);var b=this.snapToGrid({x:a.clientX,y:a.clientY}),c=this.eventData(a).sourceView;c?c.pointerup(a,b.x,b.y):this.trigger("blank:pointerup",a,b.x,b.y),this.delegateEvents()},mouseover:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b))if(b)b.mouseover(a);else{if(this.el===a.target)return;this.trigger("blank:mouseover",a)}},mouseout:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b))if(b)b.mouseout(a);else{if(this.el===a.target)return;this.trigger("blank:mouseout",a)}},mouseenter:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.findView(a.relatedTarget);if(b){if(c===b)return;b.mouseenter(a)}else{if(c)return;this.trigger("paper:mouseenter",a)}}},mouseleave:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=this.findView(a.relatedTarget);if(b){if(c===b)return;b.mouseleave(a)}else{if(c)return;this.trigger("paper:mouseleave",a)}}},mousewheel:function(a){a=joint.util.normalizeEvent(a);var b=this.findView(a.target);if(!this.guard(a,b)){var c=a.originalEvent,d=this.snapToGrid({x:c.clientX,y:c.clientY}),e=Math.max(-1,Math.min(1,c.wheelDelta||-c.detail));b?b.mousewheel(a,d.x,d.y,e):this.trigger("blank:mousewheel",a,d.x,d.y,e)}},onevent:function(a){var b=a.currentTarget,c=b.getAttribute("event");if(c){var d=this.findView(b);if(d){if(a=joint.util.normalizeEvent(a),this.guard(a,d))return;var e=this.snapToGrid({x:a.clientX,y:a.clientY});d.onevent(a,c,e.x,e.y)}}},onmagnet:function(a){var b=a.currentTarget,c=b.getAttribute("magnet");if(c){var d=this.findView(b);if(d){if(a=joint.util.normalizeEvent(a),this.guard(a,d))return;if(!this.options.validateMagnet(d,b))return;var e=this.snapToGrid(a.clientX,a.clientY);d.onmagnet(a,e.x,e.y)}}},onlabel:function(a){var b=a.currentTarget,c=this.findView(b);if(c){if(a=joint.util.normalizeEvent(a),this.guard(a,c))return;var d=this.snapToGrid(a.clientX,a.clientY);c.onlabel(a,d.x,d.y)}},delegateDragEvents:function(a,b){b||(b={}),this.eventData({data:b},{sourceView:a||null,mousemoved:0}),this.delegateDocumentEvents(null,b),this.undelegateEvents()},guard:function(a,b){return!(!this.options.guard||!this.options.guard(a,b))||(a.data&&void 0!==a.data.guarded?a.data.guarded:!(b&&b.model&&b.model instanceof joint.dia.Cell)&&(this.svg!==a.target&&this.el!==a.target&&!$.contains(this.svg,a.target)))},setGridSize:function(a){return this.options.gridSize=a,this.options.drawGrid&&this.drawGrid(),this},clearGrid:function(){return this.$grid&&this.$grid.css("backgroundImage","none"),this},_getGriRefs:function(){return this._gridCache||(this._gridCache={root:V("svg",{width:"100%",height:"100%"},V("defs")),patterns:{},add:function(a,b){V(this.root.node.childNodes[0]).append(b),this.patterns[a]=b,this.root.append(V("rect",{width:"100%",height:"100%",fill:"url(#"+a+")"}))},get:function(a){return this.patterns[a]},exist:function(a){return void 0!==this.patterns[a]}}),this._gridCache},setGrid:function(a){this.clearGrid(),this._gridCache=null,this._gridSettings=[];var b=Array.isArray(a)?a:[a||{}];return b.forEach(function(a){this._gridSettings.push.apply(this._gridSettings,this._resolveDrawGridOption(a))},this),this},_resolveDrawGridOption:function(a){var b=this.constructor.gridPatterns;if(joint.util.isString(a)&&Array.isArray(b[a]))return b[a].map(function(a){return joint.util.assign({},a)});var c=a||{args:[{}]},d=Array.isArray(c),e=c.name;if(d||e||c.markup||(e="dot"),e&&Array.isArray(b[e])){var f=b[e].map(function(a){return joint.util.assign({},a)}),g=Array.isArray(c.args)?c.args:[c.args||{}];joint.util.defaults(g[0],joint.util.omit(a,"args"));for(var h=0;h'),f=joint.util.toArray(d).map(function(a){return e({offset:a.offset,color:a.color,opacity:Number.isFinite(a.opacity)?a.opacity:1})}),g=["<"+c+">",f.join(""),""].join(""),h=joint.util.assign({id:b},a.attrs);V(g,h).appendTo(this.defs)}return b},defineMarker:function(a){if(!joint.util.isObject(a))throw new TypeError("dia.Paper: defineMarker() requires 1. argument to be an object.");var b=a.id;if(b||(b=this.svg.id+joint.util.hashCode(JSON.stringify(a))),!this.isDefined(b)){var c=joint.util.omit(a,"type","userSpaceOnUse"),d=V("marker",{id:b,orient:"auto",overflow:"visible",markerUnits:a.markerUnits||"userSpaceOnUse"},[V(a.type||"path",c)]);d.appendTo(this.defs)}return b}},{backgroundPatterns:{flipXy:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,-1,b.width,b.height),e.drawImage(a,0,0,c,d),e.setTransform(-1,0,0,1,b.width,0),e.drawImage(a,0,0,c,d),e.setTransform(1,0,0,-1,0,b.height),e.drawImage(a,0,0,c,d),b},flipX:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=2*c,b.height=d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(2*c,0),e.scale(-1,1),e.drawImage(a,0,0,c,d),b},flipY:function(a){var b=document.createElement("canvas"),c=a.width,d=a.height;b.width=c,b.height=2*d;var e=b.getContext("2d");return e.drawImage(a,0,0,c,d),e.translate(0,2*d),e.scale(1,-1),e.drawImage(a,0,0,c,d),b},watermark:function(a,b){b=b||{};var c=a.width,d=a.height,e=document.createElement("canvas");e.width=3*c,e.height=3*d;for(var f=e.getContext("2d"),h=joint.util.isNumber(b.watermarkAngle)?-b.watermarkAngle:-20,i=g.toRad(h),j=e.width/4,k=e.height/4,l=0;l<4;l++)for(var m=0;m<4;m++)(l+m)%2>0&&(f.setTransform(1,0,0,1,(2*l-1)*j,(2*m-1)*k),f.rotate(i),f.drawImage(a,-c/2,-d/2,c,d));return e}},gridPatterns:{dot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){V(a).attr({width:b.thickness*b.sx,height:b.thickness*b.sy,fill:b.color})}}],fixedDot:[{color:"#AAAAAA",thickness:1,markup:"rect",update:function(a,b){var c=b.sx<=1?b.thickness*b.sx:b.thickness;V(a).attr({width:c,height:c,fill:b.color})}}],mesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}],doubleMesh:[{color:"#AAAAAA",thickness:1,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}},{color:"#000000",thickness:3,scaleFactor:4,markup:"path",update:function(a,b){var c,d=b.width,e=b.height,f=b.thickness;c=d-f>=0&&e-f>=0?["M",d,0,"H0 M0 0 V0",e].join(" "):"M 0 0 0 0",V(a).attr({d:c,stroke:b.color,"stroke-width":b.thickness})}}]}}),function(a,b,c){var d=function(b){var d=c.cloneDeep(b)||{};this.ports=[],this.groups={},this.portLayoutNamespace=a.layout.Port,this.portLabelLayoutNamespace=a.layout.PortLabel,this._init(d)};d.prototype={getPorts:function(){return this.ports},getGroup:function(a){return this.groups[a]||{}},getPortsByGroup:function(a){return this.ports.filter(function(b){return b.group===a})},getGroupPortsMetrics:function(a,b){var d=this.getGroup(a),e=this.getPortsByGroup(a),f=d.position||{},h=f.name,i=this.portLayoutNamespace;i[h]||(h="left");var j=f.args||{},k=e.map(function(a){return a&&a.position&&a.position.args}),l=i[h](k,b,j),m={ports:e,result:[]};return c.toArray(l).reduce(function(a,c,d){var e=a.ports[d];return a.result.push({portId:e.id,portTransformation:c,labelTransformation:this._getPortLabelLayout(e,g.Point(c),b),portAttrs:e.attrs,portSize:e.size,labelSize:e.label.size}),a}.bind(this),m),m.result},_getPortLabelLayout:function(a,b,c){var d=this.portLabelLayoutNamespace,e=a.label.position.name||"left";return d[e]?d[e](b,c,a.label.position.args):null},_init:function(a){if(c.isObject(a.groups))for(var b=Object.keys(a.groups),d=0,e=b.length;d0},hasPort:function(a){return this.getPortIndex(a)!==-1},getPorts:function(){return c.cloneDeep(this.prop("ports/items"))||[]},getPort:function(a){return c.cloneDeep(c.toArray(this.prop("ports/items")).find(function(b){return b.id&&b.id===a}))},getPortsPositions:function(a){var b=this._portSettingsData.getGroupPortsMetrics(a,g.Rect(this.size()));return b.reduce(function(a,b){var c=b.portTransformation;return a[b.portId]={x:c.x,y:c.y,angle:c.angle},a},{})},getPortIndex:function(a){var b=c.isObject(a)?a.id:a;return this._isValidPortId(b)?c.toArray(this.prop("ports/items")).findIndex(function(a){return a.id===b}):-1},addPort:function(a,b){if(!c.isObject(a)||Array.isArray(a))throw new Error("Element: addPort requires an object.");var d=c.assign([],this.prop("ports/items"));return d.push(a),this.prop("ports/items",d,b),this},portProp:function(a,b,d,e){var f=this.getPortIndex(a);if(f===-1)throw new Error("Element: unable to find port with id "+a);var g=Array.prototype.slice.call(arguments,1);return Array.isArray(b)?g[0]=["ports","items",f].concat(b):c.isString(b)?g[0]=["ports/items/",f,"/",b].join(""):(g=["ports/items/"+f],c.isPlainObject(b)&&(g.push(b),g.push(d))),this.prop.apply(this,g)},_validatePorts:function(){var b=this.get("ports")||{},d=[];b=b||{};var e=c.toArray(b.items);return e.forEach(function(a){"object"!=typeof a&&d.push("Element: invalid port ",a),this._isValidPortId(a.id)||(a.id=c.uuid())},this),a.util.uniq(e,"id").length!==e.length&&d.push("Element: found id duplicities in ports."),d},_isValidPortId:function(a){return null!==a&&void 0!==a&&!c.isObject(a)},addPorts:function(a,b){return a.length&&this.prop("ports/items",c.assign([],this.prop("ports/items")).concat(a),b),this},removePort:function(a,b){var d=b||{},e=c.assign([],this.prop("ports/items")),f=this.getPortIndex(a);return f!==-1&&(e.splice(f,1),d.rewrite=!0,this.prop("ports/items",e,d)),this},_createPortData:function(){var a=this._validatePorts();if(a.length>0)throw this.set("ports",this.previous("ports")),new Error(a.join(" "));var b;this._portSettingsData&&(b=this._portSettingsData.getPorts()),this._portSettingsData=new d(this.get("ports"));var c=this._portSettingsData.getPorts();if(b){var e=c.filter(function(a){if(!b.find(function(b){return b.id===a.id}))return a}),f=b.filter(function(a){if(!c.find(function(b){return b.id===a.id}))return a});f.length>0&&this.trigger("ports:remove",this,f),e.length>0&&this.trigger("ports:add",this,e)}}}),c.assign(a.dia.ElementView.prototype,{portContainerMarkup:"g",portMarkup:[{tagName:"circle",selector:"circle",attributes:{r:10,fill:"#FFFFFF",stroke:"#000000"}}],portLabelMarkup:[{tagName:"text",selector:"text",attributes:{fill:"#000000"}}],_portElementsCache:null,_initializePorts:function(){this._portElementsCache={},this.listenTo(this.model,"change:ports",function(){this._refreshPorts()})},_refreshPorts:function(){this._removePorts(),this._portElementsCache={},this._renderPorts()},_renderPorts:function(){for(var a=[],b=this._getContainerElement(),d=0,e=b.node.childNodes.length;d1?V("g").append(h):V(h.firstChild),e=g.selectors}else b=V(f),Array.isArray(b)&&(b=V("g").append(b));if(!b)throw new Error("ElementView: Invalid port markup.");b.attr({port:a.id,"port-group":a.group});var i,j=this._getPortLabelMarkup(a.label);if(Array.isArray(j)){var k=c.parseDOMJSON(j),l=k.fragment;d=l.childNodes.length>1?V("g").append(l):V(l.firstChild),i=k.selectors}else d=V(j),Array.isArray(d)&&(d=V("g").append(d));if(!d)throw new Error("ElementView: Invalid port label markup.");var m;if(e&&i){for(var n in i)if(e[n])throw new Error("ElementView: selectors within port must be unique.");m=c.assign({},e,i)}else m=e||i;var o=V(this.portContainerMarkup).addClass("joint-port").append([b.addClass("joint-port-body"),d.addClass("joint-port-label")]);return this._portElementsCache[a.id]={portElement:o,portLabelElement:d,portSelectors:m,portLabelSelectors:i,portContentElement:b,portContentSelectors:e},o},_updatePortGroup:function(a){for(var b=g.Rect(this.model.size()),c=this.model._portSettingsData.getGroupPortsMetrics(a,b),d=0,e=c.length;d'}),joint.shapes.basic.TextView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"change:attrs",this.resize)}}),joint.shapes.basic.Generic.define("basic.Text",{attrs:{text:{"font-size":18,fill:"#000000"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Circle",{size:{width:60,height:60},attrs:{circle:{fill:"#ffffff",stroke:"#000000",r:30,cx:30,cy:30},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Ellipse",{size:{width:60,height:40},attrs:{ellipse:{fill:"#ffffff",stroke:"#000000",rx:30,ry:20,cx:30,cy:20},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-y":.5,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polygon",{size:{width:60,height:40},attrs:{polygon:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Polyline",{size:{width:60,height:40},attrs:{polyline:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Image",{attrs:{text:{"font-size":14,text:"","text-anchor":"middle","ref-x":.5,"ref-dy":20,"y-alignment":"middle",fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Generic.define("basic.Path",{size:{width:60,height:60},attrs:{path:{fill:"#ffffff",stroke:"#000000"},text:{"font-size":14,text:"","text-anchor":"middle",ref:"path","ref-x":.5,"ref-dy":10,fill:"#000000","font-family":"Arial, helvetica, sans-serif"}}},{markup:''}),joint.shapes.basic.Path.define("basic.Rhombus",{attrs:{path:{d:"M 30 0 L 60 30 30 60 0 30 z"},text:{"ref-y":.5,"ref-dy":null,"y-alignment":"middle"}}}),joint.shapes.basic.PortsModelInterface={initialize:function(){this.updatePortsAttrs(),this.on("change:inPorts change:outPorts",this.updatePortsAttrs,this),this.constructor.__super__.constructor.__super__.initialize.apply(this,arguments)},updatePortsAttrs:function(a){if(this._portSelectors){var b=joint.util.omit(this.get("attrs"),this._portSelectors);this.set("attrs",b,{silent:!0})}this._portSelectors=[];var c={};joint.util.toArray(this.get("inPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".inPorts","in");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),joint.util.toArray(this.get("outPorts")).forEach(function(a,b,d){var e=this.getPortAttrs(a,b,d.length,".outPorts","out");this._portSelectors=this._portSelectors.concat(Object.keys(e)),joint.util.assign(c,e)},this),this.attr(c,{silent:!0}),this.processPorts(),this.trigger("process:ports")},getPortSelector:function(a){var b=".inPorts",c=this.get("inPorts").indexOf(a);if(c<0&&(b=".outPorts",c=this.get("outPorts").indexOf(a),c<0))throw new Error("getPortSelector(): Port doesn't exist.");return b+">g:nth-child("+(c+1)+")>.port-body"}},joint.shapes.basic.PortsViewInterface={initialize:function(){this.listenTo(this.model,"process:ports",this.update),joint.dia.ElementView.prototype.initialize.apply(this,arguments)},update:function(){this.renderPorts(),joint.dia.ElementView.prototype.update.apply(this,arguments)},renderPorts:function(){var a=this.$(".inPorts").empty(),b=this.$(".outPorts").empty(),c=joint.util.template(this.model.portMarkup),d=this.model.ports||[];d.filter(function(a){return"in"===a.type}).forEach(function(b,d){a.append(V(c({id:d,port:b})).node)}),d.filter(function(a){return"out"===a.type}).forEach(function(a,d){b.append(V(c({id:d,port:a})).node)})}},joint.shapes.basic.Generic.define("basic.TextBlock",{attrs:{rect:{fill:"#ffffff",stroke:"#000000",width:80,height:100},text:{fill:"#000000","font-size":14,"font-family":"Arial, helvetica, sans-serif"},".content":{text:"","ref-x":.5,"ref-y":.5,"y-alignment":"middle","x-alignment":"middle"}},content:""},{markup:['','',joint.env.test("svgforeignobject")?'
':'',""].join(""),initialize:function(){this.listenTo(this,"change:size",this.updateSize),this.listenTo(this,"change:content",this.updateContent),this.updateSize(this,this.get("size")),this.updateContent(this,this.get("content")),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateSize:function(a,b){this.attr({".fobj":joint.util.assign({},b),div:{style:joint.util.assign({},b)}})},updateContent:function(a,b){joint.env.test("svgforeignobject")?this.attr({".content":{html:joint.util.sanitizeHTML(b)}}):this.attr({".content":{text:b}})},setForeignObjectSize:function(){this.updateSize.apply(this,arguments)},setDivContent:function(){this.updateContent.apply(this,arguments)}}),joint.shapes.basic.TextBlockView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.noSVGForeignObjectElement=!joint.env.test("svgforeignobject"),joint.env.test("svgforeignobject")||this.listenTo(this.model,"change:content change:size",function(a){this.updateContent(a)})},update:function(a,b){var c=this.model;if(joint.env.test("svgforeignobject"))joint.dia.ElementView.prototype.update.call(this,c,b);else{var d=joint.util.omit(b||c.get("attrs"),".content");joint.dia.ElementView.prototype.update.call(this,c,d),b&&!joint.util.has(b,".content")||this.updateContent(c,b)}},updateContent:function(a,b){var c=joint.util.merge({},(b||a.get("attrs"))[".content"]);c=joint.util.omit(c,"text");var d=joint.util.breakText(a.get("content"),a.get("size"),c,{svgDocument:this.paper.svg}),e=joint.util.setByPath({},".content",c,"/");e[".content"].text=d,joint.dia.ElementView.prototype.update.call(this,a,e)}}),function(a,b,c,d){"use strict";var e=a.Element;e.define("standard.Rectangle",{attrs:{body:{refWidth:"100%",refHeight:"100%",strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Circle",{attrs:{body:{refCx:"50%",refCy:"50%",refR:"50%",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"circle",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Ellipse",{attrs:{body:{refCx:"50%",refCy:"50%",refRx:"50%",refRy:"50%",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"ellipse",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Path",{attrs:{body:{refD:"M 0 0 L 10 0 10 10 0 10 Z",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"path",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Polygon",{attrs:{body:{refPoints:"0 0 10 0 10 10 0 10",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"polygon",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Polyline",{attrs:{body:{refPoints:"0 0 10 0 10 10 0 10 0 0",strokeWidth:2,stroke:"#333333",fill:"#FFFFFF"},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",fontSize:14,fill:"#333333"}}},{markup:[{tagName:"polyline",selector:"body"},{tagName:"text",selector:"label"}]}),e.define("standard.Image",{attrs:{image:{refWidth:"100%",refHeight:"100%"},label:{textVerticalAnchor:"top",textAnchor:"middle",refX:"50%",refY:"100%",refY2:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"image",selector:"image"},{tagName:"text",selector:"label"}]}),e.define("standard.BorderedImage",{attrs:{border:{refWidth:"100%",refHeight:"100%",stroke:"#333333",strokeWidth:2},image:{refWidth:-1,refHeight:-1,x:.5,y:.5},label:{textVerticalAnchor:"top",textAnchor:"middle",refX:"50%",refY:"100%",refY2:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"image",selector:"image"},{tagName:"rect",selector:"border",attributes:{fill:"none"}},{tagName:"text",selector:"label"}]}),e.define("standard.EmbeddedImage",{attrs:{body:{refWidth:"100%",refHeight:"100%",stroke:"#333333",fill:"#FFFFFF",strokeWidth:2},image:{refWidth:"30%",refHeight:-20,x:10,y:10,preserveAspectRatio:"xMidYMin"},label:{textVerticalAnchor:"top",textAnchor:"left",refX:"30%",refX2:20,refY:10,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"image",selector:"image"},{tagName:"text",selector:"label"}]}),e.define("standard.HeaderedRectangle",{attrs:{body:{refWidth:"100%",refHeight:"100%",strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},header:{refWidth:"100%",height:30,strokeWidth:2,stroke:"#000000",fill:"#FFFFFF"},headerText:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:15,fontSize:16,fill:"#333333"},bodyText:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"50%",refY2:15,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"rect",selector:"body"},{tagName:"rect",selector:"header"},{tagName:"text",selector:"headerText"},{tagName:"text",selector:"bodyText"}]});var f=10;joint.dia.Element.define("standard.Cylinder",{attrs:{body:{lateralArea:f,fill:"#FFFFFF",stroke:"#333333",strokeWidth:2},top:{refCx:"50%",cy:f,refRx:"50%",ry:f,fill:"#FFFFFF",stroke:"#333333",strokeWidth:2},label:{textVerticalAnchor:"middle",textAnchor:"middle",refX:"50%",refY:"100%",refY2:15,fontSize:14,fill:"#333333"}}},{markup:[{tagName:"path",selector:"body"},{tagName:"ellipse",selector:"top"},{tagName:"text",selector:"label"}],topRy:function(a,c){if(void 0===a)return this.attr("body/lateralArea");var d=b.isPercentage(a),e={lateralArea:a},f=d?{refCy:a,refRy:a,cy:null,ry:null}:{refCy:null,refRy:null,cy:a,ry:a};return this.attr({body:e,top:f},c)}},{attributes:{lateralArea:{set:function(a,c){var e=b.isPercentage(a);e&&(a=parseFloat(a)/100);var f=c.x,g=c.y,h=c.width,i=c.height,j=h/2,k=e?i*a:a,l=d.KAPPA,m=l*j,n=l*(e?i*a:a),o=f,p=f+h/2,q=f+h,r=g+k,s=r-k,t=g+i-k,u=g+i,v=["M",o,r,"L",o,t,"C",f,t+n,p-m,u,p,u,"C",p+m,u,q,t+n,q,t,"L",q,r,"C",q,r-n,p+m,s,p,s,"C",p-m,s,o,r-n,o,r,"Z"]; +return{d:v.join(" ")}}}}});var g={tagName:"foreignObject",selector:"foreignObject",attributes:{overflow:"hidden"},children:[{tagName:"div",namespaceURI:"http://www.w3.org/1999/xhtml",selector:"label",style:{width:"100%",height:"100%",position:"static",backgroundColor:"transparent",textAlign:"center",margin:0,padding:"0px 5px",boxSizing:"border-box",display:"flex",alignItems:"center",justifyContent:"center"}}]},h={tagName:"text",selector:"label",attributes:{"text-anchor":"middle"}};e.define("standard.TextBlock",{attrs:{body:{refWidth:"100%",refHeight:"100%",stroke:"#333333",fill:"#ffffff",strokeWidth:2},foreignObject:{refWidth:"100%",refHeight:"100%"},label:{style:{fontSize:14}}}},{markup:[{tagName:"rect",selector:"body"},c.test("svgforeignobject")?g:h]},{attributes:{text:{set:function(c,d,e,f){if(!(e instanceof HTMLElement)){var g=f.style||{},h={text:c,width:-5,height:"100%"},i=b.assign({textVerticalAnchor:"middle"},g);return a.attributes.textWrap.set.call(this,h,d,e,i),{fill:g.color||null}}e.textContent=c},position:function(a,b,c){if(c instanceof SVGElement)return b.center()}}}});var i=a.Link;i.define("standard.Link",{attrs:{line:{connection:!0,stroke:"#333333",strokeWidth:2,strokeLinejoin:"round",targetMarker:{type:"path",d:"M 10 -5 0 0 10 5 z"}},wrapper:{connection:!0,strokeWidth:10,strokeLinejoin:"round"}}},{markup:[{tagName:"path",selector:"wrapper",attributes:{fill:"none",cursor:"pointer",stroke:"transparent"}},{tagName:"path",selector:"line",attributes:{fill:"none","pointer-events":"none"}}]}),i.define("standard.DoubleLink",{attrs:{line:{connection:!0,stroke:"#DDDDDD",strokeWidth:4,strokeLinejoin:"round",targetMarker:{type:"path",stroke:"#000000",d:"M 10 -3 10 -10 -2 0 10 10 10 3"}},outline:{connection:!0,stroke:"#000000",strokeWidth:6,strokeLinejoin:"round"}}},{markup:[{tagName:"path",selector:"outline",attributes:{fill:"none"}},{tagName:"path",selector:"line",attributes:{fill:"none"}}]}),i.define("standard.ShadowLink",{attrs:{line:{connection:!0,stroke:"#FF0000",strokeWidth:20,strokeLinejoin:"round",targetMarker:{type:"path",stroke:"none",d:"M 0 -10 -10 0 0 10 z"},sourceMarker:{type:"path",stroke:"none",d:"M -10 -10 0 0 -10 10 0 10 0 -10 z"}},shadow:{connection:!0,refX:3,refY:6,stroke:"#000000",strokeOpacity:.2,strokeWidth:20,strokeLinejoin:"round",targetMarker:{type:"path",d:"M 0 -10 -10 0 0 10 z",stroke:"none"},sourceMarker:{type:"path",stroke:"none",d:"M -10 -10 0 0 -10 10 0 10 0 -10 z"}}}},{markup:[{tagName:"path",selector:"shadow",attributes:{fill:"none"}},{tagName:"path",selector:"line",attributes:{fill:"none"}}]})}(joint.dia,joint.util,joint.env,V),joint.routers.manhattan=function(a,b,c,d){"use strict";function e(a){this.map={},this.options=a,this.mapGridSize=100}function f(){this.items=[],this.hash={},this.values={},this.OPEN=1,this.CLOSE=2}function g(a,b){return b&&b.paddingBox?a.sourceBBox.clone().moveAndExpand(b.paddingBox):a.sourceBBox.clone()}function h(a,b){return b&&b.paddingBox?a.targetBBox.clone().moveAndExpand(b.paddingBox):a.targetBBox.clone()}function i(a,b){if(a.sourceAnchor)return a.sourceAnchor;var c=g(a,b);return c.center()}function j(a,b){if(a.targetAnchor)return a.targetAnchor;var c=h(a,b);return c.center()}function k(b,c,d,e,f){var g=360/d,h=b.theta(l(b,c,e,f)),i=a.normalizeAngle(h+g/2);return g*Math.floor(i/g)}function l(b,c,d,e){var f=e.step,g=c.x-b.x,h=c.y-b.y,i=g/d.x,j=h/d.y,k=i*f,l=j*f;return new a.Point(b.x+k,b.y+l)}function m(a,b){var c=Math.abs(a-b);return c>180?360-c:c}function n(a,b,c){var e=c.step;d.toArray(c.directions).forEach(function(a){a.gridOffsetX=a.offsetX/e*b.x,a.gridOffsetY=a.offsetY/e*b.y})}function o(a,b,c){return{source:b.clone(),x:p(c.x-b.x,a),y:p(c.y-b.y,a)}}function p(a,b){if(!a)return b;var c=Math.abs(a),d=Math.round(c/b);if(!d)return c;var e=d*b,f=c-e,g=f/d;return b+g}function q(b,c){var d=c.source,e=a.snapToGrid(b.x-d.x,c.x)+d.x,f=a.snapToGrid(b.y-d.y,c.y)+d.y;return new a.Point(e,f)}function r(a,b){return a?a.round(b.precision):a}function s(a){return a.clone().round().toString()}function t(b){return new a.Point(0===b.x?0:Math.abs(b.x)/b.x,0===b.y?0:Math.abs(b.y)/b.y)}function u(a,b,c,d,e,f){for(var g,h=[],i=t(e.difference(c)),j=s(c),k=a[j];k;){g=r(b[j],f);var l=t(g.difference(r(k.clone(),f)));l.equals(i)||(h.unshift(g),i=l),j=s(k),k=a[j]}var m=r(b[j],f),n=t(m.difference(d));return n.equals(i)||h.unshift(m),h}function v(a,b){for(var c=1/0,d=0,e=b.length;dj)&&(j=w,t=q(v,f))}var x=r(t,g);x&&(c.containsPoint(x)&&r(x.offset(l.x*f.x,l.y*f.y),g),d.push(x))}return d},[]);return c.containsPoint(i)||n.push(i),n}function x(b,c,e,g){var h,l;h=b instanceof a.Rect?i(this,g).clone():b.clone(),l=c instanceof a.Rect?j(this,g).clone():c.clone();var p,t,x,y,z=o(g.step,h,l);if(b instanceof a.Rect?(p=r(q(h,z),g),x=w(p,b,g.startDirections,z,g)):(p=r(q(h,z),g),x=[p]),c instanceof a.Rect?(t=r(q(l,z),g),y=w(l,c,g.endDirections,z,g)):(t=r(q(l,z),g),y=[t]),x=x.filter(e.isPointAccessible,e),y=y.filter(e.isPointAccessible,e),x.length>0&&y.length>0){for(var A=new f,B={},C={},D={},E=0,F=x.length;E0;){var Q,R=A.pop(),S=B[R],T=C[R],U=D[R],V=void 0===T,W=S.equals(p);if(Q=V?L?W?null:k(p,S,N,z,g):K:k(T,S,N,z,g),O.indexOf(R)>=0)return g.previousDirectionAngle=Q,u(C,B,S,p,t,g);for(E=0;Eg.maxAllowedDirectionChange)){var Y=S.clone().offset(I.gridOffsetX,I.gridOffsetY),Z=s(Y);if(!A.isClose(Z)&&e.isPointAccessible(Y)){if(O.indexOf(Z)>=0){r(Y,g);var $=Y.equals(t);if(!$){var _=k(Y,t,N,z,g),aa=m(X,_);if(aa>g.maxAllowedDirectionChange)continue}}var ba=I.cost,ca=W?0:g.penalties[J],da=U+ba+ca;(!A.isOpen(Z)||da90){var i=f;f=h,h=i}var j=d%90<45?f:h,k=new g.Line(a,j),l=90*Math.ceil(d/90),m=g.Point.fromPolar(k.squaredLength(),g.toRad(l+135),j),n=new g.Line(b,m),o=k.intersection(n),p=o?o:b,q=o?p:a,r=360/c.directions.length,s=q.theta(b),t=g.normalizeAngle(s+r/2),u=r*Math.floor(t/r);return c.previousDirectionAngle=u,p&&e.push(p.round()),e.push(b),e}};return function(c,d,e){if(!a.isFunction(joint.routers.manhattan))throw new Error("Metro requires the manhattan router.");return joint.routers.manhattan(c,a.assign({},b,d),e)}}(joint.util),joint.routers.normal=function(a,b,c){return a},joint.routers.oneSide=function(a,b,c){var d,e,f,g=b.side||"bottom",h=b.padding||40,i=c.sourceBBox,j=c.targetBBox,k=i.center(),l=j.center();switch(g){case"bottom":f=1,d="y",e="height";break;case"top":f=-1,d="y",e="height";break;case"left":f=-1,d="x",e="width";break;case"right":f=1,d="x",e="width";break;default:throw new Error("Router: invalid side")}return k[d]+=f*(i[e]/2+h),l[d]+=f*(j[e]/2+h),f*(k[d]-l[d])>0?l[d]=k[d]:k[d]=l[d],[k].concat(a,l)},joint.routers.orthogonal=function(a){function b(a,b,c){var d=new g.Point(a.x,b.y);return c.containsPoint(d)&&(d=new g.Point(b.x,a.y)),d}function c(a,b){return a["W"===b||"E"===b?"width":"height"]}function d(a,b){return a.x===b.x?a.y>b.y?"N":"S":a.y===b.y?a.x>b.x?"W":"E":null}function e(a){return new g.Rect(a.x,a.y,0,0)}function f(a,b){var c=b&&b.elementPadding||20;return a.sourceBBox.clone().inflate(c)}function h(a,b){var c=b&&b.elementPadding||20;return a.targetBBox.clone().inflate(c)}function i(a,b){if(a.sourceAnchor)return a.sourceAnchor;var c=f(a,b);return c.center()}function j(a,b){if(a.targetAnchor)return a.targetAnchor;var c=h(a,b);return c.center()}function k(a,b,c){var e=new g.Point(a.x,b.y),f=new g.Point(b.x,a.y),h=d(a,e),i=d(a,f),j=q[c],k=h===c||h!==j&&(i===j||i!==c)?e:f;return{points:[k],direction:d(k,b)}}function l(a,c,e){var f=b(a,c,e);return{points:[f],direction:d(f,c)}}function m(e,f,h,i){var j,k={},l=[new g.Point(e.x,f.y),new g.Point(f.x,e.y)],m=l.filter(function(a){return!h.containsPoint(a)}),n=m.filter(function(a){return d(a,e)!==i});if(n.length>0)j=n.filter(function(a){return d(e,a)===i}).pop(),j=j||n[0],k.points=[j],k.direction=d(j,f);else{j=a.difference(l,m)[0];var o=new g.Point(f).move(j,-c(h,i)/2),p=b(o,e,h);k.points=[p,o],k.direction=d(o,f)}return k}function n(a,b,e,f){var h=l(b,a,f),i=h.points[0];if(e.containsPoint(i)){h=l(a,b,e);var j=h.points[0];if(f.containsPoint(j)){var m=new g.Point(a).move(j,-c(e,d(a,j))/2),n=new g.Point(b).move(i,-c(f,d(b,i))/2),o=new g.Line(m,n).midpoint(),p=l(a,o,e),q=k(o,b,p.direction);h.points=[p.points[0],q.points[0]],h.direction=q.direction}}return h}function o(a,c,e,f,h){var i,j,k,l={},m=e.union(f).inflate(1),n=m.center().distance(c)>m.center().distance(a),o=n?c:a,p=n?a:c;return h?(i=g.Point.fromPolar(m.width+m.height,r[h],o),i=m.pointNearestToPoint(i).move(i,-1)):i=m.pointNearestToPoint(o).move(o,1),j=b(i,p,m),i.round().equals(j.round())?(j=g.Point.fromPolar(m.width+m.height,g.toRad(i.theta(o))+Math.PI/2,p),j=m.pointNearestToPoint(j).move(p,1).round(),k=b(i,j,m),l.points=n?[j,k,i]:[i,k,j]):l.points=n?[j,i]:[i,j],l.direction=n?d(i,c):d(j,c),l}function p(b,c,p){var q=c.elementPadding||20,r=f(p,c),s=h(p,c),t=i(p,c),u=j(p,c);r=r.union(e(t)),s=s.union(e(u)),b=a.toArray(b).map(g.Point),b.unshift(t),b.push(u);for(var v,w=[],x=0,y=b.length-1;x=Math.abs(a.y-b.y)){var k=(a.x+b.x)/2;j=g.Path.createSegment("C",k,a.y,k,b.y,b.x,b.y),e.appendSegment(j)}else{var l=(a.y+b.y)/2;j=g.Path.createSegment("C",a.x,l,b.x,l,b.x,b.y),e.appendSegment(j)}}return f?e:e.serialize()},joint.connectors.jumpover=function(a,b,c){function d(a,c,d){var e=[].concat(a,d,c);return e.reduce(function(a,c,d){var f=e[d+1];return null!=f&&(a[d]=b.line(c,f)),a},[])}function e(a){var b=a.paper._jumpOverUpdateList;null==b&&(b=a.paper._jumpOverUpdateList=[],a.paper.on("cell:pointerup",f),a.paper.model.on("reset",function(){b=a.paper._jumpOverUpdateList=[]})),b.indexOf(a)<0&&(b.push(a),a.listenToOnce(a.model,"change:connector remove",function(){b.splice(b.indexOf(a),1)}))}function f(){for(var a=this._jumpOverUpdateList,b=0;bw)||"jumpover"!==d.name)}),z=y.map(function(a){return s.findViewByModel(a)}),A=d(a,b,f),B=z.map(function(a){return null==a?[]:a===this?A:d(a.sourcePoint,a.targetPoint,a.route)},this),C=A.reduce(function(a,b){var c=y.reduce(function(a,c,d){if(c!==v){var e=g(b,B[d]);a.push.apply(a,e)}return a},[]).sort(function(a,c){return h(b.start,a)-h(b.start,c)});return c.length>0?a.push.apply(a,i(b,c,p)):a.push(b),a},[]),D=j(C,p,q);return o?D:D.serialize()}}(_,g,joint.util),function(a,b,c,d){function e(a,b,d){var e=a.toJSON();return e.angle=b||0,c.util.defaults({},d,e)}function f(a,c,d){return a.map(function(a,b,c){var d=this.pointAt((b+.5)/c.length);return(a.dx||a.dy)&&d.offset(a.dx||0,a.dy||0),e(d.round(),0,a)},b.line(c,d))}function g(a,c,d,f){var g=c.center(),h=c.width/c.height,i=c.topMiddle(),j=b.Ellipse.fromRect(c);return a.map(function(a,b,c){var k=d+f(b,c.length),l=i.clone().rotate(g,-k).scale(h,1,g),m=a.compensateRotation?-j.tangentTheta(l):0;return(a.dx||a.dy)&&l.offset(a.dx||0,a.dy||0),a.dr&&l.move(g,a.dr),e(l.round(),m,a)})}function h(a,c){var e=c.x;d.isString(e)&&(e=parseFloat(e)/100*a.width);var f=c.y;return d.isString(f)&&(f=parseFloat(f)/100*a.height),b.point(e||0,f||0)}c.layout.Port={absolute:function(a,b,c){return a.map(h.bind(null,b))},fn:function(a,b,c){return c.fn(a,b,c)},line:function(a,b,c){var d=h(b,c.start||b.origin()),e=h(b,c.end||b.corner());return f(a,d,e)},left:function(a,b,c){return f(a,b.origin(),b.bottomLeft())},right:function(a,b,c){return f(a,b.topRight(),b.corner())},top:function(a,b,c){return f(a,b.origin(),b.topRight())},bottom:function(a,b,c){return f(a,b.bottomLeft(),b.corner())},ellipseSpread:function(a,b,c){var d=c.startAngle||0,e=c.step||360/a.length;return g(a,b,d,function(a){return a*e})},ellipse:function(a,b,c){var d=c.startAngle||0,e=c.step||20;return g(a,b,d,function(a,b){return(a+.5-b/2)*e})}}}(_,g,joint,joint.util),function(a,b,c,d){function e(a,b){return d.defaultsDeep({},a,b,{x:0,y:0,angle:0,attrs:{".":{y:"0","text-anchor":"start"}}})}function f(a,b,c,f){f=d.defaults({},f,{offset:15});var h,i,j,k,l=b.center().theta(a),m=g(b),n=f.offset,o=0;lm[2]?(j=".3em",h=n,i=0,k="start"):lo[2]?(k=".3em",i=-m,j=0,l="end"):h-270&&i<-90?(g="start",j=i-180):g="end";var m=Math.round;return e({x:m(k.x),y:m(k.y),angle:c?j:0,attrs:{".":{y:l,"text-anchor":g}}})}c.layout.PortLabel={manual:function(a,b,c){return e(c,a)},left:function(a,b,c){return e(c,{x:-15,attrs:{".":{y:".3em","text-anchor":"end"}}})},right:function(a,b,c){return e(c,{x:15,attrs:{".":{y:".3em","text-anchor":"start"}}})},top:function(a,b,c){return e(c,{y:-15,attrs:{".":{"text-anchor":"middle"}}})},bottom:function(a,b,c){return e(c,{y:15,attrs:{".":{y:".6em","text-anchor":"middle"}}})},outsideOriented:function(a,b,c){return f(a,b,!0,c)},outside:function(a,b,c){return f(a,b,!1,c)},insideOriented:function(a,b,c){return h(a,b,!0,c)},inside:function(a,b,c){return h(a,b,!1,c)},radial:function(a,b,c){return i(a.difference(b.center()),!1,c)},radialOriented:function(a,b,c){return i(a.difference(b.center()),!0,c)}}}(_,g,joint,joint.util),joint.highlighters.addClass={className:joint.util.addClassNamePrefix("highlighted"),highlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).addClass(e)},unhighlight:function(a,b,c){var d=c||{},e=d.className||this.className;V(b).removeClass(e)}},joint.highlighters.opacity={highlight:function(a,b){V(b).addClass(joint.util.addClassNamePrefix("highlight-opacity"))},unhighlight:function(a,b){V(b).removeClass(joint.util.addClassNamePrefix("highlight-opacity"))}},joint.highlighters.stroke={defaultOptions:{padding:3,rx:0,ry:0,attrs:{"stroke-width":3,stroke:"#FEB663"}},_views:{},getHighlighterId:function(a,b){return a.id+JSON.stringify(b)},removeHighlighter:function(a){this._views[a]&&(this._views[a].remove(),this._views[a]=null)},highlight:function(a,b,c){var d=this.getHighlighterId(b,c);if(!this._views[d]){var e,f=joint.util.defaults(c||{},this.defaultOptions),g=V(b);try{var h=g.convertToPathData()}catch(a){e=g.bbox(!0),h=V.rectToPath(joint.util.assign({},f,e))}var i=V("path").attr({d:h,"pointer-events":"none","vector-effect":"non-scaling-stroke",fill:"none"}).attr(f.attrs),j=g.getTransformToElement(a.el),k=f.padding;if(k){e||(e=g.bbox(!0));var l=e.x+e.width/2,m=e.y+e.height/2;e=V.transformRect(e,j);var n=Math.max(e.width,1),o=Math.max(e.height,1),p=(n+k)/n,q=(o+k)/o,r=V.createSVGMatrix({a:p,b:0,c:0,d:q,e:l-p*l,f:m-q*m});j=j.multiply(r)}i.transform(j);var s=this._views[d]=new joint.mvc.View({svgElement:!0,className:"highlight-stroke",el:i.node}),t=this.removeHighlighter.bind(this,d),u=a.model;s.listenTo(u,"remove",t),s.listenTo(u.graph,"reset",t),a.vel.append(i)}},unhighlight:function(a,b,c){this.removeHighlighter(this.getHighlighterId(b,c))}},function(a,b){function c(a){return function(c,d,e,f){var g=!!f.rotate,h=g?c.getNodeUnrotatedBBox(d):c.getNodeBBox(d),i=h[a](),j=f.dx;if(j){var k=b.isPercentage(j);j=parseFloat(j),isFinite(j)&&(k&&(j/=100,j*=h.width),i.x+=j)}var l=f.dy;if(l){var m=b.isPercentage(l);l=parseFloat(l),isFinite(l)&&(m&&(l/=100,l*=h.height),i.y+=l)}return g?i.rotate(c.model.getBBox().center(),-c.model.angle()):i}}function d(a){return function(b,c,d,e){if(d instanceof Element){var f=this.paper.findView(d),g=f.getNodeBBox(d).center();return a.call(this,b,c,g,e)}return a.apply(this,arguments)}}function e(a,b,c,d){var e=a.model.angle(),f=a.getNodeBBox(b),h=f.center(),i=f.origin(),j=f.corner(),k=d.padding;if(isFinite(k)||(k=0),i.y+k<=c.y&&c.y<=j.y-k){var l=c.y-h.y;h.x+=0===e||180===e?0:1*l/Math.tan(g.toRad(e)),h.y+=l}else if(i.x+k<=c.x&&c.x<=j.x-k){var m=c.x-h.x;h.y+=90===e||270===e?0:m*Math.tan(g.toRad(e)),h.x+=m}return h}function f(a,b,c,d){var e,f,g,h=!!d.rotate;h?(e=a.getNodeUnrotatedBBox(b),g=a.model.getBBox().center(),f=a.model.angle()):e=a.getNodeBBox(b);var i=d.padding;isFinite(i)&&e.inflate(i),h&&c.rotate(g,f);var j,k=e.sideNearestToPoint(c);switch(k){case"left":j=e.leftMiddle();break;case"right":j=e.rightMiddle();break;case"top":j=e.topMiddle();break;case"bottom":j=e.bottomMiddle()}return h?j.rotate(g,-f):j}function h(a,b){var c=a.model,d=c.getBBox(),e=d.center(),f=c.angle(),h=a.findAttribute("port",b);if(h){portGroup=c.portProp(h,"group");var i=c.getPortsPositions(portGroup),j=new g.Point(i[h]).offset(d.origin());return j.rotate(e,-f),j}return e}a.anchors={center:c("center"),top:c("topMiddle"),bottom:c("bottomMiddle"),left:c("leftMiddle"),right:c("rightMiddle"),topLeft:c("origin"),topRight:c("topRight"),bottomLeft:c("bottomLeft"),bottomRight:c("corner"),perpendicular:d(e),midSide:d(f),modelCenter:h}}(joint,joint.util),function(a,b,c,d){function e(a,c){return 1===a.length?a[0]:b.sortBy(a,function(a){return a.squaredDistance(c)})[0]}function f(a,b,c){if(!isFinite(c))return a;var d=a.distance(b);return 0===c&&d>0?a:a.move(b,-Math.min(c,d-1))}function g(a){var b=a.getAttribute("stroke-width");return null===b?0:parseFloat(b)||0}function h(a,b,c,d){return f(a.end,a.start,d.offset)}function i(a,b,c,d){var h=b.getNodeBBox(c);d.stroke&&h.inflate(g(c)/2);var i=a.intersect(h),j=i?e(i,a.start):a.end;return f(j,a.start,d.offset)}function j(a,b,c,d){var h=b.model.angle();if(0===h)return i(a,b,c,d);var j=b.getNodeUnrotatedBBox(c);d.stroke&&j.inflate(g(c)/2);var k=j.center(),l=a.clone().rotate(k,h),m=l.setLength(1e6).intersect(j),n=m?e(m,l.start).rotate(k,-h):a.end;return f(n,a.start,d.offset)}function k(a,h,i,j){var k,n,o=j.selector,p=a.end;if("string"==typeof o)k=h.findBySelector(o)[0];else if(Array.isArray(o))k=b.getByPath(i,o);else{k=i;do{var q=k.tagName.toUpperCase();if("G"===q)k=k.firstChild;else{if("TITLE"!==q)break;k=k.nextSibling}}while(k)}if(!(k instanceof Element))return p;var r=h.getNodeShape(k),s=h.getNodeMatrix(k),t=h.getRootTranslateMatrix(),u=h.getRootRotateMatrix(),v=t.multiply(u).multiply(s),w=v.inverse(),x=d.transformLine(a,w),y=x.start.clone(),z=h.getNodeData(k);if(j.insideout===!1){z[m]||(z[m]=r.bbox());var A=z[m];if(A.containsPoint(y))return p}var B;if(r instanceof c.Path){var C=j.precision||2;z[l]||(z[l]=r.getSegmentSubdivisions({precision:C})),segmentSubdivisions=z[l],B={precision:C,segmentSubdivisions:z[l]}}j.extrapolate===!0&&x.setLength(1e6),n=x.intersect(r,B),n?d.isArray(n)&&(n=e(n,y)):j.sticky===!0&&(n=r instanceof c.Rect?r.pointNearestToPoint(y):r instanceof c.Ellipse?r.intersectionWithLineFromCenterToPoint(y):r.closestPoint(y,B));var D=n?d.transformPoint(n,v):p,E=j.offset||0;return j.stroke&&(E+=g(k)/2),f(D,a.start,E)}var l="segmentSubdivisons",m="shapeBBox";a.connectionPoints={anchor:h,bbox:i,rectangle:j,boundary:k}}(joint,joint.util,g,V),function(a,b){function c(a,b){return 0===b?"0%":Math.round(a/b*100)+"%"}function d(a){return function(b,d,e,f){var g=d.model.angle(),h=d.getNodeUnrotatedBBox(e),i=d.model.getBBox().center();f.rotate(i,g);var j=f.x-h.x,k=f.y-h.y;return a&&(j=c(j,h.width),k=c(k,h.height)),b.anchor={name:"topLeft",args:{dx:j,dy:k,rotate:!0}},b}}a.connectionStrategies={useDefaults:b.noop,pinAbsolute:d(!1),pinRelative:d(!0)}}(joint,joint.util),function(a,b,c,d){function e(b,c,d){var e=a.connectionStrategies.pinRelative.call(this.paper,{},c,d,b,this.model);return e.anchor}function f(a,b,c,d,e,f){var g=f.options.snapRadius,h="source"===d,i=h?0:-1,j=this.model.vertex(i)||this.getEndAnchor(h?"target":"source");return j&&(Math.abs(j.x-a.x)0?c[a-1]:b.sourceAnchor,f=a0){var d=this.getNeighborPoints(b),e=d.prev,f=d.next;Math.abs(a.x-e.x)'}),joint.shapes.erd.Entity.define("erd.WeakEntity",{attrs:{".inner":{display:"auto"},text:{text:"Weak Entity"}}}),joint.dia.Element.define("erd.Relationship",{size:{width:80,height:80},attrs:{".outer":{fill:"#3498DB",stroke:"#2980B9","stroke-width":2,points:"40,0 80,40 40,80 0,40"},".inner":{fill:"#3498DB",stroke:"#2980B9","stroke-width":2,points:"40,5 75,40 40,75 5,40",display:"none"},text:{text:"Relationship","font-family":"Arial","font-size":12,"ref-x":.5,"ref-y":.5,"y-alignment":"middle","text-anchor":"middle"}}},{markup:''}),joint.shapes.erd.Relationship.define("erd.IdentifyingRelationship",{attrs:{".inner":{display:"auto"},text:{text:"Identifying"}}}),joint.dia.Element.define("erd.Attribute",{size:{width:100,height:50},attrs:{ellipse:{transform:"translate(50, 25)"},".outer":{stroke:"#D35400","stroke-width":2,cx:0,cy:0,rx:50,ry:25,fill:"#E67E22"},".inner":{stroke:"#D35400","stroke-width":2,cx:0,cy:0,rx:45,ry:20,fill:"#E67E22",display:"none"},text:{"font-family":"Arial","font-size":14,"ref-x":.5,"ref-y":.5,"y-alignment":"middle","text-anchor":"middle"}}},{markup:''}),joint.shapes.erd.Attribute.define("erd.Multivalued",{attrs:{".inner":{display:"block"},text:{text:"multivalued"}}}),joint.shapes.erd.Attribute.define("erd.Derived",{attrs:{".outer":{"stroke-dasharray":"3,5"},text:{text:"derived"}}}),joint.shapes.erd.Attribute.define("erd.Key",{attrs:{ellipse:{"stroke-width":4},text:{text:"key","font-weight":"800","text-decoration":"underline"}}}),joint.shapes.erd.Attribute.define("erd.Normal",{attrs:{text:{text:"Normal"}}}),joint.dia.Element.define("erd.ISA",{type:"erd.ISA",size:{width:100,height:50},attrs:{polygon:{points:"0,0 50,50 100,0",fill:"#F1C40F",stroke:"#F39C12","stroke-width":2},text:{text:"ISA","font-size":18,"ref-x":.5,"ref-y":.3,"y-alignment":"middle","text-anchor":"middle"}}},{markup:''}),joint.dia.Link.define("erd.Line",{},{cardinality:function(a){this.set("labels",[{position:-20,attrs:{text:{dy:-8,text:a}}}])}}); joint.shapes.basic.Circle.define("fsa.State",{attrs:{circle:{"stroke-width":3},text:{"font-weight":"800"}}}),joint.dia.Element.define("fsa.StartState",{size:{width:20,height:20},attrs:{circle:{transform:"translate(10, 10)",r:10,fill:"#000000"}}},{markup:''}),joint.dia.Element.define("fsa.EndState",{size:{width:20,height:20},attrs:{".outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#000000"},".inner":{transform:"translate(10, 10)",r:6,fill:"#000000"}}},{markup:''}),joint.dia.Link.define("fsa.Arrow",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z"}},smooth:!0}); joint.dia.Element.define("org.Member",{size:{width:180,height:70},attrs:{rect:{width:170,height:60},".card":{fill:"#FFFFFF",stroke:"#000000","stroke-width":2,"pointer-events":"visiblePainted",rx:10,ry:10},image:{width:48,height:48,ref:".card","ref-x":10,"ref-y":5},".rank":{"text-decoration":"underline",ref:".card","ref-x":.9,"ref-y":.2,"font-family":"Courier New","font-size":14,"text-anchor":"end"},".name":{"font-weight":"800",ref:".card","ref-x":.9,"ref-y":.6,"font-family":"Courier New","font-size":14,"text-anchor":"end"}}},{markup:''}),joint.dia.Link.define("org.Arrow",{source:{selector:".card"},target:{selector:".card"},attrs:{".connection":{stroke:"#585858","stroke-width":3}},z:-1}); joint.shapes.basic.Generic.define("chess.KingWhite",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.KingBlack",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.QueenWhite",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.QueenBlack",{size:{width:42,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.RookWhite",{size:{width:32,height:34}},{markup:' '}),joint.shapes.basic.Generic.define("chess.RookBlack",{size:{width:32,height:34}},{markup:' '}),joint.shapes.basic.Generic.define("chess.BishopWhite",{size:{width:38,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.BishopBlack",{size:{width:38,height:38}},{markup:' '}),joint.shapes.basic.Generic.define("chess.KnightWhite",{size:{width:38,height:37}},{markup:' '}),joint.shapes.basic.Generic.define("chess.KnightBlack",{size:{width:38,height:37}},{markup:' '}),joint.shapes.basic.Generic.define("chess.PawnWhite",{size:{width:28,height:33}},{markup:''}),joint.shapes.basic.Generic.define("chess.PawnBlack",{size:{width:28,height:33}},{markup:''}); joint.shapes.basic.Generic.define("pn.Place",{size:{width:50,height:50},attrs:{".root":{r:25,fill:"#ffffff",stroke:"#000000",transform:"translate(25, 25)"},".label":{"text-anchor":"middle","ref-x":.5,"ref-y":-20,ref:".root",fill:"#000000","font-size":12},".tokens > circle":{fill:"#000000",r:5},".tokens.one > circle":{transform:"translate(25, 25)"},".tokens.two > circle:nth-child(1)":{transform:"translate(19, 25)"},".tokens.two > circle:nth-child(2)":{transform:"translate(31, 25)"},".tokens.three > circle:nth-child(1)":{transform:"translate(18, 29)"},".tokens.three > circle:nth-child(2)":{transform:"translate(25, 19)"},".tokens.three > circle:nth-child(3)":{transform:"translate(32, 29)"},".tokens.alot > text":{transform:"translate(25, 18)","text-anchor":"middle",fill:"#000000"}}},{markup:''}),joint.shapes.pn.PlaceView=joint.dia.ElementView.extend({},{initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.model.on("change:tokens",function(){this.renderTokens(),this.update()},this)},render:function(){joint.dia.ElementView.prototype.render.apply(this,arguments),this.renderTokens(),this.update()},renderTokens:function(){var a=this.$(".tokens").empty();a[0].className.baseVal="tokens";var b=this.model.get("tokens");if(b)switch(b){case 1:a[0].className.baseVal+=" one",a.append(V("").node);break;case 2:a[0].className.baseVal+=" two",a.append(V("").node,V("").node);break;case 3:a[0].className.baseVal+=" three",a.append(V("").node,V("").node,V("").node);break;default:a[0].className.baseVal+=" alot",a.append(V("").text(b+"").node)}}}),joint.shapes.basic.Generic.define("pn.Transition",{size:{width:12,height:50},attrs:{rect:{width:12,height:50,fill:"#000000",stroke:"#000000"},".label":{"text-anchor":"middle","ref-x":.5,"ref-y":-20,ref:"rect",fill:"#000000","font-size":12}}},{markup:''}),joint.dia.Link.define("pn.Link",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z"}}}); joint.shapes.basic.Generic.define("devs.Model",{inPorts:[],outPorts:[],size:{width:80,height:80},attrs:{".":{magnet:!1},".label":{text:"Model","ref-x":.5,"ref-y":10,"font-size":18,"text-anchor":"middle",fill:"#000"},".body":{"ref-width":"100%","ref-height":"100%",stroke:"#000"}},ports:{groups:{in:{position:{name:"left"},attrs:{".port-label":{fill:"#000"},".port-body":{fill:"#fff",stroke:"#000",r:10,magnet:!0}},label:{position:{name:"left",args:{y:10}}}},out:{position:{name:"right"},attrs:{".port-label":{fill:"#000"},".port-body":{fill:"#fff",stroke:"#000",r:10,magnet:!0}},label:{position:{name:"right",args:{y:10}}}}}}},{markup:'',portMarkup:'',portLabelMarkup:'',initialize:function(){joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments),this.on("change:inPorts change:outPorts",this.updatePortItems,this),this.updatePortItems()},updatePortItems:function(a,b,c){var d=joint.util.uniq(this.get("inPorts")),e=joint.util.difference(joint.util.uniq(this.get("outPorts")),d),f=this.createPortItems("in",d),g=this.createPortItems("out",e);this.prop("ports/items",f.concat(g),joint.util.assign({rewrite:!0},c))},createPortItem:function(a,b){return{id:b,group:a,attrs:{".port-label":{text:b}}}},createPortItems:function(a,b){return joint.util.toArray(b).map(this.createPortItem.bind(this,a))},_addGroupPort:function(a,b,c){var d=this.get(b);return this.set(b,Array.isArray(d)?d.concat(a):[a],c)},addOutPort:function(a,b){return this._addGroupPort(a,"outPorts",b)},addInPort:function(a,b){return this._addGroupPort(a,"inPorts",b)},_removeGroupPort:function(a,b,c){return this.set(b,joint.util.without(this.get(b),a),c)},removeOutPort:function(a,b){return this._removeGroupPort(a,"outPorts",b)},removeInPort:function(a,b){return this._removeGroupPort(a,"inPorts",b)},_changeGroup:function(a,b,c){return this.prop("ports/groups/"+a,joint.util.isObject(b)?b:{},c)},changeInGroup:function(a,b){return this._changeGroup("in",a,b)},changeOutGroup:function(a,b){return this._changeGroup("out",a,b)}}),joint.shapes.devs.Model.define("devs.Atomic",{size:{width:80,height:80},attrs:{".label":{text:"Atomic"}}}),joint.shapes.devs.Model.define("devs.Coupled",{size:{width:200,height:300},attrs:{".label":{text:"Coupled"}}}),joint.dia.Link.define("devs.Link",{attrs:{".connection":{"stroke-width":2}}}); -joint.shapes.basic.Generic.define("uml.Class",{attrs:{rect:{width:200},".uml-class-name-rect":{stroke:"black","stroke-width":2,fill:"#3498db"},".uml-class-attrs-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-methods-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-name-text":{ref:".uml-class-name-rect","ref-y":.5,"ref-x":.5,"text-anchor":"middle","y-alignment":"middle","font-weight":"bold",fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-attrs-text":{ref:".uml-class-attrs-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-methods-text":{ref:".uml-class-methods-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"}},name:[],attributes:[],methods:[]},{markup:['','','',"",'',""].join(""),initialize:function(){this.on("change:name change:attributes change:methods",function(){this.updateRectangles(),this.trigger("uml-update")},this),this.updateRectangles(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},getClassName:function(){return this.get("name")},updateRectangles:function(){var a=this.get("attrs"),b=[{type:"name",text:this.getClassName()},{type:"attrs",text:this.get("attributes")},{type:"methods",text:this.get("methods")}],c=0;b.forEach(function(b){var d=Array.isArray(b.text)?b.text:[b.text],e=20*d.length+20;a[".uml-class-"+b.type+"-text"].text=d.join("\n"),a[".uml-class-"+b.type+"-rect"].height=e,a[".uml-class-"+b.type+"-rect"].transform="translate(0,"+c+")",c+=e})}}),joint.shapes.uml.ClassView=joint.dia.ElementView.extend({},{initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"uml-update",function(){this.update(),this.resize()})}}),joint.shapes.uml.Class.define("uml.Abstract",{attrs:{".uml-class-name-rect":{fill:"#e74c3c"},".uml-class-attrs-rect":{fill:"#c0392b"},".uml-class-methods-rect":{fill:"#c0392b"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.AbstractView=joint.shapes.uml.ClassView,joint.shapes.uml.Class.define("uml.Interface",{attrs:{".uml-class-name-rect":{fill:"#f1c40f"},".uml-class-attrs-rect":{fill:"#f39c12"},".uml-class-methods-rect":{fill:"#f39c12"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.InterfaceView=joint.shapes.uml.ClassView,joint.dia.Link.define("uml.Generalization",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"}}}),joint.dia.Link.define("uml.Implementation",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"},".connection":{"stroke-dasharray":"3,3"}}}),joint.dia.Link.define("uml.Aggregation",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"white"}}}),joint.dia.Link.define("uml.Composition",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"black"}}}),joint.dia.Link.define("uml.Association"),joint.shapes.basic.Generic.define("uml.State",{attrs:{".uml-state-body":{width:200,height:200,rx:10,ry:10,fill:"#ecf0f1",stroke:"#bdc3c7","stroke-width":3},".uml-state-separator":{stroke:"#bdc3c7","stroke-width":2},".uml-state-name":{ref:".uml-state-body","ref-x":.5,"ref-y":5,"text-anchor":"middle",fill:"#000000","font-family":"Courier New","font-size":14},".uml-state-events":{ref:".uml-state-separator","ref-x":5,"ref-y":5,fill:"#000000","font-family":"Courier New","font-size":14}},name:"State",events:[]},{markup:['','','',"",'','','',""].join(""),initialize:function(){this.on({"change:name":this.updateName,"change:events":this.updateEvents,"change:size":this.updatePath},this),this.updateName(),this.updateEvents(),this.updatePath(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateName:function(){this.attr(".uml-state-name/text",this.get("name"))},updateEvents:function(){this.attr(".uml-state-events/text",this.get("events").join("\n"))},updatePath:function(){var a="M 0 20 L "+this.get("size").width+" 20";this.attr(".uml-state-separator/d",a,{silent:!0})}}),joint.shapes.basic.Circle.define("uml.StartState",{type:"uml.StartState",attrs:{circle:{fill:"#34495e",stroke:"#2c3e50","stroke-width":2,rx:1}}}),joint.shapes.basic.Generic.define("uml.EndState",{size:{width:20,height:20},attrs:{"circle.outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#2c3e50"},"circle.inner":{transform:"translate(10, 10)",r:6,fill:"#34495e"}}},{markup:''}),joint.dia.Link.define("uml.Transition",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z",fill:"#34495e",stroke:"#2c3e50"},".connection":{stroke:"#2c3e50"}}}); +joint.shapes.basic.Generic.define("uml.Class",{attrs:{rect:{width:200},".uml-class-name-rect":{stroke:"black","stroke-width":2,fill:"#3498db"},".uml-class-attrs-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-methods-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-name-text":{ref:".uml-class-name-rect","ref-y":.5,"ref-x":.5,"text-anchor":"middle","y-alignment":"middle","font-weight":"bold",fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-attrs-text":{ref:".uml-class-attrs-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-methods-text":{ref:".uml-class-methods-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"}},name:[],attributes:[],methods:[]},{markup:['','','',"",'',""].join(""),initialize:function(){this.on("change:name change:attributes change:methods",function(){this.updateRectangles(),this.trigger("uml-update")},this),this.updateRectangles(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},getClassName:function(){return this.get("name")},updateRectangles:function(){var a=this.get("attrs"),b=[{type:"name",text:this.getClassName()},{type:"attrs",text:this.get("attributes")},{type:"methods",text:this.get("methods")}],c=0;b.forEach(function(b){var d=Array.isArray(b.text)?b.text:[b.text],e=20*d.length+20;a[".uml-class-"+b.type+"-text"].text=d.join("\n"),a[".uml-class-"+b.type+"-rect"].height=e,a[".uml-class-"+b.type+"-rect"].transform="translate(0,"+c+")",c+=e})}}),joint.shapes.uml.ClassView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"uml-update",function(){this.update(),this.resize()})}}),joint.shapes.uml.Class.define("uml.Abstract",{attrs:{".uml-class-name-rect":{fill:"#e74c3c"},".uml-class-attrs-rect":{fill:"#c0392b"},".uml-class-methods-rect":{fill:"#c0392b"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.AbstractView=joint.shapes.uml.ClassView,joint.shapes.uml.Class.define("uml.Interface",{attrs:{".uml-class-name-rect":{fill:"#f1c40f"},".uml-class-attrs-rect":{fill:"#f39c12"},".uml-class-methods-rect":{fill:"#f39c12"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.InterfaceView=joint.shapes.uml.ClassView,joint.dia.Link.define("uml.Generalization",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"}}}),joint.dia.Link.define("uml.Implementation",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"},".connection":{"stroke-dasharray":"3,3"}}}),joint.dia.Link.define("uml.Aggregation",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"white"}}}),joint.dia.Link.define("uml.Composition",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"black"}}}),joint.dia.Link.define("uml.Association"),joint.shapes.basic.Generic.define("uml.State",{attrs:{".uml-state-body":{width:200,height:200,rx:10,ry:10,fill:"#ecf0f1",stroke:"#bdc3c7","stroke-width":3},".uml-state-separator":{stroke:"#bdc3c7","stroke-width":2},".uml-state-name":{ref:".uml-state-body","ref-x":.5,"ref-y":5,"text-anchor":"middle",fill:"#000000","font-family":"Courier New","font-size":14},".uml-state-events":{ref:".uml-state-separator","ref-x":5,"ref-y":5,fill:"#000000","font-family":"Courier New","font-size":14}},name:"State",events:[]},{markup:['','','',"",'','','',""].join(""),initialize:function(){this.on({"change:name":this.updateName,"change:events":this.updateEvents,"change:size":this.updatePath},this),this.updateName(),this.updateEvents(),this.updatePath(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateName:function(){this.attr(".uml-state-name/text",this.get("name"))},updateEvents:function(){this.attr(".uml-state-events/text",this.get("events").join("\n"))},updatePath:function(){var a="M 0 20 L "+this.get("size").width+" 20";this.attr(".uml-state-separator/d",a,{silent:!0})}}),joint.shapes.basic.Circle.define("uml.StartState",{type:"uml.StartState",attrs:{circle:{fill:"#34495e",stroke:"#2c3e50","stroke-width":2,rx:1}}}),joint.shapes.basic.Generic.define("uml.EndState",{size:{width:20,height:20},attrs:{"circle.outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#2c3e50"},"circle.inner":{transform:"translate(10, 10)",r:6,fill:"#34495e"}}},{markup:''}),joint.dia.Link.define("uml.Transition",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z",fill:"#34495e",stroke:"#2c3e50"},".connection":{stroke:"#2c3e50"}}}); joint.shapes.basic.Generic.define("logic.Gate",{size:{width:80,height:40},attrs:{".":{magnet:!1},".body":{width:100,height:50},circle:{r:7,stroke:"black",fill:"transparent","stroke-width":2}}},{operation:function(){return!0}}),joint.shapes.logic.Gate.define("logic.IO",{size:{width:60,height:30},attrs:{".body":{fill:"white",stroke:"black","stroke-width":2},".wire":{ref:".body","ref-y":.5,stroke:"black"},text:{fill:"black",ref:".body","ref-x":.5,"ref-y":.5,"y-alignment":"middle","text-anchor":"middle","font-weight":"bold","font-variant":"small-caps","text-transform":"capitalize","font-size":"14px"}}},{markup:''}),joint.shapes.logic.IO.define("logic.Input",{attrs:{".wire":{"ref-dx":0,d:"M 0 0 L 23 0"},circle:{ref:".body","ref-dx":30,"ref-y":.5,magnet:!0,class:"output",port:"out"},text:{text:"input"}}}),joint.shapes.logic.IO.define("logic.Output",{attrs:{".wire":{"ref-x":0,d:"M 0 0 L -23 0"},circle:{ref:".body","ref-x":-30,"ref-y":.5,magnet:"passive",class:"input",port:"in"},text:{text:"output"}}}),joint.shapes.logic.Gate.define("logic.Gate11",{attrs:{".input":{ref:".body","ref-x":-2,"ref-y":.5,magnet:"passive",port:"in"},".output":{ref:".body","ref-dx":2,"ref-y":.5,magnet:!0,port:"out"}}},{markup:''}),joint.shapes.logic.Gate.define("logic.Gate21",{attrs:{".input1":{ref:".body","ref-x":-2,"ref-y":.3,magnet:"passive",port:"in1"},".input2":{ref:".body","ref-x":-2,"ref-y":.7,magnet:"passive",port:"in2"},".output":{ref:".body","ref-dx":2,"ref-y":.5,magnet:!0,port:"out"}}},{markup:''}),joint.shapes.logic.Gate11.define("logic.Repeater",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PVCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjgiCiAgICAgaW5rc2NhcGU6Y3g9Ijg0LjY4NTM1MiIKICAgICBpbmtzY2FwZTpjeT0iMTUuMjg4NjI4IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzIuMTU2OTEsMjUgTCA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAyOS4wNDM0NzgsMjUgTCA1LjA0MzQ3ODEsMjUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSAyOC45Njg3NSwyLjU5Mzc1IEwgMjguOTY4NzUsNSBMIDI4Ljk2ODc1LDQ1IEwgMjguOTY4NzUsNDcuNDA2MjUgTCAzMS4xMjUsNDYuMzQzNzUgTCA3Mi4xNTYyNSwyNi4zNDM3NSBMIDcyLjE1NjI1LDIzLjY1NjI1IEwgMzEuMTI1LDMuNjU2MjUgTCAyOC45Njg3NSwyLjU5Mzc1IHogTSAzMS45Njg3NSw3LjQwNjI1IEwgNjguMDkzNzUsMjUgTCAzMS45Njg3NSw0Mi41OTM3NSBMIDMxLjk2ODc1LDcuNDA2MjUgeiIKICAgICAgIGlkPSJwYXRoMjYzOCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjY2NjYyIgLz4KICA8L2c+Cjwvc3ZnPgo="}}},{operation:function(a){return a}}),joint.shapes.logic.Gate11.define("logic.Not",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PVCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjgiCiAgICAgaW5rc2NhcGU6Y3g9Ijg0LjY4NTM1MiIKICAgICBpbmtzY2FwZTpjeT0iMTUuMjg4NjI4IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzkuMTU2OTEsMjUgTCA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAyOS4wNDM0NzgsMjUgTCA1LjA0MzQ3ODEsMjUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSAyOC45Njg3NSwyLjU5Mzc1IEwgMjguOTY4NzUsNSBMIDI4Ljk2ODc1LDQ1IEwgMjguOTY4NzUsNDcuNDA2MjUgTCAzMS4xMjUsNDYuMzQzNzUgTCA3Mi4xNTYyNSwyNi4zNDM3NSBMIDcyLjE1NjI1LDIzLjY1NjI1IEwgMzEuMTI1LDMuNjU2MjUgTCAyOC45Njg3NSwyLjU5Mzc1IHogTSAzMS45Njg3NSw3LjQwNjI1IEwgNjguMDkzNzUsMjUgTCAzMS45Njg3NSw0Mi41OTM3NSBMIDMxLjk2ODc1LDcuNDA2MjUgeiIKICAgICAgIGlkPSJwYXRoMjYzOCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjY2NjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDI2NzEiCiAgICAgICBzb2RpcG9kaTpjeD0iNzYiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA4MCwyNSBBIDQsNCAwIDEgMSA3MiwyNSBBIDQsNCAwIDEgMSA4MCwyNSB6IgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEsMCkiIC8+CiAgPC9nPgo8L3N2Zz4K"}}},{operation:function(a){return!a}}),joint.shapes.logic.Gate21.define("logic.Or",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik9SIEFOU0kuc3ZnIgogICBpbmtzY2FwZTpvdXRwdXRfZXh0ZW5zaW9uPSJvcmcuaW5rc2NhcGUub3V0cHV0LnN2Zy5pbmtzY2FwZSI+CiAgPGRlZnMKICAgICBpZD0iZGVmczQiPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjUwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjI1IDogMTAgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjcxNCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iMSA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMC41IDogMC4zMzMzMzMzMyA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyODA2IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyODE5IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjM3Mi4wNDcyNCA6IDM1MC43ODczOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI3NDQuMDk0NDggOiA1MjYuMTgxMDkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzc3IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49Ijc1IDogNDAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iMTUwIDogNjAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDYwIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTMyNzUiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNTAgOiAzMy4zMzMzMzMgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iMTAwIDogNTAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDUwIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTU1MzMiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzIgOiAyMS4zMzMzMzMgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNjQgOiAzMiA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMzIgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjU1NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDE2LjY2NjY2NyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDI1IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAyNSA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iNCIKICAgICBpbmtzY2FwZTpjeD0iMTEzLjAwMDM5IgogICAgIGlua3NjYXBlOmN5PSIxMi44OTM3MzEiCiAgICAgaW5rc2NhcGU6ZG9jdW1lbnQtdW5pdHM9InB4IgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImcyNTYwIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTpncmlkLWJib3g9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1wb2ludHM9InRydWUiCiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAwMDAiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxMzk5IgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9Ijg3NCIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMzciCiAgICAgaW5rc2NhcGU6d2luZG93LXk9Ii00IgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNzAsMjUgYyAyMCwwIDI1LDAgMjUsMCIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMSwxNSA1LDE1IgogICAgICAgaWQ9InBhdGgzMDYxIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzIsMzUgNSwzNSIKICAgICAgIGlkPSJwYXRoMzk0NCIgLz4KICAgIDxnCiAgICAgICBpZD0iZzI1NjAiCiAgICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2LjUsLTM5LjUpIj4KICAgICAgPHBhdGgKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjQwNjI1LDQ0LjUgTCAtMC40MDYyNSw0Ni45Mzc1IEMgLTAuNDA2MjUsNDYuOTM3NSA1LjI1LDUzLjkzNzU0OSA1LjI1LDY0LjUgQyA1LjI1LDc1LjA2MjQ1MSAtMC40MDYyNSw4Mi4wNjI1IC0wLjQwNjI1LDgyLjA2MjUgTCAtMi40MDYyNSw4NC41IEwgMC43NSw4NC41IEwgMTQuNzUsODQuNSBDIDE3LjE1ODA3Niw4NC41MDAwMDEgMjIuNDM5Njk5LDg0LjUyNDUxNCAyOC4zNzUsODIuMDkzNzUgQyAzNC4zMTAzMDEsNzkuNjYyOTg2IDQwLjkxMTUzNiw3NC43NTA0ODQgNDYuMDYyNSw2NS4yMTg3NSBMIDQ0Ljc1LDY0LjUgTCA0Ni4wNjI1LDYzLjc4MTI1IEMgMzUuNzU5Mzg3LDQ0LjcxNTU5IDE5LjUwNjU3NCw0NC41IDE0Ljc1LDQ0LjUgTCAwLjc1LDQ0LjUgTCAtMi40MDYyNSw0NC41IHogTSAzLjQ2ODc1LDQ3LjUgTCAxNC43NSw0Ny41IEMgMTkuNDM0MTczLDQ3LjUgMzMuMDM2ODUsNDcuMzY5NzkzIDQyLjcxODc1LDY0LjUgQyAzNy45NTE5NjQsNzIuOTI5MDc1IDMyLjE5NzQ2OSw3Ny4xODM5MSAyNyw3OS4zMTI1IEMgMjEuNjM5MzM5LDgxLjUwNzkyNCAxNy4xNTgwNzUsODEuNTAwMDAxIDE0Ljc1LDgxLjUgTCAzLjUsODEuNSBDIDUuMzczNTg4NCw3OC4zOTE1NjYgOC4yNSw3Mi40NTA2NSA4LjI1LDY0LjUgQyA4LjI1LDU2LjUyNjY0NiA1LjM0MTQ2ODYsNTAuNTk5ODE1IDMuNDY4NzUsNDcuNSB6IgogICAgICAgICBpZD0icGF0aDQ5NzMiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NzY2NjY3NjY2NjY2NjY2NzY2NzYyIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo="}}},{operation:function(a,b){return a||b}}),joint.shapes.logic.Gate21.define("logic.And",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IkFORCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgPC9kZWZzPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSI4IgogICAgIGlua3NjYXBlOmN4PSI1Ni42OTgzNDgiCiAgICAgaW5rc2NhcGU6Y3k9IjI1LjMyNjg5OSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDcwLDI1IGMgMjAsMCAyNSwwIDI1LDAiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEsMTUgNSwxNSIKICAgICAgIGlkPSJwYXRoMzA2MSIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjk5OTk5OTg4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMyLDM1IDUsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTptZWRpdW07Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDt0ZXh0LWluZGVudDowO3RleHQtYWxpZ246c3RhcnQ7dGV4dC1kZWNvcmF0aW9uOm5vbmU7bGluZS1oZWlnaHQ6bm9ybWFsO2xldHRlci1zcGFjaW5nOm5vcm1hbDt3b3JkLXNwYWNpbmc6bm9ybWFsO3RleHQtdHJhbnNmb3JtOm5vbmU7ZGlyZWN0aW9uOmx0cjtibG9jay1wcm9ncmVzc2lvbjp0Yjt3cml0aW5nLW1vZGU6bHItdGI7dGV4dC1hbmNob3I6c3RhcnQ7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDozO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGU7Zm9udC1mYW1pbHk6Qml0c3RyZWFtIFZlcmEgU2FuczstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOkJpdHN0cmVhbSBWZXJhIFNhbnMiCiAgICAgICBkPSJNIDMwLDUgTCAzMCw2LjQyODU3MTQgTCAzMCw0My41NzE0MjkgTCAzMCw0NSBMIDMxLjQyODU3MSw0NSBMIDUwLjQ3NjE5LDQ1IEMgNjEuNzQ0MDk4LDQ1IDcwLjQ3NjE5LDM1Ljk5OTk1NSA3MC40NzYxOSwyNSBDIDcwLjQ3NjE5LDE0LjAwMDA0NSA2MS43NDQwOTksNS4wMDAwMDAyIDUwLjQ3NjE5LDUgQyA1MC40NzYxOSw1IDUwLjQ3NjE5LDUgMzEuNDI4NTcxLDUgTCAzMCw1IHogTSAzMi44NTcxNDMsNy44NTcxNDI5IEMgNDAuODM0MjY0LDcuODU3MTQyOSA0NS45MTgzNjgsNy44NTcxNDI5IDQ4LjA5NTIzOCw3Ljg1NzE0MjkgQyA0OS4yODU3MTQsNy44NTcxNDI5IDQ5Ljg4MDk1Miw3Ljg1NzE0MjkgNTAuMTc4NTcxLDcuODU3MTQyOSBDIDUwLjMyNzM4MSw3Ljg1NzE0MjkgNTAuNDA5MjI3LDcuODU3MTQyOSA1MC40NDY0MjksNy44NTcxNDI5IEMgNTAuNDY1MDI5LDcuODU3MTQyOSA1MC40NzE1NDMsNy44NTcxNDI5IDUwLjQ3NjE5LDcuODU3MTQyOSBDIDYwLjIzNjg1Myw3Ljg1NzE0MyA2Ny4xNDI4NTcsMTUuNDk3MDk4IDY3LjE0Mjg1NywyNSBDIDY3LjE0Mjg1NywzNC41MDI5MDIgNTkuNzYwNjYyLDQyLjE0Mjg1NyA1MCw0Mi4xNDI4NTcgTCAzMi44NTcxNDMsNDIuMTQyODU3IEwgMzIuODU3MTQzLDcuODU3MTQyOSB6IgogICAgICAgaWQ9InBhdGgyODg0IgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NjY2NzY2NjY3Nzc3NzY2NjIiAvPgogIDwvZz4KPC9zdmc+Cg=="}}},{operation:function(a,b){return a&&b}}),joint.shapes.logic.Gate21.define("logic.Nor",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PUiBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjEiCiAgICAgaW5rc2NhcGU6Y3g9Ijc4LjY3NzY0NCIKICAgICBpbmtzY2FwZTpjeT0iMjIuMTAyMzQ0IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjM3IgogICAgIGlua3NjYXBlOndpbmRvdy15PSItNCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDc5LDI1IEMgOTksMjUgOTUsMjUgOTUsMjUiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEsMTUgNSwxNSIKICAgICAgIGlkPSJwYXRoMzA2MSIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjk5OTk5OTg4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMyLDM1IDUsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8ZwogICAgICAgaWQ9ImcyNTYwIgogICAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNi41LC0zOS41KSI+CiAgICAgIDxwYXRoCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi40MDYyNSw0NC41IEwgLTAuNDA2MjUsNDYuOTM3NSBDIC0wLjQwNjI1LDQ2LjkzNzUgNS4yNSw1My45Mzc1NDkgNS4yNSw2NC41IEMgNS4yNSw3NS4wNjI0NTEgLTAuNDA2MjUsODIuMDYyNSAtMC40MDYyNSw4Mi4wNjI1IEwgLTIuNDA2MjUsODQuNSBMIDAuNzUsODQuNSBMIDE0Ljc1LDg0LjUgQyAxNy4xNTgwNzYsODQuNTAwMDAxIDIyLjQzOTY5OSw4NC41MjQ1MTQgMjguMzc1LDgyLjA5Mzc1IEMgMzQuMzEwMzAxLDc5LjY2Mjk4NiA0MC45MTE1MzYsNzQuNzUwNDg0IDQ2LjA2MjUsNjUuMjE4NzUgTCA0NC43NSw2NC41IEwgNDYuMDYyNSw2My43ODEyNSBDIDM1Ljc1OTM4Nyw0NC43MTU1OSAxOS41MDY1NzQsNDQuNSAxNC43NSw0NC41IEwgMC43NSw0NC41IEwgLTIuNDA2MjUsNDQuNSB6IE0gMy40Njg3NSw0Ny41IEwgMTQuNzUsNDcuNSBDIDE5LjQzNDE3Myw0Ny41IDMzLjAzNjg1LDQ3LjM2OTc5MyA0Mi43MTg3NSw2NC41IEMgMzcuOTUxOTY0LDcyLjkyOTA3NSAzMi4xOTc0NjksNzcuMTgzOTEgMjcsNzkuMzEyNSBDIDIxLjYzOTMzOSw4MS41MDc5MjQgMTcuMTU4MDc1LDgxLjUwMDAwMSAxNC43NSw4MS41IEwgMy41LDgxLjUgQyA1LjM3MzU4ODQsNzguMzkxNTY2IDguMjUsNzIuNDUwNjUgOC4yNSw2NC41IEMgOC4yNSw1Ni41MjY2NDYgNS4zNDE0Njg2LDUwLjU5OTgxNSAzLjQ2ODc1LDQ3LjUgeiIKICAgICAgICAgaWQ9InBhdGg0OTczIgogICAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjc2NjY2NzY2NjY2NjY2Njc2Njc2MiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIHNvZGlwb2RpOnR5cGU9ImFyYyIKICAgICAgICAgc3R5bGU9ImZpbGw6bm9uZTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgICBpZD0icGF0aDI2MDQiCiAgICAgICAgIHNvZGlwb2RpOmN4PSI3NSIKICAgICAgICAgc29kaXBvZGk6Y3k9IjI1IgogICAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgICAgc29kaXBvZGk6cnk9IjQiCiAgICAgICAgIGQ9Ik0gNzksMjUgQSA0LDQgMCAxIDEgNzEsMjUgQSA0LDQgMCAxIDEgNzksMjUgeiIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI2LjUsMzkuNSkiIC8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K" }}},{operation:function(a,b){return!(a||b)}}),joint.shapes.logic.Gate21.define("logic.Nand",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5BTkQgQU5TSS5zdmciCiAgIGlua3NjYXBlOm91dHB1dF9leHRlbnNpb249Im9yZy5pbmtzY2FwZS5vdXRwdXQuc3ZnLmlua3NjYXBlIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCI+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxMCA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzE0IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIwLjUgOiAwLjMzMzMzMzMzIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MDYiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MTkiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzcyLjA0NzI0IDogMzUwLjc4NzM5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9Ijc0NC4wOTQ0OCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTI2LjE4MTA5IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3NzciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNzUgOiA0MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxNTAgOiA2MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNjAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMzI3NSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI1MCA6IDMzLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxMDAgOiA1MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlNTUzMyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzMiA6IDIxLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI2NCA6IDMyIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAzMiA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iMTYiCiAgICAgaW5rc2NhcGU6Y3g9Ijc4LjI4MzMwNyIKICAgICBpbmtzY2FwZTpjeT0iMTYuNDQyODQzIgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzksMjUgQyA5MS44LDI1IDk1LDI1IDk1LDI1IgogICAgICAgaWQ9InBhdGgzMDU5IgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMxLDE1IDUsMTUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk4ODtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMiwzNSA1LDM1IgogICAgICAgaWQ9InBhdGgzOTQ0IiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmb250LXNpemU6bWVkaXVtO2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7dGV4dC1pbmRlbnQ6MDt0ZXh0LWFsaWduOnN0YXJ0O3RleHQtZGVjb3JhdGlvbjpub25lO2xpbmUtaGVpZ2h0Om5vcm1hbDtsZXR0ZXItc3BhY2luZzpub3JtYWw7d29yZC1zcGFjaW5nOm5vcm1hbDt0ZXh0LXRyYW5zZm9ybTpub25lO2RpcmVjdGlvbjpsdHI7YmxvY2stcHJvZ3Jlc3Npb246dGI7d3JpdGluZy1tb2RlOmxyLXRiO3RleHQtYW5jaG9yOnN0YXJ0O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MzttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO2ZvbnQtZmFtaWx5OkJpdHN0cmVhbSBWZXJhIFNhbnM7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpCaXRzdHJlYW0gVmVyYSBTYW5zIgogICAgICAgZD0iTSAzMCw1IEwgMzAsNi40Mjg1NzE0IEwgMzAsNDMuNTcxNDI5IEwgMzAsNDUgTCAzMS40Mjg1NzEsNDUgTCA1MC40NzYxOSw0NSBDIDYxLjc0NDA5OCw0NSA3MC40NzYxOSwzNS45OTk5NTUgNzAuNDc2MTksMjUgQyA3MC40NzYxOSwxNC4wMDAwNDUgNjEuNzQ0MDk5LDUuMDAwMDAwMiA1MC40NzYxOSw1IEMgNTAuNDc2MTksNSA1MC40NzYxOSw1IDMxLjQyODU3MSw1IEwgMzAsNSB6IE0gMzIuODU3MTQzLDcuODU3MTQyOSBDIDQwLjgzNDI2NCw3Ljg1NzE0MjkgNDUuOTE4MzY4LDcuODU3MTQyOSA0OC4wOTUyMzgsNy44NTcxNDI5IEMgNDkuMjg1NzE0LDcuODU3MTQyOSA0OS44ODA5NTIsNy44NTcxNDI5IDUwLjE3ODU3MSw3Ljg1NzE0MjkgQyA1MC4zMjczODEsNy44NTcxNDI5IDUwLjQwOTIyNyw3Ljg1NzE0MjkgNTAuNDQ2NDI5LDcuODU3MTQyOSBDIDUwLjQ2NTAyOSw3Ljg1NzE0MjkgNTAuNDcxNTQzLDcuODU3MTQyOSA1MC40NzYxOSw3Ljg1NzE0MjkgQyA2MC4yMzY4NTMsNy44NTcxNDMgNjcuMTQyODU3LDE1LjQ5NzA5OCA2Ny4xNDI4NTcsMjUgQyA2Ny4xNDI4NTcsMzQuNTAyOTAyIDU5Ljc2MDY2Miw0Mi4xNDI4NTcgNTAsNDIuMTQyODU3IEwgMzIuODU3MTQzLDQyLjE0Mjg1NyBMIDMyLjg1NzE0Myw3Ljg1NzE0MjkgeiIKICAgICAgIGlkPSJwYXRoMjg4NCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2Njc2NjY2Nzc3Nzc2NjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDQwMDgiCiAgICAgICBzb2RpcG9kaTpjeD0iNzUiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA3OSwyNSBBIDQsNCAwIDEgMSA3MSwyNSBBIDQsNCAwIDEgMSA3OSwyNSB6IiAvPgogIDwvZz4KPC9zdmc+Cg=="}}},{operation:function(a,b){return!(a&&b)}}),joint.shapes.logic.Gate21.define("logic.Xor",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IlhPUiBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjUuNjU2ODU0MiIKICAgICBpbmtzY2FwZTpjeD0iMjUuOTM4MTE2IgogICAgIGlua3NjYXBlOmN5PSIxNy4yMzAwNSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDcwLDI1IGMgMjAsMCAyNSwwIDI1LDAiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzAuMzg1NzE3LDE1IEwgNC45OTk5OTk4LDE1IgogICAgICAgaWQ9InBhdGgzMDYxIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5NzY7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEuMzYyMDkxLDM1IEwgNC45OTk5OTk4LDM1IgogICAgICAgaWQ9InBhdGgzOTQ0IiAvPgogICAgPGcKICAgICAgIGlkPSJnMjU2MCIKICAgICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYuNSwtMzkuNSkiPgogICAgICA8cGF0aAogICAgICAgICBpZD0icGF0aDM1MTYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi4yNSw4MS41MDAwMDUgQyAtMy44NDczNzQsODQuMTQ0NDA1IC00LjUsODQuNTAwMDA1IC00LjUsODQuNTAwMDA1IEwgLTguMTU2MjUsODQuNTAwMDA1IEwgLTYuMTU2MjUsODIuMDYyNTA1IEMgLTYuMTU2MjUsODIuMDYyNTA1IC0wLjUsNzUuMDYyNDUxIC0wLjUsNjQuNSBDIC0wLjUsNTMuOTM3NTQ5IC02LjE1NjI1LDQ2LjkzNzUgLTYuMTU2MjUsNDYuOTM3NSBMIC04LjE1NjI1LDQ0LjUgTCAtNC41LDQ0LjUgQyAtMy43MTg3NSw0NS40Mzc1IC0zLjA3ODEyNSw0Ni4xNTYyNSAtMi4yODEyNSw0Ny41IEMgLTAuNDA4NTMxLDUwLjU5OTgxNSAyLjUsNTYuNTI2NjQ2IDIuNSw2NC41IEMgMi41LDcyLjQ1MDY1IC0wLjM5NjY5Nyw3OC4zNzk0MjUgLTIuMjUsODEuNTAwMDA1IHoiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY3NjY2Njc2MiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi40MDYyNSw0NC41IEwgLTAuNDA2MjUsNDYuOTM3NSBDIC0wLjQwNjI1LDQ2LjkzNzUgNS4yNSw1My45Mzc1NDkgNS4yNSw2NC41IEMgNS4yNSw3NS4wNjI0NTEgLTAuNDA2MjUsODIuMDYyNSAtMC40MDYyNSw4Mi4wNjI1IEwgLTIuNDA2MjUsODQuNSBMIDAuNzUsODQuNSBMIDE0Ljc1LDg0LjUgQyAxNy4xNTgwNzYsODQuNTAwMDAxIDIyLjQzOTY5OSw4NC41MjQ1MTQgMjguMzc1LDgyLjA5Mzc1IEMgMzQuMzEwMzAxLDc5LjY2Mjk4NiA0MC45MTE1MzYsNzQuNzUwNDg0IDQ2LjA2MjUsNjUuMjE4NzUgTCA0NC43NSw2NC41IEwgNDYuMDYyNSw2My43ODEyNSBDIDM1Ljc1OTM4Nyw0NC43MTU1OSAxOS41MDY1NzQsNDQuNSAxNC43NSw0NC41IEwgMC43NSw0NC41IEwgLTIuNDA2MjUsNDQuNSB6IE0gMy40Njg3NSw0Ny41IEwgMTQuNzUsNDcuNSBDIDE5LjQzNDE3Myw0Ny41IDMzLjAzNjg1LDQ3LjM2OTc5MyA0Mi43MTg3NSw2NC41IEMgMzcuOTUxOTY0LDcyLjkyOTA3NSAzMi4xOTc0NjksNzcuMTgzOTEgMjcsNzkuMzEyNSBDIDIxLjYzOTMzOSw4MS41MDc5MjQgMTcuMTU4MDc1LDgxLjUwMDAwMSAxNC43NSw4MS41IEwgMy41LDgxLjUgQyA1LjM3MzU4ODQsNzguMzkxNTY2IDguMjUsNzIuNDUwNjUgOC4yNSw2NC41IEMgOC4yNSw1Ni41MjY2NDYgNS4zNDE0Njg2LDUwLjU5OTgxNSAzLjQ2ODc1LDQ3LjUgeiIKICAgICAgICAgaWQ9InBhdGg0OTczIgogICAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjc2NjY2NzY2NjY2NjY2Njc2Njc2MiIC8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K"}}},{operation:function(a,b){return(!a||b)&&(a||!b)}}),joint.shapes.logic.Gate21.define("logic.Xnor",{attrs:{image:{"xlink:href":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IlhOT1IgQU5TSS5zdmciCiAgIGlua3NjYXBlOm91dHB1dF9leHRlbnNpb249Im9yZy5pbmtzY2FwZS5vdXRwdXQuc3ZnLmlua3NjYXBlIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCI+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxMCA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzE0IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIwLjUgOiAwLjMzMzMzMzMzIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MDYiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MTkiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzcyLjA0NzI0IDogMzUwLjc4NzM5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9Ijc0NC4wOTQ0OCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTI2LjE4MTA5IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3NzciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNzUgOiA0MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxNTAgOiA2MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNjAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMzI3NSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI1MCA6IDMzLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxMDAgOiA1MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlNTUzMyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzMiA6IDIxLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI2NCA6IDMyIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAzMiA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNTU3IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjI1IDogMTYuNjY2NjY3IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjUwIDogMjUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDI1IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgPC9kZWZzPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSI0IgogICAgIGlua3NjYXBlOmN4PSI5NS43MjM2NiIKICAgICBpbmtzY2FwZTpjeT0iLTI2Ljc3NTAyMyIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjAwMDAwMDI0O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDc4LjMzMzMzMiwyNSBDIDkxLjY2NjY2NiwyNSA5NSwyNSA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk4ODtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMC4zODU3MTcsMTUgTCA0Ljk5OTk5OTgsMTUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk3NjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMS4zNjIwOTEsMzUgTCA0Ljk5OTk5OTgsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8ZwogICAgICAgaWQ9ImcyNTYwIgogICAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNi41LC0zOS41KSI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMzUxNiIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjI1LDgxLjUwMDAwNSBDIC0zLjg0NzM3NCw4NC4xNDQ0MDUgLTQuNSw4NC41MDAwMDUgLTQuNSw4NC41MDAwMDUgTCAtOC4xNTYyNSw4NC41MDAwMDUgTCAtNi4xNTYyNSw4Mi4wNjI1MDUgQyAtNi4xNTYyNSw4Mi4wNjI1MDUgLTAuNSw3NS4wNjI0NTEgLTAuNSw2NC41IEMgLTAuNSw1My45Mzc1NDkgLTYuMTU2MjUsNDYuOTM3NSAtNi4xNTYyNSw0Ni45Mzc1IEwgLTguMTU2MjUsNDQuNSBMIC00LjUsNDQuNSBDIC0zLjcxODc1LDQ1LjQzNzUgLTMuMDc4MTI1LDQ2LjE1NjI1IC0yLjI4MTI1LDQ3LjUgQyAtMC40MDg1MzEsNTAuNTk5ODE1IDIuNSw1Ni41MjY2NDYgMi41LDY0LjUgQyAyLjUsNzIuNDUwNjUgLTAuMzk2Njk3LDc4LjM3OTQyNSAtMi4yNSw4MS41MDAwMDUgeiIKICAgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Njc2NjY2NzYyIgLz4KICAgICAgPHBhdGgKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjQwNjI1LDQ0LjUgTCAtMC40MDYyNSw0Ni45Mzc1IEMgLTAuNDA2MjUsNDYuOTM3NSA1LjI1LDUzLjkzNzU0OSA1LjI1LDY0LjUgQyA1LjI1LDc1LjA2MjQ1MSAtMC40MDYyNSw4Mi4wNjI1IC0wLjQwNjI1LDgyLjA2MjUgTCAtMi40MDYyNSw4NC41IEwgMC43NSw4NC41IEwgMTQuNzUsODQuNSBDIDE3LjE1ODA3Niw4NC41MDAwMDEgMjIuNDM5Njk5LDg0LjUyNDUxNCAyOC4zNzUsODIuMDkzNzUgQyAzNC4zMTAzMDEsNzkuNjYyOTg2IDQwLjkxMTUzNiw3NC43NTA0ODQgNDYuMDYyNSw2NS4yMTg3NSBMIDQ0Ljc1LDY0LjUgTCA0Ni4wNjI1LDYzLjc4MTI1IEMgMzUuNzU5Mzg3LDQ0LjcxNTU5IDE5LjUwNjU3NCw0NC41IDE0Ljc1LDQ0LjUgTCAwLjc1LDQ0LjUgTCAtMi40MDYyNSw0NC41IHogTSAzLjQ2ODc1LDQ3LjUgTCAxNC43NSw0Ny41IEMgMTkuNDM0MTczLDQ3LjUgMzMuMDM2ODUsNDcuMzY5NzkzIDQyLjcxODc1LDY0LjUgQyAzNy45NTE5NjQsNzIuOTI5MDc1IDMyLjE5NzQ2OSw3Ny4xODM5MSAyNyw3OS4zMTI1IEMgMjEuNjM5MzM5LDgxLjUwNzkyNCAxNy4xNTgwNzUsODEuNTAwMDAxIDE0Ljc1LDgxLjUgTCAzLjUsODEuNSBDIDUuMzczNTg4NCw3OC4zOTE1NjYgOC4yNSw3Mi40NTA2NSA4LjI1LDY0LjUgQyA4LjI1LDU2LjUyNjY0NiA1LjM0MTQ2ODYsNTAuNTk5ODE1IDMuNDY4NzUsNDcuNSB6IgogICAgICAgICBpZD0icGF0aDQ5NzMiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NzY2NjY3NjY2NjY2NjY2NzY2NzYyIgLz4KICAgIDwvZz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDM1NTEiCiAgICAgICBzb2RpcG9kaTpjeD0iNzUiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA3OSwyNSBBIDQsNCAwIDEgMSA3MSwyNSBBIDQsNCAwIDEgMSA3OSwyNSB6IiAvPgogIDwvZz4KPC9zdmc+Cg=="}}},{operation:function(a,b){return(!a||!b)&&(a||b)}}),joint.dia.Link.define("logic.Wire",{attrs:{".connection":{"stroke-width":2},".marker-vertex":{r:7}},router:{name:"orthogonal"},connector:{name:"rounded",args:{radius:10}}},{arrowheadMarkup:['','',""].join(""),vertexMarkup:['','','','','',"Remove vertex.","","",""].join("")}); -if("object"==typeof exports)var graphlib=require("graphlib"),dagre=require("dagre");graphlib=graphlib||"undefined"!=typeof window&&window.graphlib,dagre=dagre||"undefined"!=typeof window&&window.dagre,joint.layout.DirectedGraph={exportElement:function(a){return a.size()},exportLink:function(a){var b=a.get("labelSize")||{},c={minLen:a.get("minLen")||1,weight:a.get("weight")||1,labelpos:a.get("labelPosition")||"c",labeloffset:a.get("labelOffset")||0,width:b.width||0,height:b.height||0};return c},importElement:function(a,b,c){var d=this.getCell(b),e=c.node(b);a.setPosition?a.setPosition(d,e):d.set("position",{x:e.x-e.width/2,y:e.y-e.height/2})},importLink:function(a,b,c){var d=this.getCell(b.name),e=c.edge(b),f=e.points||[];if((a.setVertices||a.setLinkVertices)&&(joint.util.isFunction(a.setVertices)?a.setVertices(d,f):d.set("vertices",f.slice(1,f.length-1))),a.setLabels&&"x"in e&&"y"in e){var h={x:e.x,y:e.y};if(joint.util.isFunction(a.setLabels))a.setLabels(d,h,f);else{var i=g.Polyline(f),j=i.closestPointLength(h),k=i.pointAtLength(j),l=j/i.length();d.label(0,{position:{distance:l,offset:g.Point(h).difference(k).toJSON()}})}}},layout:function(a,b){var c;c=a instanceof joint.dia.Graph?a:(new joint.dia.Graph).resetCells(a,{dry:!0}),a=null,b=joint.util.defaults(b||{},{resizeClusters:!0,clusterPadding:10,exportElement:this.exportElement,exportLink:this.exportLink});var d=c.toGraphLib({directed:!0,multigraph:!0,compound:!0,setNodeLabel:b.exportElement,setEdgeLabel:b.exportLink,setEdgeName:function(a){return a.id}}),e={},f=b.marginX||0,h=b.marginY||0;if(b.rankDir&&(e.rankdir=b.rankDir),b.align&&(e.align=b.align),b.nodeSep&&(e.nodesep=b.nodeSep),b.edgeSep&&(e.edgesep=b.edgeSep),b.rankSep&&(e.ranksep=b.rankSep),b.ranker&&(e.ranker=b.ranker),f&&(e.marginx=f),h&&(e.marginy=h),d.setGraph(e),dagre.layout(d,{debugTiming:!!b.debugTiming}),c.startBatch("layout"),c.fromGraphLib(d,{importNode:this.importElement.bind(c,b),importEdge:this.importLink.bind(c,b)}),b.resizeClusters){var i=d.nodes().filter(function(a){return d.children(a).length>0}).map(c.getCell.bind(c)).sort(function(a,b){return b.getAncestors().length-a.getAncestors().length});joint.util.invoke(i,"fitEmbeds",{padding:b.clusterPadding})}c.stopBatch("layout");var j=d.graph();return g.Rect(f,h,Math.abs(j.width-2*f),Math.abs(j.height-2*h))},fromGraphLib:function(a,b){b=b||{};var c=b.importNode||joint.util.noop,d=b.importEdge||joint.util.noop,e=this instanceof joint.dia.Graph?this:new joint.dia.Graph;return a.nodes().forEach(function(d){c.call(e,d,a,e,b)}),a.edges().forEach(function(c){d.call(e,c,a,e,b)}),e},toGraphLib:function(a,b){b=b||{};for(var c=joint.util.pick(b,"directed","compound","multigraph"),d=new graphlib.Graph(c),e=b.setNodeLabel||joint.util.noop,f=b.setEdgeLabel||joint.util.noop,g=b.setEdgeName||joint.util.noop,h=a.get("cells"),i=0,j=h.length;i0}).map(c.getCell.bind(c)).sort(function(a,b){return b.getAncestors().length-a.getAncestors().length});joint.util.invoke(i,"fitEmbeds",{padding:b.clusterPadding})}c.stopBatch("layout");var j=d.graph();return g.Rect(f,h,Math.abs(j.width-2*f),Math.abs(j.height-2*h))},fromGraphLib:function(a,b){b=b||{};var c=b.importNode||joint.util.noop,d=b.importEdge||joint.util.noop,e=this instanceof joint.dia.Graph?this:new joint.dia.Graph;return a.nodes().forEach(function(d){c.call(e,d,a,e,b)}),a.edges().forEach(function(c){d.call(e,c,a,e,b)}),e},toGraphLib:function(a,b){b=b||{};for(var c=joint.util.pick(b,"directed","compound","multigraph"),d=new graphlib.Graph(c),e=b.setNodeLabel||joint.util.noop,f=b.setEdgeLabel||joint.util.noop,g=b.setEdgeName||joint.util.noop,h=a.get("cells"),i=0,j=h.length;i','','',"",'',""].join(""),initialize:function(){this.on("change:name change:attributes change:methods",function(){this.updateRectangles(),this.trigger("uml-update")},this),this.updateRectangles(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},getClassName:function(){return this.get("name")},updateRectangles:function(){var a=this.get("attrs"),b=[{type:"name",text:this.getClassName()},{type:"attrs",text:this.get("attributes")},{type:"methods",text:this.get("methods")}],c=0;b.forEach(function(b){var d=Array.isArray(b.text)?b.text:[b.text],e=20*d.length+20;a[".uml-class-"+b.type+"-text"].text=d.join("\n"),a[".uml-class-"+b.type+"-rect"].height=e,a[".uml-class-"+b.type+"-rect"].transform="translate(0,"+c+")",c+=e})}}),joint.shapes.uml.ClassView=joint.dia.ElementView.extend({},{initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"uml-update",function(){this.update(),this.resize()})}}),joint.shapes.uml.Class.define("uml.Abstract",{attrs:{".uml-class-name-rect":{fill:"#e74c3c"},".uml-class-attrs-rect":{fill:"#c0392b"},".uml-class-methods-rect":{fill:"#c0392b"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.AbstractView=joint.shapes.uml.ClassView,joint.shapes.uml.Class.define("uml.Interface",{attrs:{".uml-class-name-rect":{fill:"#f1c40f"},".uml-class-attrs-rect":{fill:"#f39c12"},".uml-class-methods-rect":{fill:"#f39c12"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.InterfaceView=joint.shapes.uml.ClassView,joint.dia.Link.define("uml.Generalization",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"}}}),joint.dia.Link.define("uml.Implementation",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"},".connection":{"stroke-dasharray":"3,3"}}}),joint.dia.Link.define("uml.Aggregation",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"white"}}}),joint.dia.Link.define("uml.Composition",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"black"}}}),joint.dia.Link.define("uml.Association"),joint.shapes.basic.Generic.define("uml.State",{attrs:{".uml-state-body":{width:200,height:200,rx:10,ry:10,fill:"#ecf0f1",stroke:"#bdc3c7","stroke-width":3},".uml-state-separator":{stroke:"#bdc3c7","stroke-width":2},".uml-state-name":{ref:".uml-state-body","ref-x":.5,"ref-y":5,"text-anchor":"middle",fill:"#000000","font-family":"Courier New","font-size":14},".uml-state-events":{ref:".uml-state-separator","ref-x":5,"ref-y":5,fill:"#000000","font-family":"Courier New","font-size":14}},name:"State",events:[]},{markup:['','','',"",'','','',""].join(""),initialize:function(){this.on({"change:name":this.updateName,"change:events":this.updateEvents,"change:size":this.updatePath},this),this.updateName(),this.updateEvents(),this.updatePath(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateName:function(){this.attr(".uml-state-name/text",this.get("name"))},updateEvents:function(){this.attr(".uml-state-events/text",this.get("events").join("\n"))},updatePath:function(){var a="M 0 20 L "+this.get("size").width+" 20";this.attr(".uml-state-separator/d",a,{silent:!0})}}),joint.shapes.basic.Circle.define("uml.StartState",{type:"uml.StartState",attrs:{circle:{fill:"#34495e",stroke:"#2c3e50","stroke-width":2,rx:1}}}),joint.shapes.basic.Generic.define("uml.EndState",{size:{width:20,height:20},attrs:{"circle.outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#2c3e50"},"circle.inner":{transform:"translate(10, 10)",r:6,fill:"#34495e"}}},{markup:''}),joint.dia.Link.define("uml.Transition",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z",fill:"#34495e",stroke:"#2c3e50"},".connection":{stroke:"#2c3e50"}}}); \ No newline at end of file +joint.shapes.basic.Generic.define("uml.Class",{attrs:{rect:{width:200},".uml-class-name-rect":{stroke:"black","stroke-width":2,fill:"#3498db"},".uml-class-attrs-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-methods-rect":{stroke:"black","stroke-width":2,fill:"#2980b9"},".uml-class-name-text":{ref:".uml-class-name-rect","ref-y":.5,"ref-x":.5,"text-anchor":"middle","y-alignment":"middle","font-weight":"bold",fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-attrs-text":{ref:".uml-class-attrs-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"},".uml-class-methods-text":{ref:".uml-class-methods-rect","ref-y":5,"ref-x":5,fill:"black","font-size":12,"font-family":"Times New Roman"}},name:[],attributes:[],methods:[]},{markup:['','','',"",'',""].join(""),initialize:function(){this.on("change:name change:attributes change:methods",function(){this.updateRectangles(),this.trigger("uml-update")},this),this.updateRectangles(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},getClassName:function(){return this.get("name")},updateRectangles:function(){var a=this.get("attrs"),b=[{type:"name",text:this.getClassName()},{type:"attrs",text:this.get("attributes")},{type:"methods",text:this.get("methods")}],c=0;b.forEach(function(b){var d=Array.isArray(b.text)?b.text:[b.text],e=20*d.length+20;a[".uml-class-"+b.type+"-text"].text=d.join("\n"),a[".uml-class-"+b.type+"-rect"].height=e,a[".uml-class-"+b.type+"-rect"].transform="translate(0,"+c+")",c+=e})}}),joint.shapes.uml.ClassView=joint.dia.ElementView.extend({initialize:function(){joint.dia.ElementView.prototype.initialize.apply(this,arguments),this.listenTo(this.model,"uml-update",function(){this.update(),this.resize()})}}),joint.shapes.uml.Class.define("uml.Abstract",{attrs:{".uml-class-name-rect":{fill:"#e74c3c"},".uml-class-attrs-rect":{fill:"#c0392b"},".uml-class-methods-rect":{fill:"#c0392b"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.AbstractView=joint.shapes.uml.ClassView,joint.shapes.uml.Class.define("uml.Interface",{attrs:{".uml-class-name-rect":{fill:"#f1c40f"},".uml-class-attrs-rect":{fill:"#f39c12"},".uml-class-methods-rect":{fill:"#f39c12"}}},{getClassName:function(){return["<>",this.get("name")]}}),joint.shapes.uml.InterfaceView=joint.shapes.uml.ClassView,joint.dia.Link.define("uml.Generalization",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"}}}),joint.dia.Link.define("uml.Implementation",{attrs:{".marker-target":{d:"M 20 0 L 0 10 L 20 20 z",fill:"white"},".connection":{"stroke-dasharray":"3,3"}}}),joint.dia.Link.define("uml.Aggregation",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"white"}}}),joint.dia.Link.define("uml.Composition",{attrs:{".marker-target":{d:"M 40 10 L 20 20 L 0 10 L 20 0 z",fill:"black"}}}),joint.dia.Link.define("uml.Association"),joint.shapes.basic.Generic.define("uml.State",{attrs:{".uml-state-body":{width:200,height:200,rx:10,ry:10,fill:"#ecf0f1",stroke:"#bdc3c7","stroke-width":3},".uml-state-separator":{stroke:"#bdc3c7","stroke-width":2},".uml-state-name":{ref:".uml-state-body","ref-x":.5,"ref-y":5,"text-anchor":"middle",fill:"#000000","font-family":"Courier New","font-size":14},".uml-state-events":{ref:".uml-state-separator","ref-x":5,"ref-y":5,fill:"#000000","font-family":"Courier New","font-size":14}},name:"State",events:[]},{markup:['','','',"",'','','',""].join(""),initialize:function(){this.on({"change:name":this.updateName,"change:events":this.updateEvents,"change:size":this.updatePath},this),this.updateName(),this.updateEvents(),this.updatePath(),joint.shapes.basic.Generic.prototype.initialize.apply(this,arguments)},updateName:function(){this.attr(".uml-state-name/text",this.get("name"))},updateEvents:function(){this.attr(".uml-state-events/text",this.get("events").join("\n"))},updatePath:function(){var a="M 0 20 L "+this.get("size").width+" 20";this.attr(".uml-state-separator/d",a,{silent:!0})}}),joint.shapes.basic.Circle.define("uml.StartState",{type:"uml.StartState",attrs:{circle:{fill:"#34495e",stroke:"#2c3e50","stroke-width":2,rx:1}}}),joint.shapes.basic.Generic.define("uml.EndState",{size:{width:20,height:20},attrs:{"circle.outer":{transform:"translate(10, 10)",r:10,fill:"#ffffff",stroke:"#2c3e50"},"circle.inner":{transform:"translate(10, 10)",r:6,fill:"#34495e"}}},{markup:''}),joint.dia.Link.define("uml.Transition",{attrs:{".marker-target":{d:"M 10 0 L 0 5 L 10 10 z",fill:"#34495e",stroke:"#2c3e50"},".connection":{stroke:"#2c3e50"}}}); \ No newline at end of file diff --git a/dist/vectorizer.js b/dist/vectorizer.js index aaab18740..296022c80 100644 --- a/dist/vectorizer.js +++ b/dist/vectorizer.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -39,7 +39,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. // A tiny library for making your life easier when dealing with SVG. // The only Vectorizer dependency is the Geometry library. - var V; var Vectorizer; @@ -66,11 +65,22 @@ V = Vectorizer = (function() { var ns = { xmlns: 'http://www.w3.org/2000/svg', xml: 'http://www.w3.org/XML/1998/namespace', - xlink: 'http://www.w3.org/1999/xlink' + xlink: 'http://www.w3.org/1999/xlink', + xhtml: 'http://www.w3.org/1999/xhtml' }; var SVGversion = '1.1'; + // Declare shorthands to the most used math functions. + var math = Math; + var PI = math.PI; + var atan2 = math.atan2; + var sqrt = math.sqrt; + var min = math.min; + var max = math.max; + var cos = math.cos; + var sin = math.sin; + var V = function(el, attrs, children) { // This allows using V() without the new keyword. @@ -138,11 +148,23 @@ V = Vectorizer = (function() { return this; }; + var VPrototype = V.prototype; + + Object.defineProperty(VPrototype, 'id', { + enumerable: true, + get: function() { + return this.node.id; + }, + set: function(id) { + this.node.id = id; + } + }); + /** * @param {SVGGElement} toElem * @returns {SVGMatrix} */ - V.prototype.getTransformToElement = function(toElem) { + VPrototype.getTransformToElement = function(toElem) { toElem = V.toNode(toElem); return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); }; @@ -152,7 +174,7 @@ V = Vectorizer = (function() { * @param {Object=} opt * @returns {Vectorizer|SVGMatrix} Setter / Getter */ - V.prototype.transform = function(matrix, opt) { + VPrototype.transform = function(matrix, opt) { var node = this.node; if (V.isUndefined(matrix)) { @@ -168,7 +190,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.translate = function(tx, ty, opt) { + VPrototype.translate = function(tx, ty, opt) { opt = opt || {}; ty = ty || 0; @@ -193,7 +215,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.rotate = function(angle, cx, cy, opt) { + VPrototype.rotate = function(angle, cx, cy, opt) { opt = opt || {}; @@ -219,7 +241,7 @@ V = Vectorizer = (function() { }; // Note that `scale` as the only transformation does not combine with previous values. - V.prototype.scale = function(sx, sy) { + VPrototype.scale = function(sx, sy) { sy = V.isUndefined(sy) ? sx : sy; @@ -243,7 +265,7 @@ V = Vectorizer = (function() { // 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) { + VPrototype.bbox = function(withoutTransformations, target) { var box; var node = this.node; @@ -252,7 +274,7 @@ V = Vectorizer = (function() { // 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); + return new g.Rect(0, 0, 0, 0); } try { @@ -271,21 +293,21 @@ V = Vectorizer = (function() { } if (withoutTransformations) { - return g.Rect(box); + return new g.Rect(box); } var matrix = this.getTransformToElement(target || ownerSVGElement); return V.transformRect(box, matrix); }; - + // Returns an SVGRect that contains coordinates and dimensions of the real bounding box, // i.e. after transformations are applied. // Fixes a browser implementation bug that returns incorrect bounding boxes for groups of svg elements. // Takes an (Object) `opt` argument (optional) with the following attributes: // (Object) `target` (optional): if not undefined, transform bounding boxes relative to `target`; if undefined, transform relative to this // (Boolean) `recursive` (optional): if true, recursively enter all groups and get a union of element bounding boxes (svg bbox fix); if false or undefined, return result of native function this.node.getBBox(); - V.prototype.getBBox = function(opt) { + VPrototype.getBBox = function(opt) { var options = {}; @@ -296,7 +318,7 @@ V = Vectorizer = (function() { // 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); + return new g.Rect(0, 0, 0, 0); } if (opt) { @@ -323,7 +345,7 @@ V = Vectorizer = (function() { if (!options.target) { // transform like this (that is, not at all) - return g.Rect(outputBBox); + return new g.Rect(outputBBox); } else { // transform like target var matrix = this.getTransformToElement(options.target); @@ -337,7 +359,7 @@ V = Vectorizer = (function() { var children = this.children(); var n = children.length; - + if (n === 0) { return this.getBBox({ target: options.target, recursive: false }); } @@ -376,187 +398,265 @@ V = Vectorizer = (function() { } }; - 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 eol = opt.eol; - var lines = content.split('\n'); - var tspan; - - // 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'); + // Text() helpers - // 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 + function createTextPathNode(attrs, vel) { + attrs || (attrs = {}); + var textPathElement = V('textPath'); + var d = attrs.d; + if (d && attrs['xlink:href'] === undefined) { + // If `opt.attrs` 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'); + // Wrap the text in the SVG element that points + // to a path defined by `opt.attrs` inside the `` element. + var linkedPath = V('path').attr('d', d).appendTo(vel.defs()); + textPathElement.attr('xlink:href', '#' + linkedPath.id); + } + if (V.isObject(attrs)) { // 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 = ((opt.x !== undefined) ? opt.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'; + textPathElement.attr(attrs); } + return textPathElement.node; + } - var firstLineHeight = 0; - for (var i = 0; i < lines.length; i++) { - - var vLineAttributes = { 'class': 'v-line' }; - if (i === 0) { - vLineAttributes.dy = '0em'; + function annotateTextLine(lineNode, lineAnnotations, opt) { + opt || (opt = {}); + var includeAnnotationIndices = opt.includeAnnotationIndices; + var eol = opt.eol; + var lineHeight = opt.lineHeight; + var baseSize = opt.baseSize; + var maxFontSize = 0; + var fontMetrics = {}; + var lastJ = lineAnnotations.length - 1; + for (var j = 0; j <= lastJ; j++) { + var annotation = lineAnnotations[j]; + var fontSize = null; + if (V.isObject(annotation)) { + var annotationAttrs = annotation.attrs; + var vTSpan = V('tspan', annotationAttrs); + var tspanNode = vTSpan.node; + var t = annotation.t; + if (eol && j === lastJ) t += eol; + tspanNode.textContent = t; + // Per annotation className + var annotationClass = annotationAttrs['class']; + if (annotationClass) vTSpan.addClass(annotationClass); + // 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. + if (includeAnnotationIndices) vTSpan.attr('annotations', annotation.annotations); + // Check for max font size + fontSize = parseFloat(annotationAttrs['font-size']); + if (fontSize === undefined) fontSize = baseSize; + if (fontSize && fontSize > maxFontSize) maxFontSize = fontSize; } else { - vLineAttributes.dy = lineHeight; - vLineAttributes.x = x; + if (eol && j === lastJ) annotation += eol; + tspanNode = document.createTextNode(annotation || ' '); + if (baseSize && baseSize > maxFontSize) maxFontSize = baseSize; } - var vLine = V('tspan', vLineAttributes); - - var lastI = lines.length - 1; - var line = lines[i]; - if (line) { - - // Get the line height based on the biggest font size in the annotations for this line. - var maxFontSize = 0; - if (opt.annotations) { - - // 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 }); - - var lastJ = lineAnnotations.length - 1; - for (var j = 0; j < lineAnnotations.length; j++) { + lineNode.appendChild(tspanNode); + } - var annotation = lineAnnotations[j]; - if (V.isObject(annotation)) { + if (maxFontSize) fontMetrics.maxFontSize = maxFontSize; + if (lineHeight) { + fontMetrics.lineHeight = lineHeight; + } else if (maxFontSize) { + fontMetrics.lineHeight = (maxFontSize * 1.2); + } + return fontMetrics; + } - var fontSize = parseFloat(annotation.attrs['font-size']); - if (fontSize && fontSize > maxFontSize) { - maxFontSize = fontSize; - } + var emRegex = /em$/; - 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']); - } + function convertEmToPx(em, fontSize) { + var numerical = parseFloat(em); + if (emRegex.test(em)) return numerical * fontSize; + return numerical; + } - if (eol && j === lastJ && i !== lastI) { - annotation.t += eol; - } - tspan.node.textContent = annotation.t; + function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) { + if (!Array.isArray(linesMetrics)) return 0; + var n = linesMetrics.length; + if (!n) return 0; + var lineMetrics = linesMetrics[0]; + var flMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var rLineHeights = 0; + var lineHeightPx = convertEmToPx(lineHeight, baseSizePx); + for (var i = 1; i < n; i++) { + lineMetrics = linesMetrics[i]; + var iLineHeight = convertEmToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx; + rLineHeights += iLineHeight; + } + var llMaxFont = convertEmToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; + var dy; + switch (alignment) { + case 'middle': + dy = (flMaxFont / 2) - (0.15 * llMaxFont) - (rLineHeights / 2); + break; + case 'bottom': + dy = -(0.25 * llMaxFont) - rLineHeights; + break; + default: + case 'top': + dy = (0.8 * flMaxFont) + break; + } + return dy; + } - } else { + VPrototype.text = function(content, opt) { - if (eol && j === lastJ && i !== lastI) { - annotation += eol; - } - tspan = document.createTextNode(annotation || ' '); - } - vLine.append(tspan); - } + if (content && typeof content !== 'string') throw new Error('Vectorizer: text() expects the first argument to be a string.'); - if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) { + // 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 = {}); - vLine.attr('dy', (maxFontSize * 1.2) + 'px'); - } + // End of Line character + var eol = opt.eol; + // Text along path + var textPath = opt.textPath + // Vertical shift + var verticalAnchor = opt.textVerticalAnchor; + var namedVerticalAnchor = (verticalAnchor === 'middle' || verticalAnchor === 'bottom' || verticalAnchor === 'top'); + // Horizontal shift applied to all the lines but the first. + var x = opt.x; + if (x === undefined) x = this.attr('x') || 0; + // Annotations + var iai = opt.includeAnnotationIndices; + var annotations = opt.annotations; + if (annotations && !V.isArray(annotations)) annotations = [annotations]; + // Shift all the but first by one line (`1em`) + var defaultLineHeight = opt.lineHeight; + var autoLineHeight = (defaultLineHeight === 'auto'); + var lineHeight = (autoLineHeight) ? '1.5em' : (defaultLineHeight || '1em'); + // Clearing the element + this.empty(); + this.attr({ + // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. + 'xml:space': 'preserve', + // 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. + 'display': (content) ? null : 'none' + }); + // Set default font-size if none + var fontSize = parseFloat(this.attr('font-size')); + if (!fontSize) { + fontSize = 16; + if (namedVerticalAnchor || annotations) this.attr('font-size', fontSize); + } + var doc = document; + var containerNode; + if (textPath) { + // Now all the ``s will be inside the ``. + if (typeof textPath === 'string') textPath = { d: textPath }; + containerNode = createTextPathNode(textPath, this); + } else { + containerNode = doc.createDocumentFragment(); + } + var offset = 0; + var lines = content.split('\n'); + var linesMetrics = []; + var annotatedY; + for (var i = 0, lastI = lines.length - 1; i <= lastI; i++) { + var dy = lineHeight; + var lineClassName = 'v-line'; + var lineNode = doc.createElementNS(V.namespace.xmlns, 'tspan'); + var line = lines[i]; + var lineMetrics; + if (line) { + if (annotations) { + // Find the *compacted* annotations for this line. + var lineAnnotations = V.annotateString(line, annotations, { + offset: -offset, + includeAnnotationIndices: iai + }); + lineMetrics = annotateTextLine(lineNode, lineAnnotations, { + includeAnnotationIndices: iai, + eol: (i !== lastI && eol), + lineHeight: (autoLineHeight) ? null : lineHeight, + baseSize: fontSize + }); + // Get the line height based on the biggest font size in the annotations for this line. + var iLineHeight = lineMetrics.lineHeight; + if (iLineHeight && autoLineHeight && i !== 0) dy = iLineHeight; + if (i === 0) annotatedY = lineMetrics.maxFontSize * 0.8; } else { - - if (eol && i !== lastI) { - line += eol; - } - - vLine.node.textContent = line; - } - - if (i === 0) { - firstLineHeight = maxFontSize; + if (eol && i !== lastI) line += eol; + lineNode.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'); + lineNode.textContent = '-'; + lineClassName += ' 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 = '-'; + var lineNodeStyle = lineNode.style; + lineNodeStyle.fillOpacity = 0; + lineNodeStyle.strokeOpacity = 0; + if (annotations) lineMetrics = {}; } - - V(textNode).append(vLine); - + if (lineMetrics) linesMetrics.push(lineMetrics); + if (i > 0) lineNode.setAttribute('dy', dy); + // Firefox requires 'x' to be set on the first line when inside a text path + if (i > 0 || textPath) lineNode.setAttribute('x', x); + lineNode.className.baseVal = lineClassName; + containerNode.appendChild(lineNode); offset += line.length + 1; // + 1 = newline character. } - - // `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 === null) { - this.attr('y', firstLineHeight || '0.8em'); + // Y Alignment calculation + if (namedVerticalAnchor) { + if (annotations) { + dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight); + } else if (verticalAnchor === 'top') { + // A shortcut for top alignment. It does not depend on font-size nor line-height + dy = '0.8em'; + } else { + var rh; // remaining height + if (lastI > 0) { + rh = parseFloat(lineHeight) || 1; + rh *= lastI; + if (!emRegex.test(lineHeight)) rh /= fontSize; + } else { + // Single-line text + rh = 0; + } + switch (verticalAnchor) { + case 'middle': + dy = (0.3 - (rh / 2)) + 'em' + break; + case 'bottom': + dy = (-rh - 0.3) + 'em' + break; + } + } + } else { + if (verticalAnchor === 0) { + dy = '0em'; + } else if (verticalAnchor) { + dy = verticalAnchor; + } else { + // No vertical anchor is defined + dy = 0; + // Backwards compatibility - we change the `y` attribute instead of `dy`. + if (this.attr('y') === null) this.attr('y', annotatedY || '0.8em'); + } } - + containerNode.firstChild.setAttribute('dy', dy); + // Appending lines to the element. + this.append(containerNode); return this; }; @@ -565,7 +665,7 @@ V = Vectorizer = (function() { * @param {string} name * @returns {Vectorizer} */ - V.prototype.removeAttr = function(name) { + VPrototype.removeAttr = function(name) { var qualifiedName = V.qualifyAttr(name); var el = this.node; @@ -580,7 +680,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.attr = function(name, value) { + VPrototype.attr = function(name, value) { if (V.isUndefined(name)) { @@ -615,7 +715,17 @@ V = Vectorizer = (function() { return this; }; - V.prototype.remove = function() { + VPrototype.normalizePath = function() { + + var tagName = this.tagName(); + if (tagName === 'PATH') { + this.attr('d', V.normalizePathData(this.attr('d'))); + } + + return this; + } + + VPrototype.remove = function() { if (this.node.parentNode) { this.node.parentNode.removeChild(this.node); @@ -624,7 +734,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.empty = function() { + VPrototype.empty = function() { while (this.node.firstChild) { this.node.removeChild(this.node.firstChild); @@ -638,7 +748,7 @@ V = Vectorizer = (function() { * @param {object} attrs * @returns {Vectorizer} */ - V.prototype.setAttributes = function(attrs) { + VPrototype.setAttributes = function(attrs) { for (var key in attrs) { if (attrs.hasOwnProperty(key)) { @@ -649,7 +759,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.append = function(els) { + VPrototype.append = function(els) { if (!V.isArray(els)) { els = [els]; @@ -662,13 +772,13 @@ V = Vectorizer = (function() { return this; }; - V.prototype.prepend = function(els) { + VPrototype.prepend = function(els) { var child = this.node.firstChild; return child ? V(child).before(els) : this.append(els); }; - V.prototype.before = function(els) { + VPrototype.before = function(els) { var node = this.node; var parent = node.parentNode; @@ -687,24 +797,29 @@ V = Vectorizer = (function() { return this; }; - V.prototype.appendTo = function(node) { + VPrototype.appendTo = function(node) { V.toNode(node).appendChild(this.node); return this; }, - V.prototype.svg = function() { + VPrototype.svg = function() { return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); }; - V.prototype.defs = function() { + VPrototype.tagName = function() { - var defs = this.svg().node.getElementsByTagName('defs'); + return this.node.tagName.toUpperCase(); + }; - return (defs && defs.length) ? V(defs[0]) : undefined; + VPrototype.defs = function() { + var context = this.svg() || this; + var defsNode = context.node.getElementsByTagName('defs')[0]; + if (defsNode) return V(defsNode); + return V('defs').appendTo(context); }; - V.prototype.clone = function() { + VPrototype.clone = function() { var clone = V(this.node.cloneNode(true/* deep */)); // Note that clone inherits also ID. Therefore, we need to change it here. @@ -712,13 +827,13 @@ V = Vectorizer = (function() { return clone; }; - V.prototype.findOne = function(selector) { + VPrototype.findOne = function(selector) { var found = this.node.querySelector(selector); return found ? V(found) : undefined; }; - V.prototype.find = function(selector) { + VPrototype.find = function(selector) { var vels = []; var nodes = this.node.querySelectorAll(selector); @@ -735,22 +850,22 @@ V = Vectorizer = (function() { }; // Returns an array of V elements made from children of this.node. - V.prototype.children = function() { + VPrototype.children = function() { var children = this.node.childNodes; - + var outputArray = []; for (var i = 0; i < children.length; i++) { var currentChild = children[i]; if (currentChild.nodeType === 1) { - outputArray.push(V(children[i])); + outputArray.push(V(children[i])); } } return outputArray; }; // Find an index of an element inside its container. - V.prototype.index = function() { + VPrototype.index = function() { var index = 0; var node = this.node.previousSibling; @@ -764,7 +879,7 @@ V = Vectorizer = (function() { return index; }; - V.prototype.findParentByClass = function(className, terminator) { + VPrototype.findParentByClass = function(className, terminator) { var ownerSVGElement = this.node.ownerSVGElement; var node = this.node.parentNode; @@ -783,7 +898,7 @@ V = Vectorizer = (function() { }; // https://jsperf.com/get-common-parent - V.prototype.contains = function(el) { + VPrototype.contains = function(el) { var a = this.node; var b = V.toNode(el); @@ -793,7 +908,7 @@ V = Vectorizer = (function() { }; // Convert global point into the coordinate space of this element. - V.prototype.toLocalPoint = function(x, y) { + VPrototype.toLocalPoint = function(x, y) { var svg = this.svg().node; @@ -815,7 +930,7 @@ V = Vectorizer = (function() { return globalPoint.matrixTransform(globalToLocalMatrix); }; - V.prototype.translateCenterToPoint = function(p) { + VPrototype.translateCenterToPoint = function(p) { var bbox = this.getBBox({ target: this.svg() }); var center = bbox.center(); @@ -829,7 +944,7 @@ V = Vectorizer = (function() { // 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) { + VPrototype.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 @@ -849,12 +964,12 @@ V = Vectorizer = (function() { // 2. Rotate around origin. var rotateAroundOrigin = svg.createSVGTransform(); - var angle = g.point(position).changeInAngle(position.x - reference.x, position.y - reference.y, reference); + var angle = (new 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); + var finalPosition = (new 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. @@ -882,7 +997,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.animateAlongPath = function(attrs, path) { + VPrototype.animateAlongPath = function(attrs, path) { path = V.toNode(path); @@ -920,12 +1035,12 @@ V = Vectorizer = (function() { return this; }; - V.prototype.hasClass = function(className) { + VPrototype.hasClass = function(className) { return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); }; - V.prototype.addClass = function(className) { + VPrototype.addClass = function(className) { if (!this.hasClass(className)) { var prevClasses = this.node.getAttribute('class') || ''; @@ -935,7 +1050,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.removeClass = function(className) { + VPrototype.removeClass = function(className) { if (this.hasClass(className)) { var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2'); @@ -945,7 +1060,7 @@ V = Vectorizer = (function() { return this; }; - V.prototype.toggleClass = function(className, toAdd) { + VPrototype.toggleClass = function(className, toAdd) { var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; @@ -964,7 +1079,7 @@ V = Vectorizer = (function() { // 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) { + VPrototype.sample = function(interval) { interval = interval || 1; var node = this.node; @@ -980,7 +1095,7 @@ V = Vectorizer = (function() { return samples; }; - V.prototype.convertToPath = function() { + VPrototype.convertToPath = function() { var path = V('path'); path.attr(this.attr()); @@ -991,9 +1106,9 @@ V = Vectorizer = (function() { return path; }; - V.prototype.convertToPathData = function() { + VPrototype.convertToPathData = function() { - var tagName = this.node.tagName.toUpperCase(); + var tagName = this.tagName(); switch (tagName) { case 'PATH': @@ -1015,6 +1130,56 @@ V = Vectorizer = (function() { throw new Error(tagName + ' cannot be converted to PATH.'); }; + V.prototype.toGeometryShape = function() { + var x, y, width, height, cx, cy, r, rx, ry, points, d; + switch (this.tagName()) { + + case 'RECT': + x = parseFloat(this.attr('x')) || 0; + y = parseFloat(this.attr('y')) || 0; + width = parseFloat(this.attr('width')) || 0; + height = parseFloat(this.attr('height')) || 0; + return new g.Rect(x, y, width, height); + + case 'CIRCLE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + r = parseFloat(this.attr('r')) || 0; + return new g.Ellipse({ x: cx, y: cy }, r, r); + + case 'ELLIPSE': + cx = parseFloat(this.attr('cx')) || 0; + cy = parseFloat(this.attr('cy')) || 0; + rx = parseFloat(this.attr('rx')) || 0; + ry = parseFloat(this.attr('ry')) || 0; + return new g.Ellipse({ x: cx, y: cy }, rx, ry); + + case 'POLYLINE': + points = V.getPointsFromSvgNode(this); + return new g.Polyline(points); + + case 'POLYGON': + points = V.getPointsFromSvgNode(this); + if (points.length > 1) points.push(points[0]); + return new g.Polyline(points); + + case 'PATH': + d = this.attr('d'); + if (!g.Path.isDataSupported(d)) d = V.normalizePathData(d); + return new g.Path(d); + + case 'LINE': + x1 = parseFloat(this.attr('x1')) || 0; + y1 = parseFloat(this.attr('y1')) || 0; + x2 = parseFloat(this.attr('x2')) || 0; + y2 = parseFloat(this.attr('y2')) || 0; + return new g.Line({ x: x1, y: y1 }, { x: x2, y: y2 }); + } + + // Anything else is a rectangle + return this.getBBox(); + }, + // 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. @@ -1022,7 +1187,7 @@ V = Vectorizer = (function() { // 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) { + VPrototype.findIntersection = function(ref, target) { var svg = this.svg().node; target = target || svg; @@ -1032,14 +1197,14 @@ V = Vectorizer = (function() { if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; var spot; - var tagName = this.node.localName.toUpperCase(); + var tagName = this.tagName(); // 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( + var gRect = new g.Rect( parseFloat(this.attr('x') || 0), parseFloat(this.attr('y') || 0), parseFloat(this.attr('width')), @@ -1054,7 +1219,7 @@ V = Vectorizer = (function() { 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); + spot = (new g.Rect(rect)).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation); } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { @@ -1071,7 +1236,7 @@ V = Vectorizer = (function() { // 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); + sample = new 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 @@ -1105,7 +1270,7 @@ V = Vectorizer = (function() { * @param {string} value * @returns {Vectorizer} */ - V.prototype.setAttribute = function(name, value) { + VPrototype.setAttribute = function(name, value) { var el = this.node; @@ -1147,13 +1312,16 @@ V = Vectorizer = (function() { }; V.toNode = function(el) { + return V.isV(el) ? el.node : (el.nodeName && el || el[0]); }; V.ensureId = function(node) { + node = V.toNode(node); return node.id || (node.id = V.uniqueId()); }; + // 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. This is used in the text() method but it is // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests @@ -1390,15 +1558,15 @@ V = Vectorizer = (function() { var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); // calculate skew - var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); - var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); + var skewX = ((180 / PI) * atan2(px.y, px.x) - 90); + var skewY = ((180 / PI) * atan2(py.y, py.x)); return { translateX: matrix.e, translateY: matrix.f, - scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), - scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), + scaleX: sqrt(matrix.a * matrix.a + matrix.b * matrix.b), + scaleY: sqrt(matrix.c * matrix.c + matrix.d * matrix.d), skewX: skewX, skewY: skewY, rotation: skewX // rotation is the same as skew x @@ -1419,8 +1587,8 @@ V = Vectorizer = (function() { a = d = 1; } return { - sx: b ? Math.sqrt(a * a + b * b) : a, - sy: c ? Math.sqrt(c * c + d * d) : d + sx: b ? sqrt(a * a + b * b) : a, + sy: c ? sqrt(c * c + d * d) : d }; }, @@ -1434,7 +1602,7 @@ V = Vectorizer = (function() { } return { - angle: g.normalizeAngle(g.toDeg(Math.atan2(p.y, p.x)) - 90) + angle: g.normalizeAngle(g.toDeg(atan2(p.y, p.x)) - 90) }; }, @@ -1510,19 +1678,36 @@ V = Vectorizer = (function() { p.y = r.y + r.height; var corner4 = p.matrixTransform(matrix); - var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x); - var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x); - var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y); - var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y); + var minX = min(corner1.x, corner2.x, corner3.x, corner4.x); + var maxX = max(corner1.x, corner2.x, corner3.x, corner4.x); + var minY = min(corner1.y, corner2.y, corner3.y, corner4.y); + var maxY = max(corner1.y, corner2.y, corner3.y, corner4.y); - return g.Rect(minX, minY, maxX - minX, maxY - minY); + return new g.Rect(minX, minY, maxX - minX, maxY - minY); }; V.transformPoint = function(p, matrix) { - return g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); + return new g.Point(V.createSVGPoint(p.x, p.y).matrixTransform(matrix)); }; + V.transformLine = function(l, matrix) { + + return new g.Line( + V.transformPoint(l.start, matrix), + V.transformPoint(l.end, matrix) + ); + }; + + V.transformPolyline = function(p, matrix) { + + var inPoints = (p instanceof g.Polyline) ? p.points : p; + if (!V.isArray(inPoints)) inPoints = []; + var outPoints = []; + for (var i = 0, n = inPoints.length; i < n; i++) outPoints[i] = V.transformPoint(inPoints[i], matrix); + return new g.Polyline(outPoints); + }, + // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to // an object (`{ fill: 'blue', stroke: 'red' }`). V.styleToObject = function(styleString) { @@ -1539,17 +1724,17 @@ V = Vectorizer = (function() { // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { - var svgArcMax = 2 * Math.PI - 1e-6; + var svgArcMax = 2 * PI - 1e-6; var r0 = innerRadius; var r1 = outerRadius; var a0 = startAngle; var a1 = endAngle; var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); - var df = da < Math.PI ? '0' : '1'; - var c0 = Math.cos(a0); - var s0 = Math.sin(a0); - var c1 = Math.cos(a1); - var s1 = Math.sin(a1); + var df = da < PI ? '0' : '1'; + var c0 = cos(a0); + var s0 = sin(a0); + var c1 = cos(a1); + var s1 = sin(a1); return (da >= svgArcMax) ? (r0 @@ -1750,27 +1935,23 @@ V = Vectorizer = (function() { V.convertPolygonToPathData = function(polygon) { - var points = V.getPointsFromSvgNode(V(polygon).node); - - if (!(points.length > 0)) return null; + var points = V.getPointsFromSvgNode(polygon); + if (points.length === 0) return null; return V.svgPointsToPath(points) + ' Z'; }; V.convertPolylineToPathData = function(polyline) { - var points = V.getPointsFromSvgNode(V(polyline).node); - - if (!(points.length > 0)) return null; + var points = V.getPointsFromSvgNode(polyline); + if (points.length === 0) return null; return V.svgPointsToPath(points); }; V.svgPointsToPath = function(points) { - var i; - - for (i = 0; i < points.length; i++) { + for (var i = 0, n = points.length; i < n; i++) { points[i] = points[i].x + ' ' + points[i].y; } @@ -1783,7 +1964,7 @@ V = Vectorizer = (function() { var points = []; var nodePoints = node.points; if (nodePoints) { - for (var i = 0; i < nodePoints.numberOfItems; i++) { + for (var i = 0, n = nodePoints.numberOfItems; i < n; i++) { points.push(nodePoints.getItem(i)); } } @@ -1791,7 +1972,7 @@ V = Vectorizer = (function() { return points; }; - V.KAPPA = 0.5522847498307935; + V.KAPPA = 0.551784; V.convertCircleToPathData = function(circle) { @@ -1859,10 +2040,10 @@ V = Vectorizer = (function() { var y = r.y; var width = r.width; var height = r.height; - var topRx = Math.min(r.rx || r['top-rx'] || 0, width / 2); - var bottomRx = Math.min(r.rx || r['bottom-rx'] || 0, width / 2); - var topRy = Math.min(r.ry || r['top-ry'] || 0, height / 2); - var bottomRy = Math.min(r.ry || r['bottom-ry'] || 0, height / 2); + var topRx = min(r.rx || r['top-rx'] || 0, width / 2); + var bottomRx = min(r.rx || r['bottom-rx'] || 0, width / 2); + var topRy = min(r.ry || r['top-ry'] || 0, height / 2); + var bottomRy = min(r.ry || r['bottom-ry'] || 0, height / 2); if (topRx || bottomRx || topRy || bottomRy) { d = [ @@ -1891,6 +2072,417 @@ V = Vectorizer = (function() { return d.join(' '); }; + // Take a path data string + // Return a normalized path data string + // If data cannot be parsed, return 'M 0 0' + // Adapted from Rappid normalizePath polyfill + // Highly inspired by Raphael Library (www.raphael.com) + V.normalizePathData = (function() { + + var spaces = '\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029'; + var pathCommand = new RegExp('([a-z])[' + spaces + ',]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?[' + spaces + ']*,?[' + spaces + ']*)+)', 'ig'); + var pathValues = new RegExp('(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)[' + spaces + ']*,?[' + spaces + ']*', 'ig'); + + var math = Math; + var PI = math.PI; + var sin = math.sin; + var cos = math.cos; + var tan = math.tan; + var asin = math.asin; + var sqrt = math.sqrt; + var abs = math.abs; + + function q2c(x1, y1, ax, ay, x2, y2) { + + var _13 = 1 / 3; + var _23 = 2 / 3; + return [(_13 * x1) + (_23 * ax), (_13 * y1) + (_23 * ay), (_13 * x2) + (_23 * ax), (_13 * y2) + (_23 * ay), x2, y2]; + } + + function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { + // for more information of where this math came from visit: + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + + var _120 = (PI * 120) / 180; + var rad = (PI / 180) * (+angle || 0); + var res = []; + var xy; + + var rotate = function(x, y, rad) { + + var X = (x * cos(rad)) - (y * sin(rad)); + var Y = (x * sin(rad)) + (y * cos(rad)); + return { x: X, y: Y }; + }; + + if (!recursive) { + xy = rotate(x1, y1, -rad); + x1 = xy.x; + y1 = xy.y; + + xy = rotate(x2, y2, -rad); + x2 = xy.x; + y2 = xy.y; + + var x = (x1 - x2) / 2; + var y = (y1 - y2) / 2; + var h = ((x * x) / (rx * rx)) + ((y * y) / (ry * ry)); + + if (h > 1) { + h = sqrt(h); + rx = h * rx; + ry = h * ry; + } + + var rx2 = rx * rx; + var ry2 = ry * ry; + + var k = ((large_arc_flag == sweep_flag) ? -1 : 1) * sqrt(abs(((rx2 * ry2) - (rx2 * y * y) - (ry2 * x * x)) / ((rx2 * y * y) + (ry2 * x * x)))); + + var cx = ((k * rx * y) / ry) + ((x1 + x2) / 2); + var cy = ((k * -ry * x) / rx) + ((y1 + y2) / 2); + + var f1 = asin(((y1 - cy) / ry).toFixed(9)); + var f2 = asin(((y2 - cy) / ry).toFixed(9)); + + f1 = ((x1 < cx) ? (PI - f1) : f1); + f2 = ((x2 < cx) ? (PI - f2) : f2); + + if (f1 < 0) f1 = (PI * 2) + f1; + if (f2 < 0) f2 = (PI * 2) + f2; + + if ((sweep_flag && f1) > f2) f1 = f1 - (PI * 2); + if ((!sweep_flag && f2) > f1) f2 = f2 - (PI * 2); + + } else { + f1 = recursive[0]; + f2 = recursive[1]; + cx = recursive[2]; + cy = recursive[3]; + } + + var df = f2 - f1; + + if (abs(df) > _120) { + var f2old = f2; + var x2old = x2; + var y2old = y2; + + f2 = f1 + (_120 * (((sweep_flag && f2) > f1) ? 1 : -1)); + x2 = cx + (rx * cos(f2)); + y2 = cy + (ry * sin(f2)); + + res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); + } + + df = f2 - f1; + + var c1 = cos(f1); + var s1 = sin(f1); + var c2 = cos(f2); + var s2 = sin(f2); + + var t = tan(df / 4); + + var hx = (4 / 3) * (rx * t); + var hy = (4 / 3) * (ry * t); + + var m1 = [x1, y1]; + var m2 = [x1 + (hx * s1), y1 - (hy * c1)]; + var m3 = [x2 + (hx * s2), y2 - (hy * c2)]; + var m4 = [x2, y2]; + + m2[0] = (2 * m1[0]) - m2[0]; + m2[1] = (2 * m1[1]) - m2[1]; + + if (recursive) { + return [m2, m3, m4].concat(res); + + } else { + res = [m2, m3, m4].concat(res).join().split(','); + + var newres = []; + var ii = res.length; + for (var i = 0; i < ii; i++) { + newres[i] = (i % 2) ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; + } + + return newres; + } + } + + function parsePathString(pathString) { + + if (!pathString) return null; + + var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 }; + var data = []; + + String(pathString).replace(pathCommand, function(a, b, c) { + + var params = []; + var name = b.toLowerCase(); + c.replace(pathValues, function(a, b) { + if (b) params.push(+b); + }); + + if ((name === 'm') && (params.length > 2)) { + data.push([b].concat(params.splice(0, 2))); + name = 'l'; + b = ((b === 'm') ? 'l' : 'L'); + } + + while (params.length >= paramCounts[name]) { + data.push([b].concat(params.splice(0, paramCounts[name]))); + if (!paramCounts[name]) break; + } + }); + + return data; + } + + function pathToAbsolute(pathArray) { + + if (!Array.isArray(pathArray) || !Array.isArray(pathArray && pathArray[0])) { // rough assumption + pathArray = parsePathString(pathArray); + } + + // if invalid string, return 'M 0 0' + if (!pathArray || !pathArray.length) return [['M', 0, 0]]; + + var res = []; + var x = 0; + var y = 0; + var mx = 0; + var my = 0; + var start = 0; + var pa0; + + var ii = pathArray.length; + for (var i = start; i < ii; i++) { + + var r = []; + res.push(r); + + var pa = pathArray[i]; + pa0 = pa[0]; + + if (pa0 != pa0.toUpperCase()) { + r[0] = pa0.toUpperCase(); + + var jj; + var j; + switch (r[0]) { + case 'A': + r[1] = pa[1]; + r[2] = pa[2]; + r[3] = pa[3]; + r[4] = pa[4]; + r[5] = pa[5]; + r[6] = +pa[6] + x; + r[7] = +pa[7] + y; + break; + + case 'V': + r[1] = +pa[1] + y; + break; + + case 'H': + r[1] = +pa[1] + x; + break; + + case 'M': + mx = +pa[1] + x; + my = +pa[2] + y; + + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + + default: + jj = pa.length; + for (j = 1; j < jj; j++) { + r[j] = +pa[j] + ((j % 2) ? x : y); + } + break; + } + } else { + var kk = pa.length; + for (var k = 0; k < kk; k++) { + r[k] = pa[k]; + } + } + + switch (r[0]) { + case 'Z': + x = +mx; + y = +my; + break; + + case 'H': + x = r[1]; + break; + + case 'V': + y = r[1]; + break; + + case 'M': + mx = r[r.length - 2]; + my = r[r.length - 1]; + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + + default: + x = r[r.length - 2]; + y = r[r.length - 1]; + break; + } + } + + return res; + } + + function normalize(path) { + + var p = pathToAbsolute(path); + var attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null }; + + function processPath(path, d, pcom) { + + var nx, ny; + + if (!path) return ['C', d.x, d.y, d.x, d.y, d.x, d.y]; + + if (!(path[0] in { T: 1, Q: 1 })) { + d.qx = null; + d.qy = null; + } + + switch (path[0]) { + case 'M': + d.X = path[1]; + d.Y = path[2]; + break; + + case 'A': + path = ['C'].concat(a2c.apply(0, [d.x, d.y].concat(path.slice(1)))); + break; + + case 'S': + if (pcom === 'C' || pcom === 'S') { // In 'S' case we have to take into account, if the previous command is C/S. + nx = (d.x * 2) - d.bx; // And reflect the previous + ny = (d.y * 2) - d.by; // command's control point relative to the current point. + } + else { // or some else or nothing + nx = d.x; + ny = d.y; + } + path = ['C', nx, ny].concat(path.slice(1)); + break; + + case 'T': + if (pcom === 'Q' || pcom === 'T') { // In 'T' case we have to take into account, if the previous command is Q/T. + d.qx = (d.x * 2) - d.qx; // And make a reflection similar + d.qy = (d.y * 2) - d.qy; // to case 'S'. + } + else { // or something else or nothing + d.qx = d.x; + d.qy = d.y; + } + path = ['C'].concat(q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); + break; + + case 'Q': + d.qx = path[1]; + d.qy = path[2]; + path = ['C'].concat(q2c(d.x, d.y, path[1], path[2], path[3], path[4])); + break; + + case 'H': + path = ['L'].concat(path[1], d.y); + break; + + case 'V': + path = ['L'].concat(d.x, path[1]); + break; + + // leave 'L' & 'Z' commands as they were: + + case 'L': + break; + + case 'Z': + break; + } + + return path; + } + + function fixArc(pp, i) { + + if (pp[i].length > 7) { + + pp[i].shift(); + var pi = pp[i]; + + while (pi.length) { + pcoms[i] = 'A'; // if created multiple 'C's, their original seg is saved + pp.splice(i++, 0, ['C'].concat(pi.splice(0, 6))); + } + + pp.splice(i, 1); + ii = p.length; + } + } + + var pcoms = []; // path commands of original path p + var pfirst = ''; // temporary holder for original path command + var pcom = ''; // holder for previous path command of original path + + var ii = p.length; + for (var i = 0; i < ii; i++) { + if (p[i]) pfirst = p[i][0]; // save current path command + + if (pfirst !== 'C') { // C is not saved yet, because it may be result of conversion + pcoms[i] = pfirst; // Save current path command + if (i > 0) pcom = pcoms[i - 1]; // Get previous path command pcom + } + + p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath + + if (pcoms[i] !== 'A' && pfirst === 'C') pcoms[i] = 'C'; // 'A' is the only command + // which may produce multiple 'C's + // so we have to make sure that 'C' is also 'C' in original path + + fixArc(p, i); // fixArc adds also the right amount of 'A's to pcoms + + var seg = p[i]; + var seglen = seg.length; + + attrs.x = seg[seglen - 2]; + attrs.y = seg[seglen - 1]; + + attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x; + attrs.by = parseFloat(seg[seglen - 3]) || attrs.y; + } + + // make sure normalized path data string starts with an M segment + if (!p[0][0] || p[0][0] !== 'M') { + p.unshift(['M', 0, 0]); + } + + return p; + } + + return function(pathData) { + return normalize(pathData).join(',').split(',').join(' '); + }; + })(); + V.namespace = ns; return V; diff --git a/dist/vectorizer.min.js b/dist/vectorizer.min.js index ff8ae607c..a1763096e 100644 --- a/dist/vectorizer.min.js +++ b/dist/vectorizer.min.js @@ -1,4 +1,4 @@ -/*! JointJS v2.0.1 (2017-11-15) - JavaScript diagramming library +/*! JointJS v2.1.0 (2018-04-26) - JavaScript diagramming library This Source Code Form is subject to the terms of the Mozilla Public @@ -33,7 +33,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. }(this, function(g) { -var V,Vectorizer;V=Vectorizer=function(){"use strict";var a="object"==typeof window&&!(!window.SVGAngle&&!document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure","1.1"));if(!a)return function(){throw new Error("SVG is required to use Vectorizer.")};var b={xmlns:"http://www.w3.org/2000/svg",xml:"http://www.w3.org/XML/1998/namespace",xlink:"http://www.w3.org/1999/xlink"},c="1.1",d=function(a,c,e){if(!(this instanceof d))return d.apply(Object.create(d.prototype),arguments);if(a){if(d.isV(a)&&(a=a.node),c=c||{},d.isString(a)){if("svg"===a.toLowerCase())a=d.createSvgDocument();else if("<"===a[0]){var f=d.createSvgDocument(a);if(f.childNodes.length>1){var g,h,i=[];for(g=0,h=f.childNodes.length;gu&&(u=z),c=d("tspan",y.attrs),b.includeAnnotationIndices&&c.attr("annotations",y.annotations),y.attrs.class&&c.addClass(y.attrs.class),e&&x===w&&p!==s&&(y.t+=e),c.node.textContent=y.t}else e&&x===w&&p!==s&&(y+=e),c=document.createTextNode(y||" ");r.append(c)}"auto"===b.lineHeight&&u&&0!==p&&r.attr("dy",1.2*u+"px")}else e&&p!==s&&(t+=e),r.node.textContent=t;0===p&&(o=u)}else r.addClass("v-empty-line"),r.node.style.fillOpacity=0,r.node.style.strokeOpacity=0,r.node.textContent="-";d(g).append(r),l+=t.length+1}var A=this.attr("y");return null===A&&this.attr("y",o||"0.8em"),this},d.prototype.removeAttr=function(a){var b=d.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},d.prototype.attr=function(a,b){if(d.isUndefined(a)){for(var c=this.node.attributes,e={},f=0;f'+(a||"")+"",f=d.parseXML(e,{async:!1});return f.documentElement},d.idCounter=0,d.uniqueId=function(){return"v-"+ ++d.idCounter},d.toNode=function(a){return d.isV(a)?a.node:a.nodeName&&a||a[0]},d.ensureId=function(a){return a=d.toNode(a),a.id||(a.id=d.uniqueId())},d.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},d.isUndefined=function(a){return"undefined"==typeof a},d.isString=function(a){return"string"==typeof a},d.isObject=function(a){return a&&"object"==typeof a},d.isArray=Array.isArray,d.parseXML=function(a,b){b=b||{};var c;try{var e=new DOMParser;d.isUndefined(b.async)||(e.async=b.async),c=e.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},d.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var c=a.split(":");return{ns:b[c[0]],local:c[1]}}return{ns:null,local:a}},d.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,d.transformSeparatorRegex=/[ ,]+/,d.transformationListRegex=/^(\w+)\((.*)\)/,d.transformStringToMatrix=function(a){var b=d.createSVGMatrix(),c=a&&a.match(d.transformRegex);if(!c)return b;for(var e=0,f=c.length;e=0){var g=d.transformStringToMatrix(a),h=d.decomposeMatrix(g);b=[h.translateX,h.translateY],e=[h.scaleX,h.scaleY],c=[h.rotation];var i=[];0===b[0]&&0===b[0]||i.push("translate("+b+")"),1===e[0]&&1===e[1]||i.push("scale("+e+")"),0!==c[0]&&i.push("rotate("+c+")"),a=i.join(" ")}else{var j=a.match(/translate\((.*?)\)/);j&&(b=j[1].split(f));var k=a.match(/rotate\((.*?)\)/);k&&(c=k[1].split(f));var l=a.match(/scale\((.*?)\)/);l&&(e=l[1].split(f))}}var m=e&&e[0]?parseFloat(e[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:m,sy:e&&e[1]?parseFloat(e[1]):m}}},d.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},d.decomposeMatrix=function(a){var b=d.deltaTransformPoint(a,{x:0,y:1}),c=d.deltaTransformPoint(a,{x:1,y:0}),e=180/Math.PI*Math.atan2(b.y,b.x)-90,f=180/Math.PI*Math.atan2(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:Math.sqrt(a.a*a.a+a.b*a.b),scaleY:Math.sqrt(a.c*a.c+a.d*a.d),skewX:e,skewY:f,rotation:e}},d.matrixToScale=function(a){var b,c,e,f;return a?(b=d.isUndefined(a.a)?1:a.a,f=d.isUndefined(a.d)?1:a.d,c=a.b,e=a.c):b=f=1,{sx:c?Math.sqrt(b*b+c*c):b,sy:e?Math.sqrt(e*e+f*f):f}},d.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=d.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(Math.atan2(b.y,b.x))-90)}},d.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},d.isV=function(a){return a instanceof d},d.isVElement=d.isV;var e=d("svg").node;return d.createSVGMatrix=function(a){var b=e.createSVGMatrix();for(var c in a)b[c]=a[c];return b},d.createSVGTransform=function(a){return d.isUndefined(a)?e.createSVGTransform():(a instanceof SVGMatrix||(a=d.createSVGMatrix(a)),e.createSVGTransformFromMatrix(a))},d.createSVGPoint=function(a,b){var c=e.createSVGPoint();return c.x=a,c.y=b,c},d.transformRect=function(a,b){var c=e.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var f=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var h=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var i=c.matrixTransform(b),j=Math.min(d.x,f.x,h.x,i.x),k=Math.max(d.x,f.x,h.x,i.x),l=Math.min(d.y,f.y,h.y,i.y),m=Math.max(d.y,f.y,h.y,i.y);return g.Rect(j,l,k-j,m-l)},d.transformPoint=function(a,b){return g.Point(d.createSVGPoint(a.x,a.y).matrixTransform(b))},d.styleToObject=function(a){for(var b={},c=a.split(";"),d=0;d=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L"+f*n+","+f*o+"A"+f+","+f+" 0 "+k+",0 "+f*l+","+f*m+"Z":"M"+g*l+","+g*m+"A"+g+","+g+" 0 "+k+",1 "+g*n+","+g*o+"L0,0Z"},d.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?d.isObject(a[c])&&d.isObject(b[c])?a[c]=d.mergeAttrs(a[c],b[c]):d.isObject(a[c])?a[c]=d.mergeAttrs(a[c],d.styleToObject(b[c])):d.isObject(b[c])?a[c]=d.mergeAttrs(d.styleToObject(a[c]),b[c]):a[c]=d.mergeAttrs(d.styleToObject(a[c]),d.styleToObject(b[c])):a[c]=b[c];return a},d.annotateString=function(a,b,c){b=b||[],c=c||{};for(var e,f,g,h=c.offset||0,i=[],j=[],k=0;k=n&&k=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},d.convertLineToPathData=function(a){a=d(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},d.convertPolygonToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b)+" Z":null},d.convertPolylineToPathData=function(a){var b=d.getPointsFromSvgNode(d(a).node);return b.length>0?d.svgPointsToPath(b):null},d.svgPointsToPath=function(a){var b;for(b=0;bh&&(h=m)}else e&&k===j&&(l+=e),p=document.createTextNode(l||" "),g&&g>h&&(h=g);a.appendChild(p)}return h&&(i.maxFontSize=h),f?i.lineHeight=f:h&&(i.lineHeight=1.2*h),i}function c(a,b){var c=parseFloat(a);return s.test(a)?c*b:c}function d(a,b,d,e){if(!Array.isArray(b))return 0;var f=b.length;if(!f)return 0;for(var g=b[0],h=c(g.maxFontSize,d)||d,i=0,j=c(e,d),k=1;k1){var e,g,h=[];for(e=0,g=d.childNodes.length;e0&&D.setAttribute("dy",B),(y>0||g)&&D.setAttribute("x",j),D.className.baseVal=C,r.appendChild(D),v+=E.length+1}if(i)if(l)B=d(h,x,p,o);else if("top"===h)B="0.8em";else{var I;switch(z>0?(I=parseFloat(o)||1,I*=z,s.test(o)||(I/=p)):I=0,h){case"middle":B=.3-I/2+"em";break;case"bottom":B=-I-.3+"em"}}else 0===h?B="0em":h?B=h:(B=0,null===this.attr("y")&&this.attr("y",u||"0.8em"));return r.firstChild.setAttribute("dy",B),this.append(r),this},r.removeAttr=function(a){var b=q.qualifyAttr(a),c=this.node;return b.ns?c.hasAttributeNS(b.ns,b.local)&&c.removeAttributeNS(b.ns,b.local):c.hasAttribute(a)&&c.removeAttribute(a),this},r.attr=function(a,b){if(q.isUndefined(a)){for(var c=this.node.attributes,d={},e=0;e1&&k.push(k[0]),new g.Polyline(k);case"PATH":return l=this.attr("d"),g.Path.isDataSupported(l)||(l=q.normalizePathData(l)),new g.Path(l);case"LINE":return x1=parseFloat(this.attr("x1"))||0,y1=parseFloat(this.attr("y1"))||0,x2=parseFloat(this.attr("x2"))||0,y2=parseFloat(this.attr("y2"))||0,new g.Line({x:x1,y:y1},{x:x2,y:y2})}return this.getBBox()},r.findIntersection=function(a,b){var c=this.svg().node;b=b||c;var d=this.getBBox({target:b}),e=d.center();if(d.intersectionWithLineFromCenterToPoint(a)){var f,h=this.tagName();if("RECT"===h){var i=new g.Rect(parseFloat(this.attr("x")||0),parseFloat(this.attr("y")||0),parseFloat(this.attr("width")),parseFloat(this.attr("height"))),j=this.getTransformToElement(b),k=q.decomposeMatrix(j),l=c.createSVGTransform();l.setRotate(-k.rotation,e.x,e.y);var m=q.transformRect(i,l.matrix.multiply(j));f=new g.Rect(m).intersectionWithLineFromCenterToPoint(a,k.rotation)}else if("PATH"===h||"POLYGON"===h||"POLYLINE"===h||"CIRCLE"===h||"ELLIPSE"===h){var n,o,p,r,s,t,u="PATH"===h?this:this.convertToPath(),v=u.sample(),w=1/0,x=[];for(n=0;n'+(a||"")+"",c=q.parseXML(b,{async:!1});return c.documentElement},q.idCounter=0,q.uniqueId=function(){return"v-"+ ++q.idCounter},q.toNode=function(a){return q.isV(a)?a.node:a.nodeName&&a||a[0]},q.ensureId=function(a){return a=q.toNode(a),a.id||(a.id=q.uniqueId())},q.sanitizeText=function(a){return(a||"").replace(/ /g,"\xa0")},q.isUndefined=function(a){return"undefined"==typeof a},q.isString=function(a){return"string"==typeof a},q.isObject=function(a){return a&&"object"==typeof a},q.isArray=Array.isArray,q.parseXML=function(a,b){b=b||{};var c;try{var d=new DOMParser;q.isUndefined(b.async)||(d.async=b.async),c=d.parseFromString(a,"text/xml")}catch(a){c=void 0}if(!c||c.getElementsByTagName("parsererror").length)throw new Error("Invalid XML: "+a);return c},q.qualifyAttr=function(a){if(a.indexOf(":")!==-1){var b=a.split(":");return{ns:f[b[0]],local:b[1]}}return{ns:null,local:a}},q.transformRegex=/(\w+)\(([^,)]+),?([^)]+)?\)/gi,q.transformSeparatorRegex=/[ ,]+/,q.transformationListRegex=/^(\w+)\((.*)\)/,q.transformStringToMatrix=function(a){var b=q.createSVGMatrix(),c=a&&a.match(q.transformRegex);if(!c)return b;for(var d=0,e=c.length;d=0){var f=q.transformStringToMatrix(a),g=q.decomposeMatrix(f);b=[g.translateX,g.translateY],d=[g.scaleX,g.scaleY],c=[g.rotation];var h=[];0===b[0]&&0===b[0]||h.push("translate("+b+")"),1===d[0]&&1===d[1]||h.push("scale("+d+")"),0!==c[0]&&h.push("rotate("+c+")"),a=h.join(" ")}else{var i=a.match(/translate\((.*?)\)/);i&&(b=i[1].split(e));var j=a.match(/rotate\((.*?)\)/);j&&(c=j[1].split(e));var k=a.match(/scale\((.*?)\)/);k&&(d=k[1].split(e))}}var l=d&&d[0]?parseFloat(d[0]):1;return{value:a,translate:{tx:b&&b[0]?parseInt(b[0],10):0,ty:b&&b[1]?parseInt(b[1],10):0},rotate:{angle:c&&c[0]?parseInt(c[0],10):0,cx:c&&c[1]?parseInt(c[1],10):void 0,cy:c&&c[2]?parseInt(c[2],10):void 0},scale:{sx:l,sy:d&&d[1]?parseFloat(d[1]):l}}},q.deltaTransformPoint=function(a,b){var c=b.x*a.a+b.y*a.c+0,d=b.x*a.b+b.y*a.d+0;return{x:c,y:d}},q.decomposeMatrix=function(a){var b=q.deltaTransformPoint(a,{x:0,y:1}),c=q.deltaTransformPoint(a,{x:1,y:0}),d=180/j*k(b.y,b.x)-90,e=180/j*k(c.y,c.x);return{translateX:a.e,translateY:a.f,scaleX:l(a.a*a.a+a.b*a.b),scaleY:l(a.c*a.c+a.d*a.d),skewX:d,skewY:e,rotation:d}},q.matrixToScale=function(a){var b,c,d,e;return a?(b=q.isUndefined(a.a)?1:a.a,e=q.isUndefined(a.d)?1:a.d,c=a.b,d=a.c):b=e=1,{sx:c?l(b*b+c*c):b,sy:d?l(d*d+e*e):e}},q.matrixToRotate=function(a){var b={x:0,y:1};return a&&(b=q.deltaTransformPoint(a,b)),{angle:g.normalizeAngle(g.toDeg(k(b.y,b.x))-90)}},q.matrixToTranslate=function(a){return{tx:a&&a.e||0,ty:a&&a.f||0}},q.isV=function(a){return a instanceof q},q.isVElement=q.isV;var t=q("svg").node;return q.createSVGMatrix=function(a){var b=t.createSVGMatrix();for(var c in a)b[c]=a[c];return b},q.createSVGTransform=function(a){return q.isUndefined(a)?t.createSVGTransform():(a instanceof SVGMatrix||(a=q.createSVGMatrix(a)),t.createSVGTransformFromMatrix(a))},q.createSVGPoint=function(a,b){var c=t.createSVGPoint();return c.x=a,c.y=b,c},q.transformRect=function(a,b){var c=t.createSVGPoint();c.x=a.x,c.y=a.y;var d=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y;var e=c.matrixTransform(b);c.x=a.x+a.width,c.y=a.y+a.height;var f=c.matrixTransform(b);c.x=a.x,c.y=a.y+a.height;var h=c.matrixTransform(b),i=m(d.x,e.x,f.x,h.x),j=n(d.x,e.x,f.x,h.x),k=m(d.y,e.y,f.y,h.y),l=n(d.y,e.y,f.y,h.y);return new g.Rect(i,k,j-i,l-k)},q.transformPoint=function(a,b){return new g.Point(q.createSVGPoint(a.x,a.y).matrixTransform(b))},q.transformLine=function(a,b){return new g.Line(q.transformPoint(a.start,b),q.transformPoint(a.end,b))},q.transformPolyline=function(a,b){var c=a instanceof g.Polyline?a.points:a;q.isArray(c)||(c=[]);for(var d=[],e=0,f=c.length;e=e?f?"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"M0,"+f+"A"+f+","+f+" 0 1,0 0,"+-f+"A"+f+","+f+" 0 1,0 0,"+f+"Z":"M0,"+g+"A"+g+","+g+" 0 1,1 0,"+-g+"A"+g+","+g+" 0 1,1 0,"+g+"Z":f?"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L"+f*q+","+f*r+"A"+f+","+f+" 0 "+l+",0 "+f*m+","+f*n+"Z":"M"+g*m+","+g*n+"A"+g+","+g+" 0 "+l+",1 "+g*q+","+g*r+"L0,0Z"},q.mergeAttrs=function(a,b){for(var c in b)"class"===c?a[c]=a[c]?a[c]+" "+b[c]:b[c]:"style"===c?q.isObject(a[c])&&q.isObject(b[c])?a[c]=q.mergeAttrs(a[c],b[c]):q.isObject(a[c])?a[c]=q.mergeAttrs(a[c],q.styleToObject(b[c])):q.isObject(b[c])?a[c]=q.mergeAttrs(q.styleToObject(a[c]),b[c]):a[c]=q.mergeAttrs(q.styleToObject(a[c]),q.styleToObject(b[c])):a[c]=b[c];return a},q.annotateString=function(a,b,c){b=b||[],c=c||{};for(var d,e,f,g=c.offset||0,h=[],i=[],j=0;j=m&&j=a.start&&ba.start&&c<=a.end||a.start>=b&&a.end=b?a.end+=c:a.start>=b&&(a.start+=c,a.end+=c)}),a},q.convertLineToPathData=function(a){a=q(a);var b=["M",a.attr("x1"),a.attr("y1"),"L",a.attr("x2"),a.attr("y2")].join(" ");return b},q.convertPolygonToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)+" Z"},q.convertPolylineToPathData=function(a){var b=q.getPointsFromSvgNode(a);return 0===b.length?null:q.svgPointsToPath(b)},q.svgPointsToPath=function(a){for(var b=0,c=a.length;b1&&(z=o(z),d*=z,e*=z);var A=d*d,B=e*e,C=(g==h?-1:1)*o(p((A*B-A*y*y-B*x*x)/(A*y*y+B*x*x))),D=C*d*y/e+(a+i)/2,E=C*-e*x/d+(c+q)/2,F=n(((c-E)/e).toFixed(9)),G=n(((q-E)/e).toFixed(9));F=aG&&(F-=2*j),(!h&&G)>F&&(G-=2*j)}var H=G-F;if(p(H)>t){var I=G,J=i,K=q;G=F+t*((h&&G)>F?1:-1),i=D+d*l(G),q=E+e*k(G),v=b(i,q,d,e,f,0,h,J,K,[G,I,D,E])}H=G-F;var L=l(F),M=k(F),N=l(G),O=k(G),P=m(H/4),Q=4/3*(d*P),R=4/3*(e*P),S=[a,c],T=[a+Q*M,c-R*L],U=[i+Q*O,q-R*N],V=[i,q];if(T[0]=2*S[0]-T[0],T[1]=2*S[1]-T[1],r)return[T,U,V].concat(v);v=[T,U,V].concat(v).join().split(",");for(var W=[],X=v.length,Y=0;Y2&&(c.push([d].concat(f.splice(0,2))),g="l",d="m"===d?"l":"L");f.length>=b[g]&&(c.push([d].concat(f.splice(0,b[g]))),b[g]););}),c}function d(a){if(Array.isArray(a)&&Array.isArray(a&&a[0])||(a=c(a)),!a||!a.length)return[["M",0,0]];for(var b,d=[],e=0,f=0,g=0,h=0,i=0,j=a.length,k=i;k7){a[b].shift();for(var c=a[b];c.length;)i[b]="A",a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),l=g.length}}for(var g=d(c),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i=[],j="",k="",l=g.length,m=0;m0&&(k=i[m-1])),g[m]=e(g[m],h,k),"A"!==i[m]&&"C"===j&&(i[m]="C"),f(g,m);var n=g[m],o=n.length;h.x=n[o-2],h.y=n[o-1],h.bx=parseFloat(n[o-4])||h.x,h.by=parseFloat(n[o-3])||h.y}return g[0][0]&&"M"===g[0][0]||g.unshift(["M",0,0]),g}var f="\t\n\v\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029",g=new RegExp("([a-z])["+f+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+f+"]*,?["+f+"]*)+)","ig"),h=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+f+"]*,?["+f+"]*","ig"),i=Math,j=i.PI,k=i.sin,l=i.cos,m=i.tan,n=i.asin,o=i.sqrt,p=i.abs;return function(a){return e(a).join(",").split(",").join(" ")}}(),q.namespace=f,q}(); return V; diff --git a/package-lock.json b/package-lock.json index 20b620dd0..c60e970f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jointjs", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1d9d3d9f8..664e3077d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "test": "grunt test" }, - "version": "2.0.1", + "version": "2.1.0", "main": "./dist/joint.min.js", "style": "./dist/joint.min.css", "types": "./dist/joint.d.ts", @@ -21,11 +21,10 @@ "url": "http://client.io" }, "contributors": [ - "David Durman (https://github.com/DavidDurman)", - "Roman Bruckner (https://github.com/kumilingus)", - "Charles Hill (https://github.com/chill117)", - "Vladimir Talas (https://github.com/vtalas)", - "Zbynek Stara (https://github.com/zbynekstara)" + "David Durman (https://github.com/DavidDurman)", + "Roman Bruckner (https://github.com/kumilingus)", + "Vladimir Talas (https://github.com/vtalas)", + "Zbynek Stara (https://github.com/zbynekstara)" ], "repository": { "type": "git",