blob: 3622cd86a0a177204a8b9a0686f4325828c7db9e [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxStencil
*
* Implements a generic shape which is based on a XML node as a description.
*
* shape:
*
* The outer element is *shape*, that has attributes:
*
* - "name", string, required. The stencil name that uniquely identifies the shape.
* - "w" and "h" are optional decimal view bounds. This defines your co-ordinate
* system for the graphics operations in the shape. The default is 100,100.
* - "aspect", optional string. Either "variable", the default, or "fixed". Fixed
* means always render the shape with the aspect ratio defined by the ratio w/h.
* Variable causes the ratio to match that of the geometry of the current vertex.
* - "strokewidth", optional string. Either an integer or the string "inherit".
* "inherit" indicates that the strokeWidth of the cell is only changed on scaling,
* not on resizing. Default is "1".
* If numeric values are used, the strokeWidth of the cell is changed on both
* scaling and resizing and the value defines the multiple that is applied to
* the width.
*
* connections:
*
* If you want to define specific fixed connection points on the shape use the
* *connections* element. Each *constraint* element within connections defines
* a fixed connection point on the shape. Constraints have attributes:
*
* - "perimeter", required. 1 or 0. 0 sets the connection point where specified
* by x,y. 1 Causes the position of the connection point to be extrapolated from
* the center of the shape, through x,y to the point of intersection with the
* perimeter of the shape.
* - "x" and "y" are the position of the fixed point relative to the bounds of
* the shape. They can be automatically adjusted if perimeter=1. So, (0,0) is top
* left, (0.5,0.5) the center, (1,0.5) the center of the right hand edge of the
* bounds, etc. Values may be less than 0 or greater than 1 to be positioned
* outside of the shape.
* - "name", optional string. A unique identifier for the port on the shape.
*
* background and foreground:
*
* The path of the graphics drawing is split into two elements, *foreground* and
* *background*. The split is to define which part any shadow applied to the shape
* is derived from (the background). This, generally, means the background is the
* line tracing of the outside of the shape, but not always.
*
* Any stroke, fill or fillstroke of a background must be the first element of the
* foreground element, they must not be used within *background*. If the background
* is empty, this is not required.
*
* Because the background cannot have any fill or stroke, it can contain only one
* *path*, *rect*, *roundrect* or *ellipse* element (or none). It can also not
* include *image*, *text* or *include-shape*.
*
* Note that the state, styling and drawing in mxGraph stencils is very close in
* design to that of HTML 5 canvas. Tutorials on this subject, if you're not
* familiar with the topic, will give a good high-level introduction to the
* concepts used.
*
* State:
*
* Rendering within the foreground and background elements has the concept of
* state. There are two types of operations other than state save/load, styling
* and drawing. The styling operations change the current state, so you can save
* the current state with <save/> and pull the last saved state from the state
* stack using <restore/>.
*
* Styling:
*
* The elements that change colors within the current state all take a hash
* prefixed hex color code ("#FFEA80").
*
* - *strokecolor*, this sets the color that drawing paths will be rendered in
* when a stroke or fillstroke command is issued.
* - *fillcolor*, this sets the color that the inside of closed paths will be
* rendered in when a fill or fillstroke command is issued.
* - *fontcolor*, this sets the color that fonts are rendered in when text is drawn.
*
* *alpha* defines the degree of transparency used between 1.0 for fully opaque
* and 0.0 for fully transparent.
*
* *strokewidth* defines the integer thickness of drawing elements rendered by
* stroking. Use fixed="1" to apply the value as-is, without scaling.
*
* *dashed* is "1" for dashing enabled and "0" for disabled.
*
* When *dashed* is enabled the current dash pattern, defined by *dashpattern*,
* is used on strokes. dashpattern is a sequence of space separated "on, off"
* lengths that define what distance to paint the stroke for, then what distance
* to paint nothing for, repeat... The default is "3 3". You could define a more
* complex pattern with "5 3 2 6", for example. Generally, it makes sense to have
* an even number of elements in the dashpattern, but that's not required.
*
* *linejoin*, *linecap* and *miterlimit* are best explained by the Mozilla page
* on Canvas styling (about halfway down). The values are all the same except we
* use "flat" for linecap, instead of Canvas' "butt".
*
* For font styling there are.
*
* - *fontsize*, an integer,
* - *fontstyle*, an ORed bit pattern of bold (1), italic (2) and underline (4),
* i.e bold underline is "5".
* - *fontfamily*, is a string defining the typeface to be used.
*
* Drawing:
*
* Most drawing is contained within a *path* element. Again, the graphic
* primitives are very similar to that of HTML 5 canvas.
*
* - *move* to attributes required decimals (x,y).
* - *line* to attributes required decimals (x,y).
* - *quad* to required decimals (x2,y2) via control point required decimals
* (x1,y1).
* - *curve* to required decimals (x3,y3), via control points required decimals
* (x1,y1) and (x2,y2).
* - *arc*, this doesn't follow the HTML Canvas signatures, instead it's a copy
* of the SVG arc command. The SVG specification documentation gives the best
* description of its behaviors. The attributes are named identically, they are
* decimals and all required.
* - *close* ends the current subpath and causes an automatic straight line to
* be drawn from the current point to the initial point of the current subpath.
*
* Complex drawing:
*
* In addition to the graphics primitive operations there are non-primitive
* operations. These provide an easy method to draw some basic shapes.
*
* - *rect*, attributes "x", "y", "w", "h", all required decimals
* - *roundrect*, attributes "x", "y", "w", "h", all required decimals. Also
* "arcsize" an optional decimal attribute defining how large, the corner curves
* are.
* - *ellipse*, attributes "x", "y", "w", "h", all required decimals.
*
* Note that these 3 shapes and all paths must be followed by either a fill,
* stroke, or fillstroke.
*
* Text:
*
* *text* elements have the following attributes.
*
* - "str", the text string to display, required.
* - "x" and "y", the decimal location (x,y) of the text element, required.
* - "align", the horizontal alignment of the text element, either "left",
* "center" or "right". Optional, default is "left".
* - "valign", the vertical alignment of the text element, either "top", "middle"
* or "bottom". Optional, default is "top".
* - "localized", 0 or 1, if 1 then the "str" actually contains a key to use to
* fetch the value out of mxResources. Optional, default is
* <mxStencil.defaultLocalized>.
* - "vertical", 0 or 1, if 1 the label is rendered vertically (rotated by 90
* degrees). Optional, default is 0.
* - "rotation", angle in degrees (0 to 360). The angle to rotate the text by.
* Optional, default is 0.
* - "align-shape", 0 or 1, if 0 ignore the rotation of the shape when setting
* the text rotation. Optional, default is 1.
*
* If <allowEval> is true, then the text content of the this element can define
* a function which is invoked with the shape as the only argument and returns
* the value for the text element (ignored if the str attribute is not null).
*
* Images:
*
* *image* elements can either be external URLs, or data URIs, where supported
* (not in IE 7-). Attributes are:
*
* - "src", required string. Either a data URI or URL.
* - "x", "y", required decimals. The (x,y) position of the image.
* - "w", "h", required decimals. The width and height of the image.
* - "flipH" and "flipV", optional 0 or 1. Whether to flip the image along the
* horizontal/vertical axis. Default is 0 for both.
*
* If <allowEval> is true, then the text content of the this element can define
* a function which is invoked with the shape as the only argument and returns
* the value for the image source (ignored if the src attribute is not null).
*
* Sub-shapes:
*
* *include-shape* allow stencils to be rendered within the current stencil by
* referencing the sub-stencil by name. Attributes are:
*
* - "name", required string. The unique shape name of the stencil.
* - "x", "y", "w", "h", required decimals. The (x,y) position of the sub-shape
* and its width and height.
*
* Constructor: mxStencil
*
* Constructs a new generic shape by setting <desc> to the given XML node and
* invoking <parseDescription> and <parseConstraints>.
*
* Parameters:
*
* desc - XML node that contains the stencil description.
*/
function mxStencil(desc)
{
this.desc = desc;
this.parseDescription();
this.parseConstraints();
};
/**
* Variable: defaultLocalized
*
* Static global variable that specifies the default value for the localized
* attribute of the text element. Default is false.
*/
mxStencil.defaultLocalized = false;
/**
* Function: allowEval
*
* Static global switch that specifies if the use of eval is allowed for
* evaluating text content and images. Default is false. Set this to true
* if stencils can not contain user input.
*/
mxStencil.allowEval = false;
/**
* Variable: desc
*
* Holds the XML node with the stencil description.
*/
mxStencil.prototype.desc = null;
/**
* Variable: constraints
*
* Holds an array of <mxConnectionConstraints> as defined in the shape.
*/
mxStencil.prototype.constraints = null;
/**
* Variable: aspect
*
* Holds the aspect of the shape. Default is 'auto'.
*/
mxStencil.prototype.aspect = null;
/**
* Variable: w0
*
* Holds the width of the shape. Default is 100.
*/
mxStencil.prototype.w0 = null;
/**
* Variable: h0
*
* Holds the height of the shape. Default is 100.
*/
mxStencil.prototype.h0 = null;
/**
* Variable: bgNodes
*
* Holds the XML node with the stencil description.
*/
mxStencil.prototype.bgNode = null;
/**
* Variable: fgNodes
*
* Holds the XML node with the stencil description.
*/
mxStencil.prototype.fgNode = null;
/**
* Variable: strokewidth
*
* Holds the strokewidth direction from the description.
*/
mxStencil.prototype.strokewidth = null;
/**
* Function: parseDescription
*
* Reads <w0>, <h0>, <aspect>, <bgNodes> and <fgNodes> from <desc>.
*/
mxStencil.prototype.parseDescription = function()
{
// LATER: Preprocess nodes for faster painting
this.fgNode = this.desc.getElementsByTagName('foreground')[0];
this.bgNode = this.desc.getElementsByTagName('background')[0];
this.w0 = Number(this.desc.getAttribute('w') || 100);
this.h0 = Number(this.desc.getAttribute('h') || 100);
// Possible values for aspect are: variable and fixed where
// variable means fill the available space and fixed means
// use w0 and h0 to compute the aspect.
var aspect = this.desc.getAttribute('aspect');
this.aspect = (aspect != null) ? aspect : 'variable';
// Possible values for strokewidth are all numbers and "inherit"
// where the inherit means take the value from the style (ie. the
// user-defined stroke-width). Note that the strokewidth is scaled
// by the minimum scaling that is used to draw the shape (sx, sy).
var sw = this.desc.getAttribute('strokewidth');
this.strokewidth = (sw != null) ? sw : '1';
};
/**
* Function: parseConstraints
*
* Reads the constraints from <desc> into <constraints> using
* <parseConstraint>.
*/
mxStencil.prototype.parseConstraints = function()
{
var conns = this.desc.getElementsByTagName('connections')[0];
if (conns != null)
{
var tmp = mxUtils.getChildNodes(conns);
if (tmp != null && tmp.length > 0)
{
this.constraints = [];
for (var i = 0; i < tmp.length; i++)
{
this.constraints.push(this.parseConstraint(tmp[i]));
}
}
}
};
/**
* Function: parseConstraint
*
* Parses the given XML node and returns its <mxConnectionConstraint>.
*/
mxStencil.prototype.parseConstraint = function(node)
{
var x = Number(node.getAttribute('x'));
var y = Number(node.getAttribute('y'));
var perimeter = node.getAttribute('perimeter') == '1';
var name = node.getAttribute('name');
return new mxConnectionConstraint(new mxPoint(x, y), perimeter, name);
};
/**
* Function: evaluateTextAttribute
*
* Gets the given attribute as a text. The return value from <evaluateAttribute>
* is used as a key to <mxResources.get> if the localized attribute in the text
* node is 1 or if <defaultLocalized> is true.
*/
mxStencil.prototype.evaluateTextAttribute = function(node, attribute, shape)
{
var result = this.evaluateAttribute(node, attribute, shape);
var loc = node.getAttribute('localized');
if ((mxStencil.defaultLocalized && loc == null) || loc == '1')
{
result = mxResources.get(result);
}
return result;
};
/**
* Function: evaluateAttribute
*
* Gets the attribute for the given name from the given node. If the attribute
* does not exist then the text content of the node is evaluated and if it is
* a function it is invoked with <shape> as the only argument and the return
* value is used as the attribute value to be returned.
*/
mxStencil.prototype.evaluateAttribute = function(node, attribute, shape)
{
var result = node.getAttribute(attribute);
if (result == null)
{
var text = mxUtils.getTextContent(node);
if (text != null && mxStencil.allowEval)
{
var funct = mxUtils.eval(text);
if (typeof(funct) == 'function')
{
result = funct(shape);
}
}
}
return result;
};
/**
* Function: drawShape
*
* Draws this stencil inside the given bounds.
*/
mxStencil.prototype.drawShape = function(canvas, shape, x, y, w, h)
{
// TODO: Internal structure (array of special structs?), relative and absolute
// coordinates (eg. note shape, process vs star, actor etc.), text rendering
// and non-proportional scaling, how to implement pluggable edge shapes
// (start, segment, end blocks), pluggable markers, how to implement
// swimlanes (title area) with this API, add icon, horizontal/vertical
// label, indicator for all shapes, rotation
var direction = mxUtils.getValue(shape.style, mxConstants.STYLE_DIRECTION, null);
var aspect = this.computeAspect(shape.style, x, y, w, h, direction);
var minScale = Math.min(aspect.width, aspect.height);
var sw = (this.strokewidth == 'inherit') ?
Number(mxUtils.getNumber(shape.style, mxConstants.STYLE_STROKEWIDTH, 1)) :
Number(this.strokewidth) * minScale;
canvas.setStrokeWidth(sw);
this.drawChildren(canvas, shape, x, y, w, h, this.bgNode, aspect, false);
this.drawChildren(canvas, shape, x, y, w, h, this.fgNode, aspect, true);
};
/**
* Function: drawChildren
*
* Draws this stencil inside the given bounds.
*/
mxStencil.prototype.drawChildren = function(canvas, shape, x, y, w, h, node, aspect, disableShadow)
{
if (node != null && w > 0 && h > 0)
{
var tmp = node.firstChild;
while (tmp != null)
{
if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT)
{
this.drawNode(canvas, shape, tmp, aspect, disableShadow);
}
tmp = tmp.nextSibling;
}
}
};
/**
* Function: computeAspect
*
* Returns a rectangle that contains the offset in x and y and the horizontal
* and vertical scale in width and height used to draw this shape inside the
* given <mxRectangle>.
*
* Parameters:
*
* shape - <mxShape> to be drawn.
* bounds - <mxRectangle> that should contain the stencil.
* direction - Optional direction of the shape to be darwn.
*/
mxStencil.prototype.computeAspect = function(shape, x, y, w, h, direction)
{
var x0 = x;
var y0 = y;
var sx = w / this.w0;
var sy = h / this.h0;
var inverse = (direction == mxConstants.DIRECTION_NORTH || direction == mxConstants.DIRECTION_SOUTH);
if (inverse)
{
sy = w / this.h0;
sx = h / this.w0;
var delta = (w - h) / 2;
x0 += delta;
y0 -= delta;
}
if (this.aspect == 'fixed')
{
sy = Math.min(sx, sy);
sx = sy;
// Centers the shape inside the available space
if (inverse)
{
x0 += (h - this.w0 * sx) / 2;
y0 += (w - this.h0 * sy) / 2;
}
else
{
x0 += (w - this.w0 * sx) / 2;
y0 += (h - this.h0 * sy) / 2;
}
}
return new mxRectangle(x0, y0, sx, sy);
};
/**
* Function: drawNode
*
* Draws this stencil inside the given bounds.
*/
mxStencil.prototype.drawNode = function(canvas, shape, node, aspect, disableShadow)
{
var name = node.nodeName;
var x0 = aspect.x;
var y0 = aspect.y;
var sx = aspect.width;
var sy = aspect.height;
var minScale = Math.min(sx, sy);
if (name == 'save')
{
canvas.save();
}
else if (name == 'restore')
{
canvas.restore();
}
else if (name == 'path')
{
canvas.begin();
// Renders the elements inside the given path
var childNode = node.firstChild;
while (childNode != null)
{
if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT)
{
this.drawNode(canvas, shape, childNode, aspect, disableShadow);
}
childNode = childNode.nextSibling;
}
}
else if (name == 'close')
{
canvas.close();
}
else if (name == 'move')
{
canvas.moveTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy);
}
else if (name == 'line')
{
canvas.lineTo(x0 + Number(node.getAttribute('x')) * sx, y0 + Number(node.getAttribute('y')) * sy);
}
else if (name == 'quad')
{
canvas.quadTo(x0 + Number(node.getAttribute('x1')) * sx,
y0 + Number(node.getAttribute('y1')) * sy,
x0 + Number(node.getAttribute('x2')) * sx,
y0 + Number(node.getAttribute('y2')) * sy);
}
else if (name == 'curve')
{
canvas.curveTo(x0 + Number(node.getAttribute('x1')) * sx,
y0 + Number(node.getAttribute('y1')) * sy,
x0 + Number(node.getAttribute('x2')) * sx,
y0 + Number(node.getAttribute('y2')) * sy,
x0 + Number(node.getAttribute('x3')) * sx,
y0 + Number(node.getAttribute('y3')) * sy);
}
else if (name == 'arc')
{
canvas.arcTo(Number(node.getAttribute('rx')) * sx,
Number(node.getAttribute('ry')) * sy,
Number(node.getAttribute('x-axis-rotation')),
Number(node.getAttribute('large-arc-flag')),
Number(node.getAttribute('sweep-flag')),
x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy);
}
else if (name == 'rect')
{
canvas.rect(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
Number(node.getAttribute('w')) * sx,
Number(node.getAttribute('h')) * sy);
}
else if (name == 'roundrect')
{
var arcsize = Number(node.getAttribute('arcsize'));
if (arcsize == 0)
{
arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100;
}
var w = Number(node.getAttribute('w')) * sx;
var h = Number(node.getAttribute('h')) * sy;
var factor = Number(arcsize) / 100;
var r = Math.min(w * factor, h * factor);
canvas.roundrect(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
w, h, r, r);
}
else if (name == 'ellipse')
{
canvas.ellipse(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
Number(node.getAttribute('w')) * sx,
Number(node.getAttribute('h')) * sy);
}
else if (name == 'image')
{
if (!shape.outline)
{
var src = this.evaluateAttribute(node, 'src', shape);
canvas.image(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
Number(node.getAttribute('w')) * sx,
Number(node.getAttribute('h')) * sy,
src, false, node.getAttribute('flipH') == '1',
node.getAttribute('flipV') == '1');
}
}
else if (name == 'text')
{
if (!shape.outline)
{
var str = this.evaluateTextAttribute(node, 'str', shape);
var rotation = node.getAttribute('vertical') == '1' ? -90 : 0;
if (node.getAttribute('align-shape') == '0')
{
var dr = shape.rotation;
// Depends on flipping
var flipH = mxUtils.getValue(shape.style, mxConstants.STYLE_FLIPH, 0) == 1;
var flipV = mxUtils.getValue(shape.style, mxConstants.STYLE_FLIPV, 0) == 1;
if (flipH && flipV)
{
rotation -= dr;
}
else if (flipH || flipV)
{
rotation += dr;
}
else
{
rotation -= dr;
}
}
rotation -= node.getAttribute('rotation');
canvas.text(x0 + Number(node.getAttribute('x')) * sx,
y0 + Number(node.getAttribute('y')) * sy,
0, 0, str, node.getAttribute('align') || 'left',
node.getAttribute('valign') || 'top', false, '',
null, false, rotation);
}
}
else if (name == 'include-shape')
{
var stencil = mxStencilRegistry.getStencil(node.getAttribute('name'));
if (stencil != null)
{
var x = x0 + Number(node.getAttribute('x')) * sx;
var y = y0 + Number(node.getAttribute('y')) * sy;
var w = Number(node.getAttribute('w')) * sx;
var h = Number(node.getAttribute('h')) * sy;
stencil.drawShape(canvas, shape, x, y, w, h);
}
}
else if (name == 'fillstroke')
{
canvas.fillAndStroke();
}
else if (name == 'fill')
{
canvas.fill();
}
else if (name == 'stroke')
{
canvas.stroke();
}
else if (name == 'strokewidth')
{
var s = (node.getAttribute('fixed') == '1') ? 1 : minScale;
canvas.setStrokeWidth(Number(node.getAttribute('width')) * s);
}
else if (name == 'dashed')
{
canvas.setDashed(node.getAttribute('dashed') == '1');
}
else if (name == 'dashpattern')
{
var value = node.getAttribute('pattern');
if (value != null)
{
var tmp = value.split(' ');
var pat = [];
for (var i = 0; i < tmp.length; i++)
{
if (tmp[i].length > 0)
{
pat.push(Number(tmp[i]) * minScale);
}
}
value = pat.join(' ');
canvas.setDashPattern(value);
}
}
else if (name == 'strokecolor')
{
canvas.setStrokeColor(node.getAttribute('color'));
}
else if (name == 'linecap')
{
canvas.setLineCap(node.getAttribute('cap'));
}
else if (name == 'linejoin')
{
canvas.setLineJoin(node.getAttribute('join'));
}
else if (name == 'miterlimit')
{
canvas.setMiterLimit(Number(node.getAttribute('limit')));
}
else if (name == 'fillcolor')
{
canvas.setFillColor(node.getAttribute('color'));
}
else if (name == 'alpha')
{
canvas.setAlpha(node.getAttribute('alpha'));
}
else if (name == 'fontcolor')
{
canvas.setFontColor(node.getAttribute('color'));
}
else if (name == 'fontstyle')
{
canvas.setFontStyle(node.getAttribute('style'));
}
else if (name == 'fontfamily')
{
canvas.setFontFamily(node.getAttribute('family'));
}
else if (name == 'fontsize')
{
canvas.setFontSize(Number(node.getAttribute('size')) * minScale);
}
if (disableShadow && (name == 'fillstroke' || name == 'fill' || name == 'stroke'))
{
disableShadow = false;
canvas.setShadow(false);
}
};