/** * 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 and pull the last saved state from the state * stack using . * * 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. * * *fillalpha* defines the degree of fill transparency used between 1.0 for fully * opaque and 0.0 for fully transparent. * * *strokealpha* defines the degree of stroke 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 * . * - "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 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 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 to the given XML node and * invoking and . * * Parameters: * * desc - XML node that contains the stencil description. */ function mxStencil(desc) { this.desc = desc; this.parseDescription(); this.parseConstraints(); }; /** * Extends mxShape. */ mxUtils.extend(mxStencil, mxShape); /** * 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 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 , , , and from . */ 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 into using * . */ 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 . */ 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 * is used as a key to if the localized attribute in the text * node is 1 or if 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 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) { var stack = canvas.states.slice(); // 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); // Draws a transparent rectangle for catching events if (shape.style != null && mxUtils.getValue(shape.style, mxConstants.STYLE_POINTER_EVENTS, '0') == '1') { canvas.setStrokeColor(mxConstants.NONE); canvas.rect(x, y, w, h); canvas.stroke(); canvas.setStrokeColor(shape.stroke); } this.drawChildren(canvas, shape, x, y, w, h, this.bgNode, aspect, false, true); this.drawChildren(canvas, shape, x, y, w, h, this.fgNode, aspect, true, !shape.outline || shape.style == null || mxUtils.getValue( shape.style, mxConstants.STYLE_BACKGROUND_OUTLINE, 0) == 0); // Restores stack for unequal count of save/restore calls if (canvas.states.length != stack.length) { canvas.states = stack; } }; /** * Function: drawChildren * * Draws this stencil inside the given bounds. */ mxStencil.prototype.drawChildren = function(canvas, shape, x, y, w, h, node, aspect, disableShadow, paint) { 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, paint); } 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 . * * Parameters: * * shape - to be drawn. * bounds - 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, paint) { 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 (paint) { if (name == 'path') { canvas.begin(); var parseRegularly = true; if (node.getAttribute('rounded') == '1') { parseRegularly = false; var arcSize = Number(node.getAttribute('arcSize')); var pointCount = 0; var segs = []; // Renders the elements inside the given path var childNode = node.firstChild; while (childNode != null) { if (childNode.nodeType == mxConstants.NODETYPE_ELEMENT) { var childName = childNode.nodeName; if (childName == 'move' || childName == 'line') { if (childName == 'move' || segs.length == 0) { segs.push([]); } segs[segs.length - 1].push(new mxPoint(x0 + Number(childNode.getAttribute('x')) * sx, y0 + Number(childNode.getAttribute('y')) * sy)); pointCount++; } else { //We only support move and line for rounded corners parseRegularly = true; break; } } childNode = childNode.nextSibling; } if (!parseRegularly && pointCount > 0) { for (var i = 0; i < segs.length; i++) { var close = false, ps = segs[i][0], pe = segs[i][segs[i].length - 1]; if (ps.x == pe.x && ps.y == pe.y) { segs[i].pop(); close = true; } this.addPoints(canvas, segs[i], true, arcSize, close); } } else { parseRegularly = true; } } if (parseRegularly) { // 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, paint); } 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 == 'fillalpha') { canvas.setAlpha(node.getAttribute('alpha')); } else if (name == 'strokealpha') { 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); } } };