/** * Copyright (c) 2006-2015, JGraph Ltd * Copyright (c) 2006-2015, Gaudenz Alder */ /** * Class: mxArrowConnector * * Extends to implement an new rounded arrow shape with support for * waypoints and double arrows. (The shape is used to represent edges, not * vertices.) This shape is registered under * in . * * Constructor: mxArrowConnector * * Constructs a new arrow shape. * * Parameters: * * points - Array of that define the points. This is stored in * . * fill - String that defines the fill color. This is stored in . * stroke - String that defines the stroke color. This is stored in . * strokewidth - Optional integer that defines the stroke width. Default is * 1. This is stored in . * arrowWidth - Optional integer that defines the arrow width. Default is * . This is stored in . * spacing - Optional integer that defines the spacing between the arrow shape * and its endpoints. Default is . This is stored in * . * endSize - Optional integer that defines the size of the arrowhead. Default * is . This is stored in . */ function mxArrowConnector(points, fill, stroke, strokewidth, arrowWidth, spacing, endSize) { mxShape.call(this); this.points = points; this.fill = fill; this.stroke = stroke; this.strokewidth = (strokewidth != null) ? strokewidth : 1; this.arrowWidth = (arrowWidth != null) ? arrowWidth : mxConstants.ARROW_WIDTH; this.arrowSpacing = (spacing != null) ? spacing : mxConstants.ARROW_SPACING; this.startSize = mxConstants.ARROW_SIZE / 5; this.endSize = mxConstants.ARROW_SIZE / 5; }; /** * Extends mxShape. */ mxUtils.extend(mxArrowConnector, mxShape); /** * Variable: useSvgBoundingBox * * Allows to use the SVG bounding box in SVG. Default is false for performance * reasons. */ mxArrowConnector.prototype.useSvgBoundingBox = true; /** * Function: isRoundable * * Hook for subclassers. */ mxArrowConnector.prototype.isRoundable = function() { return true; }; /** * Variable: resetStyles * * Overrides mxShape to reset spacing. */ mxArrowConnector.prototype.resetStyles = function() { mxShape.prototype.resetStyles.apply(this, arguments); this.arrowSpacing = mxConstants.ARROW_SPACING; }; /** * Overrides apply to get smooth transition from default start- and endsize. */ mxArrowConnector.prototype.apply = function(state) { mxShape.prototype.apply.apply(this, arguments); if (this.style != null) { this.startSize = mxUtils.getNumber(this.style, mxConstants.STYLE_STARTSIZE, mxConstants.ARROW_SIZE / 5) * 3; this.endSize = mxUtils.getNumber(this.style, mxConstants.STYLE_ENDSIZE, mxConstants.ARROW_SIZE / 5) * 3; } }; /** * Function: augmentBoundingBox * * Augments the bounding box with the edge width and markers. */ mxArrowConnector.prototype.augmentBoundingBox = function(bbox) { mxShape.prototype.augmentBoundingBox.apply(this, arguments); var w = this.getEdgeWidth(); if (this.isMarkerStart()) { w = Math.max(w, this.getStartArrowWidth()); } if (this.isMarkerEnd()) { w = Math.max(w, this.getEndArrowWidth()); } bbox.grow((w / 2 + this.strokewidth) * this.scale); }; /** * Function: paintEdgeShape * * Paints the line shape. */ mxArrowConnector.prototype.paintEdgeShape = function(c, pts) { // Geometry of arrow var strokeWidth = this.strokewidth; if (this.outline) { strokeWidth = Math.max(1, mxUtils.getNumber(this.style, mxConstants.STYLE_STROKEWIDTH, this.strokewidth)); } var startWidth = this.getStartArrowWidth() + strokeWidth; var endWidth = this.getEndArrowWidth() + strokeWidth; var edgeWidth = this.outline ? this.getEdgeWidth() + strokeWidth : this.getEdgeWidth(); var openEnded = this.isOpenEnded(); var markerStart = this.isMarkerStart(); var markerEnd = this.isMarkerEnd(); var spacing = (openEnded) ? 0 : this.arrowSpacing + strokeWidth / 2; var startSize = this.startSize + strokeWidth; var endSize = this.endSize + strokeWidth; var isRounded = this.isArrowRounded(); // Base vector (between first points) var pe = pts[pts.length - 1]; // Finds first non-overlapping point var i0 = 1; while (i0 < pts.length - 1 && pts[i0].x == pts[0].x && pts[i0].y == pts[0].y) { i0++; } var dx = pts[i0].x - pts[0].x; var dy = pts[i0].y - pts[0].y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist == 0) { return; } // Computes the norm and the inverse norm var nx = dx / dist; var nx2, nx1 = nx; var ny = dy / dist; var ny2, ny1 = ny; var orthx = edgeWidth * ny; var orthy = -edgeWidth * nx; // Stores the inbound function calls in reverse order in fns var fns = []; if (isRounded) { c.setLineJoin('round'); } else if (pts.length > 2) { // Only mitre if there are waypoints c.setMiterLimit(1.42); } c.begin(); var startNx = nx; var startNy = ny; if (markerStart && !openEnded) { this.paintMarker(c, pts[0].x, pts[0].y, nx, ny, startSize, startWidth, edgeWidth, spacing, true); } else { var outStartX = pts[0].x + orthx / 2 + spacing * nx; var outStartY = pts[0].y + orthy / 2 + spacing * ny; var inEndX = pts[0].x - orthx / 2 + spacing * nx; var inEndY = pts[0].y - orthy / 2 + spacing * ny; if (openEnded) { c.moveTo(outStartX, outStartY); fns.push(function() { c.lineTo(inEndX, inEndY); }); } else { c.moveTo(inEndX, inEndY); c.lineTo(outStartX, outStartY); } } var dx1 = 0; var dy1 = 0; var dist1 = 0; for (var i = 0; i < pts.length - 2; i++) { // Work out in which direction the line is bending var pos = mxUtils.relativeCcw(pts[i].x, pts[i].y, pts[i+1].x, pts[i+1].y, pts[i+2].x, pts[i+2].y); dx1 = pts[i+2].x - pts[i+1].x; dy1 = pts[i+2].y - pts[i+1].y; dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); if (dist1 != 0) { nx1 = dx1 / dist1; ny1 = dy1 / dist1; var tmp1 = nx * nx1 + ny * ny1; var tmp = Math.max(Math.sqrt((tmp1 + 1) / 2), 0.04); // Work out the normal orthogonal to the line through the control point and the edge sides intersection nx2 = (nx + nx1); ny2 = (ny + ny1); var dist2 = Math.sqrt(nx2 * nx2 + ny2 * ny2); if (dist2 != 0) { nx2 = nx2 / dist2; ny2 = ny2 / dist2; // Higher strokewidths require a larger minimum bend, 0.35 covers all but the most extreme cases var strokeWidthFactor = Math.max(tmp, Math.min(this.strokewidth / 200 + 0.04, 0.35)); var angleFactor = (pos != 0 && isRounded) ? Math.max(0.1, strokeWidthFactor) : Math.max(tmp, 0.06); var outX = pts[i+1].x + ny2 * edgeWidth / 2 / angleFactor; var outY = pts[i+1].y - nx2 * edgeWidth / 2 / angleFactor; var inX = pts[i+1].x - ny2 * edgeWidth / 2 / angleFactor; var inY = pts[i+1].y + nx2 * edgeWidth / 2 / angleFactor; if (pos == 0 || !isRounded) { // If the two segments are aligned, or if we're not drawing curved sections between segments // just draw straight to the intersection point c.lineTo(outX, outY); (function(x, y) { fns.push(function() { c.lineTo(x, y); }); })(inX, inY); } else if (pos == -1) { var c1x = inX + ny * edgeWidth; var c1y = inY - nx * edgeWidth; var c2x = inX + ny1 * edgeWidth; var c2y = inY - nx1 * edgeWidth; c.lineTo(c1x, c1y); c.quadTo(outX, outY, c2x, c2y); (function(x, y) { fns.push(function() { c.lineTo(x, y); }); })(inX, inY); } else { c.lineTo(outX, outY); (function(x, y) { var c1x = outX - ny * edgeWidth; var c1y = outY + nx * edgeWidth; var c2x = outX - ny1 * edgeWidth; var c2y = outY + nx1 * edgeWidth; fns.push(function() { c.quadTo(x, y, c1x, c1y); }); fns.push(function() { c.lineTo(c2x, c2y); }); })(inX, inY); } nx = nx1; ny = ny1; } } } orthx = edgeWidth * ny1; orthy = - edgeWidth * nx1; if (markerEnd && !openEnded) { this.paintMarker(c, pe.x, pe.y, -nx, -ny, endSize, endWidth, edgeWidth, spacing, false); } else { c.lineTo(pe.x - spacing * nx1 + orthx / 2, pe.y - spacing * ny1 + orthy / 2); var inStartX = pe.x - spacing * nx1 - orthx / 2; var inStartY = pe.y - spacing * ny1 - orthy / 2; if (!openEnded) { c.lineTo(inStartX, inStartY); } else { c.moveTo(inStartX, inStartY); fns.splice(0, 0, function() { c.moveTo(inStartX, inStartY); }); } } for (var i = fns.length - 1; i >= 0; i--) { fns[i](); } if (openEnded) { c.end(); c.stroke(); } else { c.close(); c.fillAndStroke(); } // Workaround for shadow on top of base arrow c.setShadow(false); // Need to redraw the markers without the low miter limit c.setMiterLimit(4); if (isRounded) { c.setLineJoin('flat'); } if (pts.length > 2) { // Only to repaint markers if no waypoints // Need to redraw the markers without the low miter limit c.setMiterLimit(4); if (markerStart && !openEnded) { c.begin(); this.paintMarker(c, pts[0].x, pts[0].y, startNx, startNy, startSize, startWidth, edgeWidth, spacing, true); c.stroke(); c.end(); } if (markerEnd && !openEnded) { c.begin(); this.paintMarker(c, pe.x, pe.y, -nx, -ny, endSize, endWidth, edgeWidth, spacing, true); c.stroke(); c.end(); } } }; /** * Function: paintMarker * * Paints the marker. */ mxArrowConnector.prototype.paintMarker = function(c, ptX, ptY, nx, ny, size, arrowWidth, edgeWidth, spacing, initialMove) { var widthArrowRatio = edgeWidth / arrowWidth; var orthx = edgeWidth * ny / 2; var orthy = -edgeWidth * nx / 2; var spaceX = (spacing + size) * nx; var spaceY = (spacing + size) * ny; if (initialMove) { c.moveTo(ptX - orthx + spaceX, ptY - orthy + spaceY); } else { c.lineTo(ptX - orthx + spaceX, ptY - orthy + spaceY); } c.lineTo(ptX - orthx / widthArrowRatio + spaceX, ptY - orthy / widthArrowRatio + spaceY); c.lineTo(ptX + spacing * nx, ptY + spacing * ny); c.lineTo(ptX + orthx / widthArrowRatio + spaceX, ptY + orthy / widthArrowRatio + spaceY); c.lineTo(ptX + orthx + spaceX, ptY + orthy + spaceY); } /** * Function: isArrowRounded * * Returns wether the arrow is rounded */ mxArrowConnector.prototype.isArrowRounded = function() { return this.isRounded; }; /** * Function: getStartArrowWidth * * Returns the width of the start arrow */ mxArrowConnector.prototype.getStartArrowWidth = function() { return mxConstants.ARROW_WIDTH; }; /** * Function: getEndArrowWidth * * Returns the width of the end arrow */ mxArrowConnector.prototype.getEndArrowWidth = function() { return mxConstants.ARROW_WIDTH; }; /** * Function: getEdgeWidth * * Returns the width of the body of the edge */ mxArrowConnector.prototype.getEdgeWidth = function() { return mxConstants.ARROW_WIDTH / 3; }; /** * Function: isOpenEnded * * Returns whether the ends of the shape are drawn */ mxArrowConnector.prototype.isOpenEnded = function() { return false; }; /** * Function: isMarkerStart * * Returns whether the start marker is drawn */ mxArrowConnector.prototype.isMarkerStart = function() { return (mxUtils.getValue(this.style, mxConstants.STYLE_STARTARROW, mxConstants.NONE) != mxConstants.NONE); }; /** * Function: isMarkerEnd * * Returns whether the end marker is drawn */ mxArrowConnector.prototype.isMarkerEnd = function() { return (mxUtils.getValue(this.style, mxConstants.STYLE_ENDARROW, mxConstants.NONE) != mxConstants.NONE); };