diff options
Diffstat (limited to 'src/js/shape/mxShape.js')
-rw-r--r-- | src/js/shape/mxShape.js | 2045 |
1 files changed, 2045 insertions, 0 deletions
diff --git a/src/js/shape/mxShape.js b/src/js/shape/mxShape.js new file mode 100644 index 0000000..44ba3e7 --- /dev/null +++ b/src/js/shape/mxShape.js @@ -0,0 +1,2045 @@ +/** + * $Id: mxShape.js,v 1.173 2012-07-31 11:46:53 gaudenz Exp $ + * Copyright (c) 2006-2010, JGraph Ltd + */ +/** + * Class: mxShape + * + * Base class for all shapes. A shape in mxGraph is a + * separate implementation for SVG, VML and HTML. Which + * implementation to use is controlled by the <dialect> + * property which is assigned from within the <mxCellRenderer> + * when the shape is created. The dialect must be assigned + * for a shape, and it does normally depend on the browser and + * the confiuration of the graph (see <mxGraph> rendering hint). + * + * For each supported shape in SVG and VML, a corresponding + * shape exists in mxGraph, namely for text, image, rectangle, + * rhombus, ellipse and polyline. The other shapes are a + * combination of these shapes (eg. label and swimlane) + * or they consist of one or more (filled) path objects + * (eg. actor and cylinder). The HTML implementation is + * optional but may be required for a HTML-only view of + * the graph. + * + * Custom Shapes: + * + * To extend from this class, the basic code looks as follows. + * In the special case where the custom shape consists only of + * one filled region or one filled region and an additional stroke + * the <mxActor> and <mxCylinder> should be subclassed, + * respectively. These implement <redrawPath> in order to create + * the path expression for VML and SVG via a unified API (see + * <mxPath>). <mxCylinder.redrawPath> has an additional boolean + * argument to draw the foreground and background separately. + * + * (code) + * function CustomShape() { } + * + * CustomShape.prototype = new mxShape(); + * CustomShape.prototype.constructor = CustomShape; + * (end) + * + * To register a custom shape in an existing graph instance, + * one must register the shape under a new name in the graph's + * cell renderer as follows: + * + * (code) + * graph.cellRenderer.registerShape('customShape', CustomShape); + * (end) + * + * The second argument is the name of the constructor. + * + * In order to use the shape you can refer to the given name above + * in a stylesheet. For example, to change the shape for the default + * vertex style, the following code is used: + * + * (code) + * var style = graph.getStylesheet().getDefaultVertexStyle(); + * style[mxConstants.STYLE_SHAPE] = 'customShape'; + * (end) + * + * Constructor: mxShape + * + * Constructs a new shape. + */ +function mxShape() { }; + +/** + * Variable: SVG_STROKE_TOLERANCE + * + * Event-tolerance for SVG strokes (in px). Default is 8. + */ +mxShape.prototype.SVG_STROKE_TOLERANCE = 8; + +/** + * Variable: scale + * + * Holds the scale in which the shape is being painted. + */ +mxShape.prototype.scale = 1; + +/** + * Variable: dialect + * + * Holds the dialect in which the shape is to be painted. + * This can be one of the DIALECT constants in <mxConstants>. + */ +mxShape.prototype.dialect = null; + +/** + * Variable: crisp + * + * Special attribute for SVG rendering to set the shape-rendering attribute to + * crispEdges in the output. This is ignored in IE. Default is false. To + * disable antialias in IE, the explorer.css file can be changed as follows: + * + * [code] + * v\:* { + * behavior: url(#default#VML); + * antialias: false; + * } + * [/code] + */ +mxShape.prototype.crisp = false; + +/** + * Variable: roundedCrispSvg + * + * Specifies if crisp rendering should be enabled for rounded shapes. + * Default is true. + */ +mxShape.prototype.roundedCrispSvg = true; + +/** + * Variable: mixedModeHtml + * + * Specifies if <createHtml> should be used in mixed Html mode. + * Default is true. + */ +mxShape.prototype.mixedModeHtml = true; + +/** + * Variable: preferModeHtml + * + * Specifies if <createHtml> should be used in prefer Html mode. + * Default is true. + */ +mxShape.prototype.preferModeHtml = true; + +/** + * Variable: bounds + * + * Holds the <mxRectangle> that specifies the bounds of this shape. + */ +mxShape.prototype.bounds = null; + +/** + * Variable: points + * + * Holds the array of <mxPoints> that specify the points of this shape. + */ +mxShape.prototype.points = null; + +/** + * Variable: node + * + * Holds the outermost DOM node that represents this shape. + */ +mxShape.prototype.node = null; + +/** + * Variable: label + * + * Reference to the DOM node that should contain the label. This is null + * if the label should be placed inside <node> or <innerNode>. + */ +mxShape.prototype.label = null; + +/** + * Variable: innerNode + * + * Holds the DOM node that graphically represents this shape. This may be + * null if the outermost DOM <node> represents this shape. + */ +mxShape.prototype.innerNode = null; + +/** + * Variable: style + * + * Holds the style of the cell state that corresponds to this shape. This may + * be null if the shape is used directly, without a cell state. + */ +mxShape.prototype.style = null; + +/** + * Variable: startOffset + * + * Specifies the offset in pixels from the first point in <points> and + * the actual start of the shape. + */ +mxShape.prototype.startOffset = null; + +/** + * Variable: endOffset + * + * Specifies the offset in pixels from the last point in <points> and + * the actual start of the shape. + */ +mxShape.prototype.endOffset = null; + +/** + * Variable: boundingBox + * + * Contains the bounding box of the shape, that is, the smallest rectangle + * that includes all pixels of the shape. + */ +mxShape.prototype.boundingBox = null; + +/** + * Variable: vmlNodes + * + * Array if VML node names to fix in IE8 standards mode. + */ +mxShape.prototype.vmlNodes = ['node', 'strokeNode', 'fillNode', 'shadowNode']; + +/** + * Variable: vmlScale + * + * Internal scaling for VML using coordsize for better precision. + */ +mxShape.prototype.vmlScale = 1; + +/** + * Variable: strokewidth + * + * Holds the current strokewidth. Default is 1. + */ +mxShape.prototype.strokewidth = 1; + +/** + * Function: setCursor + * + * Sets the cursor on the given shape. + * + * Parameters: + * + * cursor - The cursor to be used. + */ +mxShape.prototype.setCursor = function(cursor) +{ + if (cursor == null) + { + cursor = ''; + } + + this.cursor = cursor; + + if (this.innerNode != null) + { + this.innerNode.style.cursor = cursor; + } + + if (this.node != null) + { + this.node.style.cursor = cursor; + } + + if (this.pipe != null) + { + this.pipe.style.cursor = cursor; + } +}; + +/** + * Function: getCursor + * + * Returns the current cursor. + */ +mxShape.prototype.getCursor = function() +{ + return this.cursor; +}; + +/** + * Function: init + * + * Initializes the shape by creaing the DOM node using <create> + * and adding it into the given container. + * + * Parameters: + * + * container - DOM node that will contain the shape. + */ +mxShape.prototype.init = function(container) +{ + if (this.node == null) + { + this.node = this.create(container); + + if (container != null) + { + container.appendChild(this.node); + + // Workaround for broken VML in IE8 standards mode. This gives an ID to + // each element that is referenced from this instance. After adding the + // DOM to the document, the outerHTML is overwritten to fix the VML + // rendering and the references are restored. + if (document.documentMode == 8 && mxUtils.isVml(this.node)) + { + this.reparseVml(); + } + } + } + + // Gradients are inserted late when the owner SVG element is known + if (this.insertGradientNode != null) + { + this.insertGradient(this.insertGradientNode); + this.insertGradientNode = null; + } +}; + +/** + * Function: reparseVml + * + * Forces a parsing of the outerHTML of this node and restores all references specified in <vmlNodes>. + * This is a workaround for the VML rendering bug in IE8 standards mode. + */ +mxShape.prototype.reparseVml = function() +{ + // Assigns temporary IDs to VML nodes so that references can be restored when + // inserted into the DOM as a string + for (var i = 0; i < this.vmlNodes.length; i++) + { + if (this[this.vmlNodes[i]] != null) + { + this[this.vmlNodes[i]].setAttribute('id', 'mxTemporaryReference-' + this.vmlNodes[i]); + } + } + + this.node.outerHTML = this.node.outerHTML; + + // Restores references to the actual DOM nodes + for (var i = 0; i < this.vmlNodes.length; i++) + { + if (this[this.vmlNodes[i]] != null) + { + this[this.vmlNodes[i]] = this.node.ownerDocument.getElementById('mxTemporaryReference-' + this.vmlNodes[i]); + this[this.vmlNodes[i]].removeAttribute('id'); + } + } +}; + +/** + * Function: insertGradient + * + * Inserts the given gradient node. + */ +mxShape.prototype.insertGradient = function(node) +{ + // Gradients are inserted late when the owner SVG element is known + if (node != null) + { + // Checks if the given gradient already exists inside the SVG element + // that also contains the node that represents this shape. If the gradient + // with the same ID exists in another SVG element, then this will add + // a copy of the gradient with a different ID to the SVG element and update + // the reference accordingly. This is required in Firefox because if the + // referenced fill element is removed from the DOM the shape appears black. + var count = 0; + var id = node.getAttribute('id'); + var gradient = document.getElementById(id); + + while (gradient != null && gradient.ownerSVGElement != this.node.ownerSVGElement) + { + count++; + id = node.getAttribute('id') + '-' + count; + gradient = document.getElementById(id); + } + + // According to specification, gradients should be put in a defs + // section in the first child of the owner SVG element. However, + // it turns out that gradients only work when added as follows. + if (gradient == null) + { + node.setAttribute('id', id); + this.node.ownerSVGElement.appendChild(node); + gradient = node; + } + + if (gradient != null) + { + var ref = 'url(#' + id + ')'; + var tmp = (this.innerNode != null) ? this.innerNode : this.node; + + if (tmp != null && tmp.getAttribute('fill') != ref) + { + tmp.setAttribute('fill', ref); + } + } + } +}; + +/** + * Function: isMixedModeHtml + * + * Used to determine if a shape can be rendered using <createHtml> in mixed + * mode Html without compromising the display accuracy. The default + * implementation will check if the shape is not rounded or rotated and has + * no gradient, and will use a DIV if that is the case. It will also check + * if <mxShape.mixedModeHtml> is true, which is the default settings. + * Subclassers can either override <mixedModeHtml> or this function if the + * result depends on dynamic values. The graph's dialect is available via + * <dialect>. + */ +mxShape.prototype.isMixedModeHtml = function() +{ + return this.mixedModeHtml && !this.isRounded && !this.isShadow && this.gradient == null && + mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, 0) == 0 && + mxUtils.getValue(this.style, mxConstants.STYLE_ROTATION, 0) == 0; +}; + +/** + * Function: create + * + * Creates and returns the DOM node(s) for the shape in + * the given container. This implementation invokes + * <createSvg>, <createHtml> or <createVml> depending + * on the <dialect> and style settings. + * + * Parameters: + * + * container - DOM node that will contain the shape. + */ +mxShape.prototype.create = function(container) +{ + var node = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + node = this.createSvg(); + } + else if (this.dialect == mxConstants.DIALECT_STRICTHTML || + (this.preferModeHtml && this.dialect == mxConstants.DIALECT_PREFERHTML) || + (this.isMixedModeHtml() && this.dialect == mxConstants.DIALECT_MIXEDHTML)) + { + node = this.createHtml(); + } + else + { + node = this.createVml(); + } + + return node; +}; + +/** + * Function: createHtml + * + * Creates and returns the HTML DOM node(s) to represent + * this shape. This implementation falls back to <createVml> + * so that the HTML creation is optional. + */ +mxShape.prototype.createHtml = function() +{ + var node = document.createElement('DIV'); + this.configureHtmlShape(node); + + return node; +}; + +/** + * Function: destroy + * + * Destroys the shape by removing it from the DOM and releasing the DOM + * node associated with the shape using <mxEvent.release>. + */ +mxShape.prototype.destroy = function() +{ + if (this.node != null) + { + mxEvent.release(this.node); + + if (this.node.parentNode != null) + { + this.node.parentNode.removeChild(this.node); + } + + if (this.node.glassOverlay) + { + this.node.glassOverlay.parentNode.removeChild(this.node.glassOverlay); + this.node.glassOverlay = null; + } + + this.node = null; + } +}; + +/** + * Function: apply + * + * Applies the style of the given <mxCellState> to the shape. This + * implementation assigns the following styles to local fields: + * + * - <mxConstants.STYLE_FILLCOLOR> => fill + * - <mxConstants.STYLE_GRADIENTCOLOR> => gradient + * - <mxConstants.STYLE_GRADIENT_DIRECTION> => gradientDirection + * - <mxConstants.STYLE_OPACITY> => opacity + * - <mxConstants.STYLE_STROKECOLOR> => stroke + * - <mxConstants.STYLE_STROKEWIDTH> => strokewidth + * - <mxConstants.STYLE_SHADOW> => isShadow + * - <mxConstants.STYLE_DASHED> => isDashed + * - <mxConstants.STYLE_SPACING> => spacing + * - <mxConstants.STYLE_STARTSIZE> => startSize + * - <mxConstants.STYLE_ENDSIZE> => endSize + * - <mxConstants.STYLE_ROUNDED> => isRounded + * - <mxConstants.STYLE_STARTARROW> => startArrow + * - <mxConstants.STYLE_ENDARROW> => endArrow + * - <mxConstants.STYLE_ROTATION> => rotation + * - <mxConstants.STYLE_DIRECTION> => direction + * + * This keeps a reference to the <style>. If you need to keep a reference to + * the cell, you can override this method and store a local reference to + * state.cell or the <mxCellState> itself. + * + * Parameters: + * + * state - <mxCellState> of the corresponding cell. + */ +mxShape.prototype.apply = function(state) +{ + var style = state.style; + this.style = style; + + if (style != null) + { + this.fill = mxUtils.getValue(style, mxConstants.STYLE_FILLCOLOR, this.fill); + this.gradient = mxUtils.getValue(style, mxConstants.STYLE_GRADIENTCOLOR, this.gradient); + this.gradientDirection = mxUtils.getValue(style, mxConstants.STYLE_GRADIENT_DIRECTION, this.gradientDirection); + this.opacity = mxUtils.getValue(style, mxConstants.STYLE_OPACITY, this.opacity); + this.stroke = mxUtils.getValue(style, mxConstants.STYLE_STROKECOLOR, this.stroke); + this.strokewidth = mxUtils.getNumber(style, mxConstants.STYLE_STROKEWIDTH, this.strokewidth); + this.isShadow = mxUtils.getValue(style, mxConstants.STYLE_SHADOW, this.isShadow); + this.isDashed = mxUtils.getValue(style, mxConstants.STYLE_DASHED, this.isDashed); + this.spacing = mxUtils.getValue(style, mxConstants.STYLE_SPACING, this.spacing); + this.startSize = mxUtils.getNumber(style, mxConstants.STYLE_STARTSIZE, this.startSize); + this.endSize = mxUtils.getNumber(style, mxConstants.STYLE_ENDSIZE, this.endSize); + this.isRounded = mxUtils.getValue(style, mxConstants.STYLE_ROUNDED, this.isRounded); + this.startArrow = mxUtils.getValue(style, mxConstants.STYLE_STARTARROW, this.startArrow); + this.endArrow = mxUtils.getValue(style, mxConstants.STYLE_ENDARROW, this.endArrow); + this.rotation = mxUtils.getValue(style, mxConstants.STYLE_ROTATION, this.rotation); + this.direction = mxUtils.getValue(style, mxConstants.STYLE_DIRECTION, this.direction); + + if (this.fill == 'none') + { + this.fill = null; + } + + if (this.gradient == 'none') + { + this.gradient = null; + } + + if (this.stroke == 'none') + { + this.stroke = null; + } + } +}; + +/** + * Function: createSvgGroup + * + * Creates a SVG group element and adds the given shape as a child of the + * element. The child is stored in <innerNode> for later access. + */ +mxShape.prototype.createSvgGroup = function(shape) +{ + var g = document.createElementNS(mxConstants.NS_SVG, 'g'); + + // Creates the shape inside an svg group + this.innerNode = document.createElementNS(mxConstants.NS_SVG, shape); + this.configureSvgShape(this.innerNode); + + // Avoids anti-aliasing for non-rounded rectangles with a + // strokewidth of 1 or more pixels + if (shape == 'rect' && this.strokewidth * this.scale >= 1 && !this.isRounded) + { + this.innerNode.setAttribute('shape-rendering', 'optimizeSpeed'); + } + + // Creates the shadow + this.shadowNode = this.createSvgShadow(this.innerNode); + + if (this.shadowNode != null) + { + g.appendChild(this.shadowNode); + } + + // Appends the main shape after the shadow + g.appendChild(this.innerNode); + + return g; +}; + +/** + * Function: createSvgShadow + * + * Creates a clone of the given node and configures the node's color + * to use <mxConstants.SHADOWCOLOR>. + */ +mxShape.prototype.createSvgShadow = function(node) +{ + if (this.isShadow) + { + var shadow = node.cloneNode(true); + shadow.setAttribute('opacity', mxConstants.SHADOW_OPACITY); + + if (this.fill != null && this.fill != mxConstants.NONE) + { + shadow.setAttribute('fill', mxConstants.SHADOWCOLOR); + } + + if (this.stroke != null && this.stroke != mxConstants.NONE) + { + shadow.setAttribute('stroke', mxConstants.SHADOWCOLOR); + } + + return shadow; + } + + return null; +}; + +/** + * Function: configureHtmlShape + * + * Configures the specified HTML node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxShape.prototype.configureHtmlShape = function(node) +{ + if (mxUtils.isVml(node)) + { + this.configureVmlShape(node); + } + else + { + node.style.position = 'absolute'; + node.style.overflow = 'hidden'; + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.style.borderColor = color; + + if (this.isDashed) + { + node.style.borderStyle = 'dashed'; + } + else if (this.strokewidth > 0) + { + node.style.borderStyle = 'solid'; + } + + node.style.borderWidth = Math.ceil(this.strokewidth * this.scale) + 'px'; + } + else + { + node.style.borderWidth = '0px'; + } + + color = this.fill; + node.style.background = ''; + + if (color != null && color != mxConstants.NONE) + { + node.style.backgroundColor = color; + } + else if (this.points == null) + { + this.configureTransparentBackground(node); + } + + if (this.opacity != null) + { + mxUtils.setOpacity(node, this.opacity); + } + } +}; + +/** + * Function: updateVmlFill + * + * Updates the given VML fill node. + */ +mxShape.prototype.updateVmlFill = function(node, c1, c2, dir, alpha) +{ + node.color = c1; + + if (alpha != null && alpha != 100) + { + node.opacity = alpha + '%'; + + if (c2 != null) + { + // LATER: Set namespaced attribute without using setAttribute + // which is required for updating the value in IE8 standards. + node.setAttribute('o:opacity2', alpha + '%'); + } + } + + if (c2 != null) + { + node.type = 'gradient'; + node.color2 = c2; + var angle = '180'; + + if (this.gradientDirection == mxConstants.DIRECTION_EAST) + { + angle = '270'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_WEST) + { + angle = '90'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_NORTH) + { + angle = '0'; + } + + node.angle = angle; + } +}; + +/** + * Function: updateVmlStrokeNode + * + * Creates the stroke node for VML. + */ +mxShape.prototype.updateVmlStrokeNode = function(parent) +{ + // Stroke node is always needed to specify defaults that match SVG output + if (this.strokeNode == null) + { + this.strokeNode = document.createElement('v:stroke'); + + // To math SVG defaults jointsyle miter and miterlimit 4 + this.strokeNode.joinstyle = 'miter'; + this.strokeNode.miterlimit = 4; + + parent.appendChild(this.strokeNode); + } + + if (this.opacity != null) + { + this.strokeNode.opacity = this.opacity + '%'; + } + + this.updateVmlDashStyle(); +}; + +/** + * Function: updateVmlStrokeColor + * + * Updates the VML stroke color for the given node. + */ +mxShape.prototype.updateVmlStrokeColor = function(node) +{ + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.stroked = 'true'; + node.strokecolor = color; + } + else + { + node.stroked = 'false'; + } +}; + +/** + * Function: configureVmlShape + * + * Configures the specified VML node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxShape.prototype.configureVmlShape = function(node) +{ + node.style.position = 'absolute'; + this.updateVmlStrokeColor(node); + node.style.background = ''; + var color = this.fill; + + if (color != null && color != mxConstants.NONE) + { + if (this.fillNode == null) + { + this.fillNode = document.createElement('v:fill'); + node.appendChild(this.fillNode); + } + + this.updateVmlFill(this.fillNode, color, this.gradient, this.gradientDirection, this.opacity); + } + else + { + node.filled = 'false'; + + if (this.points == null) + { + this.configureTransparentBackground(node); + } + } + + this.updateVmlStrokeNode(node); + + if (this.isShadow) + { + this.createVmlShadow(node); + } + + // Fixes possible hang in IE when arcsize is set on non-rects + if (node.nodeName == 'roundrect') + { + // Workaround for occasional "member not found" error + try + { + var f = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + + if (this.style != null) + { + f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, f); + } + + node.setAttribute('arcsize', String(f) + '%'); + } + catch (e) + { + // ignore + } + } +}; + +/** + * Function: createVmlShadow + * + * Creates the VML shadow node. + */ +mxShape.prototype.createVmlShadow = function(node) +{ + // Adds a shadow only once per shape + if (this.shadowNode == null) + { + this.shadowNode = document.createElement('v:shadow'); + this.shadowNode.on = 'true'; + this.shadowNode.color = mxConstants.SHADOWCOLOR; + this.shadowNode.opacity = (mxConstants.SHADOW_OPACITY * 100) + '%'; + + this.shadowStrokeNode = document.createElement('v:stroke'); + this.shadowNode.appendChild(this.shadowStrokeNode); + + node.appendChild(this.shadowNode); + } +}; + +/** + * Function: configureTransparentBackground + * + * Hook to make the background of a shape transparent. This hook was added as + * a workaround for the "display non secure items" warning dialog in IE which + * appears if the background:url(transparent.gif) is used in the overlay pane + * of a diagram. Since only mxImageShapes currently exist in the overlay pane + * this function is only overridden in mxImageShape. + */ +mxShape.prototype.configureTransparentBackground = function(node) +{ + node.style.background = 'url(\'' + mxClient.imageBasePath + '/transparent.gif\')'; +}; + +/** + * Function: configureSvgShape + * + * Configures the specified SVG node by applying the current color, + * bounds, shadow, opacity etc. + */ +mxShape.prototype.configureSvgShape = function(node) +{ + var color = this.stroke; + + if (color != null && color != mxConstants.NONE) + { + node.setAttribute('stroke', color); + } + else + { + node.setAttribute('stroke', 'none'); + } + + color = this.fill; + + if (color != null && color != mxConstants.NONE) + { + // Fetches a reference to a shared gradient + if (this.gradient != null) + { + var id = this.getGradientId(color, this.gradient); + + if (this.gradientNode != null && this.gradientNode.getAttribute('id') != id) + { + this.gradientNode = null; + node.setAttribute('fill', ''); + } + + if (this.gradientNode == null) + { + this.gradientNode = this.createSvgGradient(id, + color, this.gradient, node); + node.setAttribute('fill', 'url(#'+id+')'); + } + } + else + { + // TODO: Remove gradient from document if no longer shared + this.gradientNode = null; + node.setAttribute('fill', color); + } + } + else + { + node.setAttribute('fill', 'none'); + } + + if (this.opacity != null) + { + // Improves opacity performance in Firefox + node.setAttribute('fill-opacity', this.opacity / 100); + node.setAttribute('stroke-opacity', this.opacity / 100); + } +}; + +/** + * Function: getGradientId + * + * Creates a unique ID for the gradient of this shape. + */ +mxShape.prototype.getGradientId = function(start, end) +{ + // Removes illegal characters from gradient ID + if (start.charAt(0) == '#') + { + start = start.substring(1); + } + + if (end.charAt(0) == '#') + { + end = end.substring(1); + } + + // Workaround for gradient IDs not working in Safari 5 / Chrome 6 + // if they contain uppercase characters + start = start.toLowerCase(); + end = end.toLowerCase(); + + var dir = null; + + if (this.gradientDirection == null || + this.gradientDirection == mxConstants.DIRECTION_SOUTH) + { + dir = 'south'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_EAST) + { + dir = 'east'; + } + else + { + var tmp = start; + start = end; + end = tmp; + + if (this.gradientDirection == mxConstants.DIRECTION_NORTH) + { + dir = 'south'; + } + else if (this.gradientDirection == mxConstants.DIRECTION_WEST) + { + dir = 'east'; + } + } + + return 'mx-gradient-'+start+'-'+end+'-'+dir; +}; + +/** + * Function: createSvgPipe + * + * Creates an invisible path which is used to increase the hit detection for + * edges in SVG. + */ +mxShape.prototype.createSvgPipe = function(id, start, end, node) +{ + var pipe = document.createElementNS(mxConstants.NS_SVG, 'path'); + pipe.setAttribute('pointer-events', 'stroke'); + pipe.setAttribute('fill', 'none'); + pipe.setAttribute('visibility', 'hidden'); + // Workaround for Opera ignoring the visiblity attribute above while + // other browsers need a stroke color to perform the hit-detection but + // do not ignore the visibility attribute. Side-effect is that Opera's + // hit detection for horizontal/vertical edges seems to ignore the pipe. + pipe.setAttribute('stroke', (mxClient.IS_OP) ? 'none' : 'white'); + + return pipe; +}; + +/** + * Function: createSvgGradient + * + * Creates a gradient object for SVG using the specified startcolor, + * endcolor and opacity. + */ +mxShape.prototype.createSvgGradient = function(id, start, end, node) +{ + var gradient = this.insertGradientNode; + + if (gradient == null) + { + gradient = document.createElementNS(mxConstants.NS_SVG, 'linearGradient'); + gradient.setAttribute('id', id); + gradient.setAttribute('x1', '0%'); + gradient.setAttribute('y1', '0%'); + gradient.setAttribute('x2', '0%'); + gradient.setAttribute('y2', '0%'); + + if (this.gradientDirection == null || + this.gradientDirection == mxConstants.DIRECTION_SOUTH) + { + gradient.setAttribute('y2', '100%'); + } + else if (this.gradientDirection == mxConstants.DIRECTION_EAST) + { + gradient.setAttribute('x2', '100%'); + } + else if (this.gradientDirection == mxConstants.DIRECTION_NORTH) + { + gradient.setAttribute('y1', '100%'); + } + else if (this.gradientDirection == mxConstants.DIRECTION_WEST) + { + gradient.setAttribute('x1', '100%'); + } + + var stop = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop.setAttribute('offset', '0%'); + stop.setAttribute('style', 'stop-color:'+start); + gradient.appendChild(stop); + + stop = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop.setAttribute('offset', '100%'); + stop.setAttribute('style', 'stop-color:'+end); + gradient.appendChild(stop); + } + + // Inserted later when the owner SVG element is known + this.insertGradientNode = gradient; + + return gradient; +}; + +/** + * Function: createPoints + * + * Creates a path expression using the specified commands for this.points. + * If <isRounded> is true, then the path contains curves for the corners. + */ +mxShape.prototype.createPoints = function(moveCmd, lineCmd, curveCmd, isRelative) +{ + var offsetX = (isRelative) ? this.bounds.x : 0; + var offsetY = (isRelative) ? this.bounds.y : 0; + + // Workaround for crisp shape-rendering in IE9 + var crisp = (this.crisp && this.dialect == mxConstants.DIALECT_SVG && mxClient.IS_IE) ? 0.5 : 0; + + if (isNaN(this.points[0].x) || isNaN(this.points[0].y)) + { + return null; + } + + var size = mxConstants.LINE_ARCSIZE * this.scale; + var p0 = this.points[0]; + + if (this.startOffset != null) + { + p0 = p0.clone(); + p0.x += this.startOffset.x; + p0.y += this.startOffset.y; + } + + var points = moveCmd + ' ' + (Math.round(p0.x - offsetX) + crisp) + ' ' + + (Math.round(p0.y - offsetY) + crisp) + ' '; + + for (var i = 1; i < this.points.length; i++) + { + p0 = this.points[i - 1]; + var pt = this.points[i]; + + if (isNaN(pt.x) || isNaN(pt.y)) + { + return null; + } + + if (i == this.points.length - 1 && this.endOffset != null) + { + pt = pt.clone(); + pt.x += this.endOffset.x; + pt.y += this.endOffset.y; + } + + var dx = p0.x - pt.x; + var dy = p0.y - pt.y; + + if ((this.isRounded && i < this.points.length - 1) && + (dx != 0 || dy != 0) && this.scale > 0.3) + { + // Draws a line from the last point to the current point with a spacing + // of size off the current point into direction of the last point + var dist = Math.sqrt(dx * dx + dy * dy); + var nx1 = dx * Math.min(size, dist / 2) / dist; + var ny1 = dy * Math.min(size, dist / 2) / dist; + points += lineCmd + ' ' + (Math.round(pt.x + nx1 - offsetX) + crisp) + ' ' + + (Math.round(pt.y + ny1 - offsetY) + crisp) + ' '; + + // Draws a curve from the last point to the current point with a spacing + // of size off the current point into direction of the next point + var pe = this.points[i+1]; + dx = pe.x - pt.x; + dy = pe.y - pt.y; + + dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); + + if (dist != 0) + { + var nx2 = dx * Math.min(size, dist / 2) / dist; + var ny2 = dy * Math.min(size, dist / 2) / dist; + + points += curveCmd + ' ' + Math.round(pt.x - offsetX) + ' '+ + Math.round(pt.y - offsetY) + ' ' + Math.round(pt.x - offsetX) + ',' + + Math.round(pt.y - offsetY) + ' ' + (Math.round(pt.x + nx2 - offsetX) + crisp) + ' ' + + (Math.round(pt.y + ny2 - offsetY) + crisp) + ' '; + } + } + else + { + points += lineCmd + ' ' + (Math.round(pt.x - offsetX) + crisp) + ' ' + (Math.round(pt.y - offsetY) + crisp) + ' '; + } + } + + return points; +}; + +/** + * Function: updateHtmlShape + * + * Updates the bounds or points of the specified HTML node and + * updates the inner children to reflect the changes. + */ +mxShape.prototype.updateHtmlShape = function(node) +{ + if (node != null) + { + if (mxUtils.isVml(node)) + { + this.updateVmlShape(node); + } + else + { + var sw = Math.ceil(this.strokewidth * this.scale); + node.style.borderWidth = Math.max(1, sw) + 'px'; + + if (this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) && + !isNaN(this.bounds.width) && !isNaN(this.bounds.height)) + { + node.style.left = Math.round(this.bounds.x - sw / 2) + 'px'; + node.style.top = Math.round(this.bounds.y - sw / 2) + 'px'; + + if (document.compatMode == 'CSS1Compat') + { + sw = -sw; + } + + node.style.width = Math.round(Math.max(0, this.bounds.width + sw)) + 'px'; + node.style.height = Math.round(Math.max(0, this.bounds.height + sw)) + 'px'; + + if (this.bounds.width == 0 || this.bounds.height == 0) + { + node.style.visibility = 'hidden'; + } + else + { + node.style.visibility = 'visible'; + } + } + } + + if (this.points != null && this.bounds != null && !mxUtils.isVml(node)) + { + if (this.divContainer == null) + { + this.divContainer = node; + } + + while (this.divContainer.firstChild != null) + { + mxEvent.release(this.divContainer.firstChild); + this.divContainer.removeChild(this.divContainer.firstChild); + } + + node.style.borderStyle = ''; + node.style.background = ''; + + if (this.points.length == 2) + { + var p0 = this.points[0]; + var pe = this.points[1]; + + var dx = pe.x - p0.x; + var dy = pe.y - p0.y; + + if (dx == 0 || dy == 0) + { + node.style.borderStyle = 'solid'; + } + else + { + node.style.width = Math.round(this.bounds.width + 1) + 'px'; + node.style.height = Math.round(this.bounds.height + 1) + 'px'; + + var length = Math.sqrt(dx * dx + dy * dy); + var dotCount = 1 + (length / (8 * this.scale)); + + var nx = dx / dotCount; + var ny = dy / dotCount; + var x = p0.x - this.bounds.x; + var y = p0.y - this.bounds.y; + + for (var i = 0; i < dotCount; i++) + { + var tmp = document.createElement('DIV'); + + tmp.style.position = 'absolute'; + tmp.style.overflow = 'hidden'; + + tmp.style.left = Math.round(x) + 'px'; + tmp.style.top = Math.round(y) + 'px'; + tmp.style.width = Math.max(1, 2 * this.scale) + 'px'; + tmp.style.height = Math.max(1, 2 * this.scale) + 'px'; + + tmp.style.backgroundColor = this.stroke; + this.divContainer.appendChild(tmp); + + x += nx; + y += ny; + } + } + } + else if (this.points.length == 3) + { + var mid = this.points[1]; + + var n = '0'; + var s = '1'; + var w = '0'; + var e = '1'; + + if (mid.x == this.bounds.x) + { + e = '0'; + w = '1'; + } + + if (mid.y == this.bounds.y) + { + n = '1'; + s = '0'; + } + + node.style.borderStyle = 'solid'; + node.style.borderWidth = n + ' ' + e + ' ' + s + ' ' + w + 'px'; + } + else + { + node.style.width = Math.round(this.bounds.width + 1) + 'px'; + node.style.height = Math.round(this.bounds.height + 1) + 'px'; + var last = this.points[0]; + + for (var i = 1; i < this.points.length; i++) + { + var next = this.points[i]; + + // TODO: Use one div for multiple lines + var tmp = document.createElement('DIV'); + + tmp.style.position = 'absolute'; + tmp.style.overflow = 'hidden'; + + tmp.style.borderColor = this.stroke; + tmp.style.borderStyle = 'solid'; + tmp.style.borderWidth = '1 0 0 1px'; + + var x = Math.min(next.x, last.x) - this.bounds.x; + var y = Math.min(next.y, last.y) - this.bounds.y; + var w = Math.max(1, Math.abs(next.x - last.x)); + var h = Math.max(1, Math.abs(next.y - last.y)); + + tmp.style.left = x + 'px'; + tmp.style.top = y + 'px'; + tmp.style.width = w + 'px'; + tmp.style.height = h + 'px'; + + this.divContainer.appendChild(tmp); + last = next; + } + } + } + } +}; + +/** + * Function: updateVmlDashStyle + * + * Updates the dashstyle in the stroke node. + */ +mxShape.prototype.updateVmlDashStyle = function() +{ + if (this.isDashed) + { + if (this.strokeNode.dashstyle != 'dash') + { + this.strokeNode.dashstyle = 'dash'; + } + } + else if (this.strokeNode.dashstyle != 'solid') + { + this.strokeNode.dashstyle = 'solid'; + } +}; + +/** + * Function: updateVmlShape + * + * Updates the bounds or points of the specified VML node and + * updates the inner children to reflect the changes. + */ +mxShape.prototype.updateVmlShape = function(node) +{ + node.strokeweight = (this.strokewidth * this.scale) + 'px'; + + // Dash pattern needs updating as it depends on strokeweight in VML + if (this.strokeNode != null) + { + this.updateVmlDashStyle(); + } + + // Updates the offset of the shadow + if (this.shadowNode != null) + { + var dx = Math.round(mxConstants.SHADOW_OFFSET_X * this.scale); + var dy = Math.round(mxConstants.SHADOW_OFFSET_Y * this.scale); + this.shadowNode.offset = dx + 'px,' + dy + 'px'; + } + + if (this.bounds != null && !isNaN(this.bounds.x) && !isNaN(this.bounds.y) && + !isNaN(this.bounds.width) && !isNaN(this.bounds.height)) + { + var f = 1; + + var w = Math.max(0, Math.round(this.bounds.width)); + var h = Math.max(0, Math.round(this.bounds.height)); + + // Groups and shapes need a coordsize + if (this.points != null || node.nodeName == 'shape' || node.nodeName == 'group') + { + var tmp = (node.parentNode.nodeName == 'group') ? 1 : this.vmlScale; + node.coordsize = (w * tmp) + ',' + (h * tmp); + } + else if (node.parentNode.nodeName == 'group') + { + f = this.vmlScale; + } + + // Only top-level nodes are non-relative and rotated + if (node.parentNode != this.node) + { + node.style.left = Math.round(this.bounds.x * f) + 'px'; + node.style.top = Math.round(this.bounds.y * f) + 'px'; + + if (this.points == null) + { + if (this.rotation != null && this.rotation != 0) + { + node.style.rotation = this.rotation; + } + else if (node.style.rotation != null) + { + node.style.rotation = ''; + } + } + } + + node.style.width = (w * f) + 'px'; + node.style.height = (h * f) + 'px'; + } + + if (this.points != null && node.nodeName != 'group') + { + if (node.nodeName == 'polyline' && node.points != null) + { + var points = ''; + + for (var i = 0; i < this.points.length; i++) + { + points += this.points[i].x + ',' + this.points[i].y + ' '; + } + + node.points.value = points; + + node.style.left = null; + node.style.top = null; + node.style.width = null; + node.style.height = null; + } + else if (this.bounds != null) + { + var points = this.createPoints('m', 'l', 'c', true); + + // Smooth style for VML (experimental) + if (this.style != null && this.style[mxConstants.STYLE_SMOOTH]) + { + var pts = this.points; + var n = pts.length; + + if (n > 3) + { + var x0 = this.bounds.x; + var y0 = this.bounds.y; + points = 'm ' + Math.round(pts[0].x - x0) + ' ' + Math.round(pts[0].y - y0) + ' qb'; + + for (var i = 1; i < n - 1; i++) + { + points += ' ' + Math.round(pts[i].x - x0) + ' ' + Math.round(pts[i].y - y0); + } + + points += ' nf l ' + Math.round(pts[n - 1].x - x0) + ' ' + Math.round(pts[n - 1].y - y0); + } + } + + node.path = points + ' e'; + } + } +}; + +/** + * Function: updateSvgBounds + * + * Updates the bounds of the given node using <bounds>. + */ +mxShape.prototype.updateSvgBounds = function(node) +{ + var w = this.bounds.width; + var h = this.bounds.height; + + if (this.isRounded && !(this.crisp && mxClient.IS_IE)) + { + node.setAttribute('x', this.bounds.x); + node.setAttribute('y', this.bounds.y); + node.setAttribute('width', w); + node.setAttribute('height', h); + } + else + { + // Workaround for crisp shape-rendering in IE9 + var dd = (this.crisp && mxClient.IS_IE) ? 0.5 : 0; + node.setAttribute('x', Math.round(this.bounds.x) + dd); + node.setAttribute('y', Math.round(this.bounds.y) + dd); + + w = Math.round(w); + h = Math.round(h); + + node.setAttribute('width', w); + node.setAttribute('height', h); + } + + if (this.isRounded) + { + var f = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; + + if (this.style != null) + { + f = mxUtils.getValue(this.style, mxConstants.STYLE_ARCSIZE, f) / 100; + } + + var r = Math.min(w * f, h * f); + node.setAttribute('rx', r); + node.setAttribute('ry', r); + } + + this.updateSvgTransform(node, node == this.shadowNode); +}; + +/** + * Function: updateSvgPath + * + * Updates the path of the given node using <points>. + */ +mxShape.prototype.updateSvgPath = function(node) +{ + var d = this.createPoints('M', 'L', 'C', false); + + if (d != null) + { + node.setAttribute('d', d); + + // Smooth style for SVG (experimental) + if (this.style != null && this.style[mxConstants.STYLE_SMOOTH]) + { + var pts = this.points; + var n = pts.length; + + if (n > 3) + { + var points = 'M '+pts[0].x+' '+pts[0].y+' '; + points += ' Q '+pts[1].x + ' ' + pts[1].y + ' ' + + ' '+pts[2].x + ' ' + pts[2].y; + + for (var i = 3; i < n; i++) + { + points += ' T ' + pts[i].x + ' ' + pts[i].y; + } + + node.setAttribute('d', points); + } + } + + node.removeAttribute('x'); + node.removeAttribute('y'); + node.removeAttribute('width'); + node.removeAttribute('height'); + } +}; + +/** + * Function: updateSvgScale + * + * Updates the properties of the given node that depend on the scale and checks + * the crisp rendering attribute. + */ +mxShape.prototype.updateSvgScale = function(node) +{ + node.setAttribute('stroke-width', Math.round(Math.max(1, this.strokewidth * this.scale))); + + if (this.isDashed) + { + var phase = Math.max(1, Math.round(3 * this.scale * this.strokewidth)); + node.setAttribute('stroke-dasharray', phase + ' ' + phase); + } + + if (this.crisp && (this.roundedCrispSvg || this.isRounded != true) && + (this.rotation == null || this.rotation == 0)) + { + node.setAttribute('shape-rendering', 'crispEdges'); + } + else + { + node.removeAttribute('shape-rendering'); + } +}; + +/** + * Function: updateSvgShape + * + * Updates the bounds or points of the specified SVG node and + * updates the inner children to reflect the changes. + */ +mxShape.prototype.updateSvgShape = function(node) +{ + if (this.points != null && this.points[0] != null) + { + this.updateSvgPath(node); + } + else if (this.bounds != null) + { + this.updateSvgBounds(node); + } + + this.updateSvgScale(node); +}; + +/** + * Function: getSvgShadowTransform + * + * Returns the current transformation for SVG shadows. + */ +mxShape.prototype.getSvgShadowTransform = function(node, shadow) +{ + var dx = mxConstants.SHADOW_OFFSET_X * this.scale; + var dy = mxConstants.SHADOW_OFFSET_Y * this.scale; + + return 'translate(' + dx + ' ' + dy + ')'; +}; + +/** + * Function: updateSvgTransform + * + * Updates the tranform of the given node. + */ +mxShape.prototype.updateSvgTransform = function(node, shadow) +{ + var st = (shadow) ? this.getSvgShadowTransform() : ''; + + if (this.rotation != null && this.rotation != 0) + { + var cx = this.bounds.x + this.bounds.width / 2; + var cy = this.bounds.y + this.bounds.height / 2; + node.setAttribute('transform', 'rotate(' + this.rotation + ',' + cx + ',' + cy + ') ' + st); + } + else + { + if (shadow) + { + node.setAttribute('transform', st); + } + else + { + node.removeAttribute('transform'); + } + } +}; + +/** + * Function: reconfigure + * + * Reconfigures this shape. This will update the colors etc in + * addition to the bounds or points. + */ +mxShape.prototype.reconfigure = function() +{ + if (this.dialect == mxConstants.DIALECT_SVG) + { + if (this.innerNode != null) + { + this.configureSvgShape(this.innerNode); + } + else + { + this.configureSvgShape(this.node); + } + + if (this.insertGradientNode != null) + { + this.insertGradient(this.insertGradientNode); + this.insertGradientNode = null; + } + } + else if (mxUtils.isVml(this.node)) + { + this.node.style.visibility = 'hidden'; + this.configureVmlShape(this.node); + this.node.style.visibility = 'visible'; + } + else + { + this.node.style.visibility = 'hidden'; + this.configureHtmlShape(this.node); + this.node.style.visibility = 'visible'; + } +}; + +/** + * Function: redraw + * + * Invokes <redrawSvg>, <redrawVml> or <redrawHtml> depending on the + * dialect of the shape. + */ +mxShape.prototype.redraw = function() +{ + this.updateBoundingBox(); + + if (this.dialect == mxConstants.DIALECT_SVG) + { + this.redrawSvg(); + } + else if (mxUtils.isVml(this.node)) + { + this.node.style.visibility = 'hidden'; + this.redrawVml(); + this.node.style.visibility = 'visible'; + } + else + { + this.redrawHtml(); + } +}; + +/** + * Function: updateBoundingBox + * + * Updates the <boundingBox> for this shape using <createBoundingBox> and + * <augmentBoundingBox> and stores the result in <boundingBox>. + */ +mxShape.prototype.updateBoundingBox = function() +{ + if (this.bounds != null) + { + var bbox = this.createBoundingBox(); + this.augmentBoundingBox(bbox); + + var rot = Number(mxUtils.getValue(this.style, mxConstants.STYLE_ROTATION, 0)); + + if (rot != 0) + { + bbox = mxUtils.getBoundingBox(bbox, rot); + } + + bbox.x = Math.floor(bbox.x); + bbox.y = Math.floor(bbox.y); + // TODO: Fix rounding errors + bbox.width = Math.ceil(bbox.width); + bbox.height = Math.ceil(bbox.height); + + this.boundingBox = bbox; + } +}; + +/** + * Function: createBoundingBox + * + * Returns a new rectangle that represents the bounding box of the bare shape + * with no shadows or strokewidths. + */ +mxShape.prototype.createBoundingBox = function() +{ + return this.bounds.clone(); +}; + +/** + * Function: augmentBoundingBox + * + * Augments the bounding box with the strokewidth and shadow offsets. + */ +mxShape.prototype.augmentBoundingBox = function(bbox) +{ + if (this.isShadow) + { + bbox.width += Math.ceil(mxConstants.SHADOW_OFFSET_X * this.scale); + bbox.height += Math.ceil(mxConstants.SHADOW_OFFSET_Y * this.scale); + } + + // Adds strokeWidth + var sw = Math.ceil(this.strokewidth * this.scale); + bbox.grow(Math.ceil(sw / 2)); +}; + +/** + * Function: redrawSvg + * + * Redraws this SVG shape by invoking <updateSvgShape> on this.node, + * this.innerNode and this.shadowNode. + */ +mxShape.prototype.redrawSvg = function() +{ + if (this.innerNode != null) + { + this.updateSvgShape(this.innerNode); + + if (this.shadowNode != null) + { + this.updateSvgShape(this.shadowNode); + } + } + else + { + this.updateSvgShape(this.node); + + // Updates the transform of the shadow + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform()); + } + } + + this.updateSvgGlassPane(); +}; + +/** + * Function: updateVmlGlassPane + * + * Draws the glass overlay if mxConstants.STYLE_GLASS is 1. + */ +mxShape.prototype.updateVmlGlassPane = function() +{ + // Currently only used in mxLabel. Most shapes would have to be changed to use + // a group node in VML which might affect performance for glass-less cells. + if (this.bounds != null && this.node.nodeName == 'group' && this.style != null && + mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, 0) == 1) + { + // Glass overlay + if (this.node.glassOverlay == null) + { + // Creates glass overlay + this.node.glassOverlay = document.createElement('v:shape'); + this.node.glassOverlay.setAttribute('filled', 'true'); + this.node.glassOverlay.setAttribute('fillcolor', 'white'); + this.node.glassOverlay.setAttribute('stroked', 'false'); + + var fillNode = document.createElement('v:fill'); + fillNode.setAttribute('type', 'gradient'); + fillNode.setAttribute('color', 'white'); + fillNode.setAttribute('color2', 'white'); + fillNode.setAttribute('opacity', '90%'); + fillNode.setAttribute('o:opacity2', '15%'); + fillNode.setAttribute('angle', '180'); + + this.node.glassOverlay.appendChild(fillNode); + this.node.appendChild(this.node.glassOverlay); + } + + var size = 0.4; + + // TODO: Mask with rectangle or rounded rectangle of label + var b = this.bounds; + var sw = Math.ceil(this.strokewidth * this.scale / 2 + 1); + var d = 'm ' + (-sw) + ' ' + (-sw) + ' l ' + (-sw) + ' ' + Math.round(b.height * size) + + ' c ' + Math.round(b.width * 0.3) + ' ' + Math.round(b.height * 0.6) + + ' ' + Math.round(b.width * 0.7) + ' ' + Math.round(b.height * 0.6) + + ' ' + Math.round(b.width + sw) + ' ' + Math.round(b.height * size) + + ' l '+Math.round(b.width + sw)+' ' + (-sw) + ' x e'; + this.node.glassOverlay.style.position = 'absolute'; + this.node.glassOverlay.style.width = b.width + 'px'; + this.node.glassOverlay.style.height = b.height + 'px'; + this.node.glassOverlay.setAttribute('coordsize', + Math.round(this.bounds.width) + ',' + + Math.round(this.bounds.height)); + this.node.glassOverlay.setAttribute('path', d); + } + else if (this.node.glassOverlay != null) + { + this.node.glassOverlay.parentNode.removeChild(this.node.glassOverlay); + this.node.glassOverlay = null; + } +}; + +/** + * Function: updateSvgGlassPane + * + * Draws the glass overlay if mxConstants.STYLE_GLASS is 1. + */ +mxShape.prototype.updateSvgGlassPane = function() +{ + if (this.node.nodeName == 'g' && this.style != null && + mxUtils.getValue(this.style, mxConstants.STYLE_GLASS, 0) == 1) + { + // Glass overlay + if (this.node.glassOverlay == null) + { + // Glass overlay gradient + if (this.node.ownerSVGElement.glassGradient == null) + { + // Creates glass overlay gradient + var glassGradient = document.createElementNS(mxConstants.NS_SVG, 'linearGradient'); + glassGradient.setAttribute('x1', '0%'); + glassGradient.setAttribute('y1', '0%'); + glassGradient.setAttribute('x2', '0%'); + glassGradient.setAttribute('y2', '100%'); + + var stop1 = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('style', 'stop-color:#ffffff;stop-opacity:0.9'); + glassGradient.appendChild(stop1); + + var stop2 = document.createElementNS(mxConstants.NS_SVG, 'stop'); + stop2.setAttribute('offset', '100%'); + stop2.setAttribute('style', 'stop-color:#ffffff;stop-opacity:0.1'); + glassGradient.appendChild(stop2); + + // Finds a unique ID for the gradient + var prefix = 'mx-glass-gradient-'; + var counter = 0; + + while (document.getElementById(prefix+counter) != null) + { + counter++; + } + + glassGradient.setAttribute('id', prefix+counter); + this.node.ownerSVGElement.appendChild(glassGradient); + this.node.ownerSVGElement.glassGradient = glassGradient; + } + + // Creates glass overlay + this.node.glassOverlay = document.createElementNS(mxConstants.NS_SVG, 'path'); + // LATER: Not sure what the behaviour is for mutiple SVG elements in page. + // Probably its possible that this points to an element in another SVG + // node which when removed will result in an undefined background. + var id = this.node.ownerSVGElement.glassGradient.getAttribute('id'); + this.node.glassOverlay.setAttribute('style', 'fill:url(#'+id+');'); + this.node.appendChild(this.node.glassOverlay); + } + + var size = 0.4; + + // TODO: Mask with rectangle or rounded rectangle of label + var b = this.bounds; + var sw = Math.ceil(this.strokewidth * this.scale / 2); + var d = 'm ' + (b.x - sw) + ',' + (b.y - sw) + + ' L ' + (b.x - sw) + ',' + (b.y + b.height * size) + + ' Q '+ (b.x + b.width * 0.5) + ',' + (b.y + b.height * 0.7) + ' '+ + (b.x + b.width + sw) + ',' + (b.y + b.height * size) + + ' L ' + (b.x + b.width + sw) + ',' + (b.y - sw) + ' z'; + this.node.glassOverlay.setAttribute('d', d); + } + else if (this.node.glassOverlay != null) + { + this.node.glassOverlay.parentNode.removeChild(this.node.glassOverlay); + this.node.glassOverlay = null; + } +}; + +/** + * Function: redrawVml + * + * Redraws this VML shape by invoking <updateVmlShape> on this.node. + */ +mxShape.prototype.redrawVml = function() +{ + this.node.style.visibility = 'hidden'; + this.updateVmlShape(this.node); + this.updateVmlGlassPane(); + this.node.style.visibility = 'visible'; +}; + +/** + * Function: redrawHtml + * + * Redraws this HTML shape by invoking <updateHtmlShape> on this.node. + */ +mxShape.prototype.redrawHtml = function() +{ + this.updateHtmlShape(this.node); +}; + +/** + * Function: getRotation + * + * Returns the current rotation including direction. + */ +mxShape.prototype.getRotation = function() +{ + var rot = this.rotation || 0; + + // Default direction is east (ignored if rotation exists) + if (this.direction != null) + { + if (this.direction == 'north') + { + rot += 270; + } + else if (this.direction == 'west') + { + rot += 180; + } + else if (this.direction == 'south') + { + rot += 90; + } + } + + return rot; +}; + +/** + * Function: createPath + * + * Creates an <mxPath> for the specified format and origin. The path object is + * then passed to <redrawPath> and <mxPath.getPath> is returned. + */ +mxShape.prototype.createPath = function(arg) +{ + var x = this.bounds.x; + var y = this.bounds.y; + var w = this.bounds.width; + var h = this.bounds.height; + var dx = 0; + var dy = 0; + + // Inverts bounds for stencils which are rotated 90 or 270 degrees + if (this.direction == 'north' || this.direction == 'south') + { + dx = (w - h) / 2; + dy = (h - w) / 2; + x += dx; + y += dy; + var tmp = w; + w = h; + h = tmp; + } + + var rotation = this.getRotation(); + var path = null; + + if (this.dialect == mxConstants.DIALECT_SVG) + { + path = new mxPath('svg'); + path.setTranslate(x, y); + + // Adds rotation as a separate transform + if (rotation != 0) + { + var cx = this.bounds.getCenterX(); + var cy = this.bounds.getCenterY(); + var transform = 'rotate(' + rotation + ' ' + cx + ' ' + cy + ')'; + + if (this.innerNode != null) + { + this.innerNode.setAttribute('transform', transform); + } + + if (this.foreground != null) + { + this.foreground.setAttribute('transform', transform); + } + + // Shadow needs different transform so that it ends up on the correct side + if (this.shadowNode != null) + { + this.shadowNode.setAttribute('transform', this.getSvgShadowTransform() + ' ' + transform); + } + } + } + else + { + path = new mxPath('vml'); + path.setTranslate(dx, -dx); + path.scale = this.vmlScale; + + if (rotation != 0) + { + this.node.style.rotation = rotation; + } + } + + this.redrawPath(path, x, y, w, h, arg); + + return path.getPath(); +}; + +/** + * Function: redrawPath + * + * Draws the path for this shape. This implementation is empty. See + * <mxActor> and <mxCylinder> for implementations. + */ +mxShape.prototype.redrawPath = function(path, x, y, w, h) +{ + // do nothing +}; |