blob: 2abae891246048280555db6e36c389cdfe3aad77 [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxArrowConnector
*
* Extends <mxShape> 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 <mxConstants.SHAPE_ARROW_CONNECTOR>
* in <mxCellRenderer>.
*
* Constructor: mxArrowConnector
*
* Constructs a new arrow shape.
*
* Parameters:
*
* points - Array of <mxPoints> that define the points. This is stored in
* <mxShape.points>.
* fill - String that defines the fill color. This is stored in <fill>.
* stroke - String that defines the stroke color. This is stored in <stroke>.
* strokewidth - Optional integer that defines the stroke width. Default is
* 1. This is stored in <strokewidth>.
* arrowWidth - Optional integer that defines the arrow width. Default is
* <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>.
* spacing - Optional integer that defines the spacing between the arrow shape
* and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in
* <spacing>.
* endSize - Optional integer that defines the size of the arrowhead. Default
* is <mxConstants.ARROW_SIZE>. This is stored in <endSize>.
*/
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;
/**
* 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;
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: paintEdgeShape
*
* Paints the line shape.
*/
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);
};