summaryrefslogtreecommitdiff
path: root/src/js/util/mxImageExport.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/util/mxImageExport.js')
-rw-r--r--src/js/util/mxImageExport.js1412
1 files changed, 1412 insertions, 0 deletions
diff --git a/src/js/util/mxImageExport.js b/src/js/util/mxImageExport.js
new file mode 100644
index 0000000..dcbcf9a
--- /dev/null
+++ b/src/js/util/mxImageExport.js
@@ -0,0 +1,1412 @@
+/**
+ * $Id: mxImageExport.js,v 1.47 2012-09-24 14:54:32 gaudenz Exp $
+ * Copyright (c) 2006-2010, JGraph Ltd
+ */
+/**
+ * Class: mxImageExport
+ *
+ * Creates a new image export instance to be used with an export canvas. Here
+ * is an example that uses this class to create an image via a backend using
+ * <mxXmlExportCanvas>.
+ *
+ * (code)
+ * var xmlDoc = mxUtils.createXmlDocument();
+ * var root = xmlDoc.createElement('output');
+ * xmlDoc.appendChild(root);
+ *
+ * var xmlCanvas = new mxXmlCanvas2D(root);
+ * var imgExport = new mxImageExport();
+ * imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas);
+ *
+ * var bounds = graph.getGraphBounds();
+ * var w = Math.ceil(bounds.x + bounds.width);
+ * var h = Math.ceil(bounds.y + bounds.height);
+ *
+ * var xml = mxUtils.getXml(root);
+ * new mxXmlRequest('export', 'format=png&w=' + w +
+ * '&h=' + h + '&bg=#F9F7ED&xml=' + encodeURIComponent(xml))
+ * .simulate(document, '_blank');
+ * (end)
+ *
+ * In order to export images for a graph whose container is not visible or not
+ * part of the DOM, the following workaround can be used to compute the size of
+ * the labels.
+ *
+ * (code)
+ * mxText.prototype.getTableSize = function(table)
+ * {
+ * var oldParent = table.parentNode;
+ *
+ * document.body.appendChild(table);
+ * var size = new mxRectangle(0, 0, table.offsetWidth, table.offsetHeight);
+ * oldParent.appendChild(table);
+ *
+ * return size;
+ * };
+ * (end)
+ *
+ * Constructor: mxImageExport
+ *
+ * Constructs a new image export.
+ */
+function mxImageExport()
+{
+ this.initShapes();
+ this.initMarkers();
+};
+
+/**
+ * Variable: includeOverlays
+ *
+ * Specifies if overlays should be included in the export. Default is false.
+ */
+mxImageExport.prototype.includeOverlays = false;
+
+/**
+ * Variable: glassSize
+ *
+ * Reference to the thread while the animation is running.
+ */
+mxImageExport.prototype.glassSize = 0.4;
+
+/**
+ * Variable: shapes
+ *
+ * Holds implementations for the built-in shapes.
+ */
+mxImageExport.prototype.shapes = null;
+
+/**
+ * Variable: markers
+ *
+ * Holds implementations for the built-in markers.
+ */
+mxImageExport.prototype.markers = null;
+
+/**
+ * Function: drawState
+ *
+ * Draws the given state and all its descendants to the given canvas.
+ */
+mxImageExport.prototype.drawState = function(state, canvas)
+{
+ if (state != null)
+ {
+ if (state.shape != null)
+ {
+ var shape = (state.shape.stencil != null) ?
+ state.shape.stencil :
+ this.shapes[state.style[mxConstants.STYLE_SHAPE]];
+
+ if (shape == null)
+ {
+ // Checks if there is a custom shape
+ if (typeof(state.shape.redrawPath) == 'function')
+ {
+ shape = this.createShape(state, canvas);
+ }
+ // Uses a rectangle for all vertices where no shape can be found
+ else if (state.view.graph.getModel().isVertex(state.cell))
+ {
+ shape = this.shapes['rectangle'];
+ }
+ }
+
+ if (shape != null)
+ {
+ this.drawShape(state, canvas, shape);
+
+ if (this.includeOverlays)
+ {
+ this.drawOverlays(state, canvas);
+ }
+ }
+ }
+
+ var graph = state.view.graph;
+ var childCount = graph.model.getChildCount(state.cell);
+
+ for (var i = 0; i < childCount; i++)
+ {
+ var childState = graph.view.getState(graph.model.getChildAt(state.cell, i));
+ this.drawState(childState, canvas);
+ }
+ }
+};
+
+/**
+ * Function: createShape
+ *
+ * Creates a shape wrapper for the custom shape in the given cell state and
+ * links its output to the given canvas.
+ */
+mxImageExport.prototype.createShape = function(state, canvas)
+{
+ return {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ var path =
+ {
+ translate: new mxPoint(bounds.x, bounds.y),
+ moveTo: function(x, y)
+ {
+ canvas.moveTo(this.translate.x + x, this.translate.y + y);
+ },
+ lineTo: function(x, y)
+ {
+ canvas.lineTo(this.translate.x + x, this.translate.y + y);
+ },
+ quadTo: function(x1, y1, x, y)
+ {
+ canvas.quadTo(this.translate.x + x1, this.translate.y + y1, this.translate.x + x, this.translate.y + y);
+ },
+ curveTo: function(x1, y1, x2, y2, x, y)
+ {
+ canvas.curveTo(this.translate.x + x1, this.translate.y + y1, this.translate.x + x2, this.translate.y + y2, this.translate.x + x, this.translate.y + y);
+ },
+ end: function()
+ {
+ // do nothing
+ },
+ close: function()
+ {
+ canvas.close();
+ }
+ };
+
+ if (!background)
+ {
+ canvas.fillAndStroke();
+ }
+
+ // LATER: Remove empty path if shape does not implement foreground, add shadow/clipping
+ canvas.begin();
+ state.shape.redrawPath.call(state.shape, path, bounds.x, bounds.y, bounds.width, bounds.height, !background);
+
+ if (!background)
+ {
+ canvas.fillAndStroke();
+ }
+
+ return true;
+ }
+ };
+};
+
+/**
+ * Function: drawOverlays
+ *
+ * Draws the overlays for the given state. This is called if <includeOverlays>
+ * is true.
+ */
+mxImageExport.prototype.drawOverlays = function(state, canvas)
+{
+ if (state.overlays != null)
+ {
+ state.overlays.visit(function(id, shape)
+ {
+ var bounds = shape.bounds;
+
+ if (bounds != null)
+ {
+ canvas.image(bounds.x, bounds.y, bounds.width, bounds.height, shape.image);
+ }
+ });
+ }
+};
+
+/**
+ * Function: drawShape
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.drawShape = function(state, canvas, shape)
+{
+ var rotation = mxUtils.getNumber(state.style, mxConstants.STYLE_ROTATION, 0);
+ var direction = mxUtils.getValue(state.style, mxConstants.STYLE_DIRECTION, null);
+
+ // New styles for shape flipping the stencil
+ var flipH = state.style[mxConstants.STYLE_STENCIL_FLIPH];
+ var flipV = state.style[mxConstants.STYLE_STENCIL_FLIPV];
+
+ if (flipH ? !flipV : flipV)
+ {
+ rotation *= -1;
+ }
+
+ // Default direction is east (ignored if rotation exists)
+ if (direction != null)
+ {
+ if (direction == 'north')
+ {
+ rotation += 270;
+ }
+ else if (direction == 'west')
+ {
+ rotation += 180;
+ }
+ else if (direction == 'south')
+ {
+ rotation += 90;
+ }
+ }
+
+ if (flipH && flipV)
+ {
+ rotation += 180;
+ flipH = false;
+ flipV = false;
+ }
+
+ // Saves the global state for each cell
+ canvas.save();
+
+ // Adds rotation and horizontal/vertical flipping
+ // FIXME: Rotation and stencil flip only supported for stencil shapes
+ rotation = rotation % 360;
+
+ if (rotation != 0 || flipH || flipV)
+ {
+ canvas.rotate(rotation, flipH, flipV, state.getCenterX(), state.getCenterY());
+ }
+
+ // Note: Overwritten in mxStencil.paintShape (can depend on aspect)
+ var scale = state.view.scale;
+ var sw = mxUtils.getNumber(state.style, mxConstants.STYLE_STROKEWIDTH, 1) * scale;
+ canvas.setStrokeWidth(sw);
+
+ var sw2 = sw / 2;
+ var bg = this.getBackgroundBounds(state);
+
+ // Stencils will rotate the bounds as required
+ if (state.shape.stencil == null && (direction == 'south' || direction == 'north'))
+ {
+ var dx = (bg.width - bg.height) / 2;
+ bg.x += dx;
+ bg.y += -dx;
+ var tmp = bg.width;
+ bg.width = bg.height;
+ bg.height = tmp;
+ }
+
+ var bb = new mxRectangle(bg.x - sw2, bg.y - sw2, bg.width + sw, bg.height + sw);
+ var alpha = mxUtils.getValue(state.style, mxConstants.STYLE_OPACITY, 100) / 100;
+
+ var shp = state.style[mxConstants.STYLE_SHAPE];
+ var imageShape = shp == mxConstants.SHAPE_IMAGE;
+ var gradientColor = (imageShape) ? null : mxUtils.getValue(state.style, mxConstants.STYLE_GRADIENTCOLOR);
+
+ // Converts colors with special keyword none to null
+ if (gradientColor == mxConstants.NONE)
+ {
+ gradientColor = null;
+ }
+
+ var fcKey = (imageShape) ? mxConstants.STYLE_IMAGE_BACKGROUND : mxConstants.STYLE_FILLCOLOR;
+ var fillColor = mxUtils.getValue(state.style, fcKey, null);
+
+ if (fillColor == mxConstants.NONE)
+ {
+ fillColor = null;
+ }
+
+ var scKey = (imageShape) ? mxConstants.STYLE_IMAGE_BORDER : mxConstants.STYLE_STROKECOLOR;
+ var strokeColor = mxUtils.getValue(state.style, scKey, null);
+
+ if (strokeColor == mxConstants.NONE)
+ {
+ strokeColor = null;
+ }
+
+ var glass = (fillColor != null && (shp == mxConstants.SHAPE_LABEL || shp == mxConstants.SHAPE_RECTANGLE));
+
+ // Draws the shadow if the fillColor is not transparent
+ if (mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, false))
+ {
+ this.drawShadow(canvas, state, shape, rotation, flipH, flipV, bg, alpha, fillColor != null);
+ }
+
+ canvas.setAlpha(alpha);
+
+ // Sets the dashed state
+ if (mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') == '1')
+ {
+ canvas.setDashed(true);
+
+ // Supports custom dash patterns
+ var dash = state.style['dashPattern'];
+
+ if (dash != null)
+ {
+ canvas.setDashPattern(dash);
+ }
+ }
+
+ // Draws background and foreground
+ if (strokeColor != null || fillColor != null)
+ {
+ if (strokeColor != null)
+ {
+ canvas.setStrokeColor(strokeColor);
+ }
+
+ if (fillColor != null)
+ {
+ if (gradientColor != null && gradientColor != 'transparent')
+ {
+ canvas.setGradient(fillColor, gradientColor, bg.x, bg.y, bg.width, bg.height, direction);
+ }
+ else
+ {
+ canvas.setFillColor(fillColor);
+ }
+ }
+
+ // Draws background and foreground of shape
+ glass = shape.drawShape(canvas, state, bg, true, false) && glass;
+ shape.drawShape(canvas, state, bg, false, false);
+ }
+
+ // Draws the glass effect
+ // Requires background in generic shape for clipping
+ if (glass && mxUtils.getValue(state.style, mxConstants.STYLE_GLASS, 0) == 1)
+ {
+ this.drawGlass(state, canvas, bb, shape, this.glassSize);
+ }
+
+ // Draws the image (currently disabled for everything but image and label shapes)
+ if (imageShape || shp == mxConstants.SHAPE_LABEL)
+ {
+ var src = state.view.graph.getImage(state);
+
+ if (src != null)
+ {
+ var imgBounds = this.getImageBounds(state);
+
+ if (imgBounds != null)
+ {
+ this.drawImage(state, canvas, imgBounds, src);
+ }
+ }
+ }
+
+ // Restores canvas state
+ canvas.restore();
+
+ // Draws the label (label has separate rotation)
+ var txt = state.text;
+
+ // Does not use mxCellRenderer.getLabelValue to avoid conversion of HTML entities for VML
+ var label = state.view.graph.getLabel(state.cell);
+
+ if (txt != null && label != null && label.length > 0)
+ {
+ canvas.save();
+ canvas.setAlpha(mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_OPACITY, 100) / 100);
+ var bounds = new mxRectangle(txt.boundingBox.x, txt.boundingBox.y, txt.boundingBox.width, txt.boundingBox.height);
+ var vert = mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, 1) == 0;
+
+ // Vertical error offset
+ bounds.y += 2;
+
+ if (vert)
+ {
+ if (txt.dialect != mxConstants.DIALECT_SVG)
+ {
+ var cx = bounds.x + bounds.width / 2;
+ var cy = bounds.y + bounds.height / 2;
+ var tmp = bounds.width;
+ bounds.width = bounds.height;
+ bounds.height = tmp;
+ bounds.x = cx - bounds.width / 2;
+ bounds.y = cy - bounds.height / 2;
+ }
+ else if (txt.dialect == mxConstants.DIALECT_SVG)
+ {
+ // Workarounds for different label bounding boxes (mostly ignoring rotation).
+ // LATER: Fix in mxText so that the bounding box is consistent and rotated.
+ // TODO: Check non-center/middle-aligned vertical labels in VML for IE8.
+ var b = state.y + state.height;
+ var cx = bounds.getCenterX() - state.x;
+ var cy = bounds.getCenterY() - state.y;
+
+ var y = b - cx - bounds.height / 2;
+ bounds.x = state.x + cy - bounds.width / 2;
+ bounds.y = y;
+ //bounds.x -= state.height / 2 - state.width / 2;
+ //bounds.y -= state.width / 2 - state.height / 2;
+ }
+ }
+
+ this.drawLabelBackground(state, canvas, bounds, vert);
+ this.drawLabel(state, canvas, bounds, vert, label);
+ canvas.restore();
+ }
+};
+
+/**
+ * Function: drawGlass
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.drawShadow = function(canvas, state, shape, rotation, flipH, flipV, bounds, alpha, filled)
+{
+ // Requires background in generic shape for shadow, looks like only one
+ // fillAndStroke is allowed per current path, try working around that
+ // Computes rotated shadow offset
+ var rad = rotation * Math.PI / 180;
+ var cos = Math.cos(-rad);
+ var sin = Math.sin(-rad);
+ var offset = mxUtils.getRotatedPoint(new mxPoint(mxConstants.SHADOW_OFFSET_X, mxConstants.SHADOW_OFFSET_Y), cos, sin);
+
+ if (flipH)
+ {
+ offset.x *= -1;
+ }
+
+ if (flipV)
+ {
+ offset.y *= -1;
+ }
+
+ // TODO: Use save/restore instead of negative offset to restore (requires fix for HTML canvas)
+ canvas.translate(offset.x, offset.y);
+
+ // Returns true if a shadow has been painted (path has been created)
+ if (shape.drawShape(canvas, state, bounds, true, true))
+ {
+ canvas.setAlpha(mxConstants.SHADOW_OPACITY * alpha);
+ canvas.shadow(mxConstants.SHADOWCOLOR, filled);
+ }
+
+ canvas.translate(-offset.x, -offset.y);
+};
+
+/**
+ * Function: drawGlass
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.drawGlass = function(state, canvas, bounds, shape, size)
+{
+ // LATER: Clipping region should include stroke
+ if (shape.drawShape(canvas, state, bounds, true, false))
+ {
+ canvas.save();
+ canvas.clip();
+ canvas.setGlassGradient(bounds.x, bounds.y, bounds.width, bounds.height);
+
+ canvas.begin();
+ canvas.moveTo(bounds.x, bounds.y);
+ canvas.lineTo(bounds.x, (bounds.y + bounds.height * size));
+ canvas.quadTo((bounds.x + bounds.width * 0.5),
+ (bounds.y + bounds.height * 0.7), bounds.x + bounds.width,
+ (bounds.y + bounds.height * size));
+ canvas.lineTo(bounds.x + bounds.width, bounds.y);
+ canvas.close();
+
+ canvas.fill();
+ canvas.restore();
+ }
+};
+
+/**
+ * Function: drawImage
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.drawImage = function(state, canvas, bounds, image)
+{
+ var aspect = mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE_ASPECT, 1) == 1;
+ var flipH = mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE_FLIPH, 0) == 1;
+ var flipV = mxUtils.getValue(state.style, mxConstants.STYLE_IMAGE_FLIPV, 0) == 1;
+
+ canvas.image(bounds.x, bounds.y, bounds.width, bounds.height, image, aspect, flipH, flipV);
+};
+
+/**
+ * Function: drawLabelBackground
+ *
+ * Draws background for the label of the given state to the given canvas.
+ */
+mxImageExport.prototype.drawLabelBackground = function(state, canvas, bounds, vert)
+{
+ var stroke = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_BORDERCOLOR);
+ var fill = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_BACKGROUNDCOLOR);
+
+ if (stroke == mxConstants.NONE)
+ {
+ stroke = null;
+ }
+
+ if (fill == mxConstants.NONE)
+ {
+ fill = null;
+ }
+
+ if (stroke != null || fill != null)
+ {
+ var x = bounds.x;
+ var y = bounds.y - mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_PADDING, 0);
+ var w = bounds.width;
+ var h = bounds.height;
+
+ if (vert)
+ {
+ x += (w - h) / 2;
+ y += (h - w) / 2;
+ var tmp = w;
+ w = h;
+ h = tmp;
+ }
+
+ if (fill != null)
+ {
+ canvas.setFillColor(fill);
+ }
+
+ if (stroke != null)
+ {
+ canvas.setStrokeColor(stroke);
+ canvas.setStrokeWidth(1);
+ canvas.setDashed(false);
+ }
+
+ canvas.rect(x, y, w, h);
+
+ if (fill != null && stroke != null)
+ {
+ canvas.fillAndStroke();
+ }
+ else if (fill != null)
+ {
+ canvas.fill();
+ }
+ else if (stroke != null)
+ {
+ canvas.stroke();
+ }
+ }
+};
+
+/**
+ * Function: drawLabel
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.drawLabel = function(state, canvas, bounds, vert, str)
+{
+ var scale = state.view.scale;
+
+ // Applies color
+ canvas.setFontColor(mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, '#000000'));
+
+ // Applies font settings
+ canvas.setFontFamily(mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY,
+ mxConstants.DEFAULT_FONTFAMILY));
+ canvas.setFontStyle(mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0));
+ canvas.setFontSize(mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE,
+ mxConstants.DEFAULT_FONTSIZE) * scale);
+
+ var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT);
+
+ // Uses null alignment for default values (valign default is 'top' which is fine)
+ if (align == 'left')
+ {
+ align = null;
+ }
+
+ var y = bounds.y - mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_PADDING, 0);
+ var wrap = state.view.graph.isWrapping(state.cell);
+ var html = state.view.graph.isHtmlLabel(state.cell);
+
+ // Replaces linefeeds in HTML markup to match the display output
+ if (html && mxText.prototype.replaceLinefeeds)
+ {
+ str = str.replace(/\n/g, '<br/>');
+ }
+
+ canvas.text(bounds.x, y, bounds.width, bounds.height, str, align, null, vert, wrap, (html) ? 'html' : '');
+};
+
+/**
+ * Function: getBackgroundBounds
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.getBackgroundBounds = function(state)
+{
+ if (state.style[mxConstants.STYLE_SHAPE] == mxConstants.SHAPE_SWIMLANE)
+ {
+ var scale = state.view.scale;
+ var start = mxUtils.getValue(state.style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE) * scale;
+ var w = state.width;
+ var h = state.height;
+
+ if (mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, true))
+ {
+ h = start;
+ }
+ else
+ {
+ w = start;
+ }
+
+ return new mxRectangle(state.x, state.y, Math.min(state.width, w), Math.min(state.height, h));
+ }
+ else
+ {
+ return new mxRectangle(state.x, state.y, state.width, state.height);
+ }
+};
+
+/**
+ * Function: getImageBounds
+ *
+ * Draws the given state to the given canvas.
+ */
+mxImageExport.prototype.getImageBounds = function(state)
+{
+ var bounds = new mxRectangle(state.x, state.y, state.width, state.height);
+ var style = state.style;
+
+ if (mxUtils.getValue(style, mxConstants.STYLE_SHAPE) != mxConstants.SHAPE_IMAGE)
+ {
+ var imgAlign = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_ALIGN, mxConstants.ALIGN_LEFT);
+ var imgValign = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE);
+ var imgWidth = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_WIDTH, mxConstants.DEFAULT_IMAGESIZE);
+ var imgHeight = mxUtils.getValue(style, mxConstants.STYLE_IMAGE_HEIGHT, mxConstants.DEFAULT_IMAGESIZE);
+ var spacing = mxUtils.getValue(style, mxConstants.STYLE_SPACING, 2);
+
+ if (imgAlign == mxConstants.ALIGN_CENTER)
+ {
+ bounds.x += (bounds.width - imgWidth) / 2;
+ }
+ else if (imgAlign == mxConstants.ALIGN_RIGHT)
+ {
+ bounds.x += bounds.width - imgWidth - spacing - 2;
+ }
+ else
+ // LEFT
+ {
+ bounds.x += spacing + 4;
+ }
+
+ if (imgValign == mxConstants.ALIGN_TOP)
+ {
+ bounds.y += spacing;
+ }
+ else if (imgValign == mxConstants.ALIGN_BOTTOM)
+ {
+ bounds.y += bounds.height - imgHeight - spacing;
+ }
+ else
+ // MIDDLE
+ {
+ bounds.y += (bounds.height - imgHeight) / 2;
+ }
+
+ bounds.width = imgWidth;
+ bounds.height = imgHeight;
+ }
+
+ return bounds;
+};
+
+/**
+ * Function: drawMarker
+ *
+ * Initializes the built-in shapes.
+ */
+mxImageExport.prototype.drawMarker = function(canvas, state, source)
+{
+ var offset = null;
+
+ // Computes the norm and the inverse norm
+ var pts = state.absolutePoints;
+ var n = pts.length;
+
+ var p0 = (source) ? pts[1] : pts[n - 2];
+ var pe = (source) ? pts[0] : pts[n - 1];
+
+ var dx = pe.x - p0.x;
+ var dy = pe.y - p0.y;
+
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
+
+ var unitX = dx / dist;
+ var unitY = dy / dist;
+
+ var size = mxUtils.getValue(state.style, (source) ?
+ mxConstants.STYLE_STARTSIZE :
+ mxConstants.STYLE_ENDSIZE,
+ mxConstants.DEFAULT_MARKERSIZE);
+
+ // Allow for stroke width in the end point used and the
+ // orthogonal vectors describing the direction of the marker
+ // TODO: Should get strokewidth from canvas (same for strokecolor)
+ var sw = mxUtils.getValue(state.style, mxConstants.STYLE_STROKEWIDTH, 1);
+
+ pe = pe.clone();
+
+ var type = mxUtils.getValue(state.style, (source) ?
+ mxConstants.STYLE_STARTARROW :
+ mxConstants.STYLE_ENDARROW);
+ var f = this.markers[type];
+
+ if (f != null)
+ {
+ offset = f(canvas, state, type, pe, unitX, unitY, size, source, sw);
+ }
+
+ return offset;
+};
+
+/**
+ * Function: initShapes
+ *
+ * Initializes the built-in shapes.
+ */
+mxImageExport.prototype.initShapes = function()
+{
+ this.shapes = [];
+
+ // Implements the rectangle and rounded rectangle shape
+ this.shapes['rectangle'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ // Paints the shape
+ if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false))
+ {
+ var f = mxUtils.getValue(state.style, mxConstants.STYLE_ARCSIZE, mxConstants.RECTANGLE_ROUNDING_FACTOR * 100) / 100;
+ var r = Math.min(bounds.width * f, bounds.height * f);
+ canvas.roundrect(bounds.x, bounds.y, bounds.width, bounds.height, r, r);
+ }
+ else
+ {
+ canvas.rect(bounds.x, bounds.y, bounds.width, bounds.height);
+ }
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+ // Implements the swimlane shape
+ this.shapes['swimlane'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ if (mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false))
+ {
+ var r = Math.min(bounds.width * mxConstants.RECTANGLE_ROUNDING_FACTOR,
+ bounds.height * mxConstants.RECTANGLE_ROUNDING_FACTOR);
+ canvas.roundrect(bounds.x, bounds.y, bounds.width, bounds.height, r, r);
+ }
+ else
+ {
+ canvas.rect(bounds.x, bounds.y, bounds.width, bounds.height);
+ }
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ canvas.begin();
+
+ var x = state.x;
+ var y = state.y;
+ var w = state.width;
+ var h = state.height;
+
+ if (mxUtils.getValue(state.style, mxConstants.STYLE_HORIZONTAL, 1) == 0)
+ {
+ x += bounds.width;
+ w -= bounds.width;
+
+ canvas.moveTo(x, y);
+ canvas.lineTo(x + w, y);
+ canvas.lineTo(x + w, y + h);
+ canvas.lineTo(x, y + h);
+ }
+ else
+ {
+ y += bounds.height;
+ h -= bounds.height;
+
+ canvas.moveTo(x, y);
+ canvas.lineTo(x, y + h);
+ canvas.lineTo(x + w, y + h);
+ canvas.lineTo(x + w, y);
+ }
+
+ canvas.stroke();
+ }
+ }
+ };
+
+ this.shapes['image'] = this.shapes['rectangle'];
+ this.shapes['label'] = this.shapes['rectangle'];
+
+ var imageExport = this;
+
+ this.shapes['connector'] =
+ {
+ translatePoint: function(points, index, offset)
+ {
+ if (offset != null)
+ {
+ var pt = points[index].clone();
+ pt.x += offset.x;
+ pt.y += offset.y;
+ points[index] = pt;
+ }
+ },
+
+ drawShape: function(canvas, state, bounds, background, shadow)
+ {
+ if (background)
+ {
+ var rounded = mxUtils.getValue(state.style, mxConstants.STYLE_ROUNDED, false);
+ var arcSize = mxConstants.LINE_ARCSIZE / 2;
+
+ // Does not draw the markers in the shadow to match the display
+ canvas.setFillColor((shadow) ? mxConstants.NONE : mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, "#000000"));
+ canvas.setDashed(false);
+ var pts = state.absolutePoints.slice();
+ this.translatePoint(pts, 0, imageExport.drawMarker(canvas, state, true));
+ this.translatePoint(pts, pts.length - 1, imageExport.drawMarker(canvas, state, false));
+ canvas.setDashed(mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') == '1');
+
+ var pt = pts[0];
+ var pe = pts[pts.length - 1];
+ canvas.begin();
+ canvas.moveTo(pt.x, pt.y);
+
+ // Draws the line segments
+ for (var i = 1; i < pts.length - 1; i++)
+ {
+ var tmp = pts[i];
+ var dx = pt.x - tmp.x;
+ var dy = pt.y - tmp.y;
+
+ if ((rounded && i < pts.length - 1) && (dx != 0 || dy != 0))
+ {
+ // 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(arcSize, dist / 2) / dist;
+ var ny1 = dy * Math.min(arcSize, dist / 2) / dist;
+
+ var x1 = tmp.x + nx1;
+ var y1 = tmp.y + ny1;
+ canvas.lineTo(x1, y1);
+
+ // 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 next = pts[i + 1];
+ dx = next.x - tmp.x;
+ dy = next.y - tmp.y;
+
+ dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
+ var nx2 = dx * Math.min(arcSize, dist / 2) / dist;
+ var ny2 = dy * Math.min(arcSize, dist / 2) / dist;
+
+ var x2 = tmp.x + nx2;
+ var y2 = tmp.y + ny2;
+
+ canvas.curveTo(tmp.x, tmp.y, tmp.x, tmp.y, x2, y2);
+ tmp = new mxPoint(x2, y2);
+ }
+ else
+ {
+ canvas.lineTo(tmp.x, tmp.y);
+ }
+
+ pt = tmp;
+ }
+
+ canvas.lineTo(pe.x, pe.y);
+ canvas.stroke();
+
+ return true;
+ }
+ else
+ {
+ // no foreground
+ }
+ }
+ };
+
+ this.shapes['arrow'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ // Geometry of arrow
+ var spacing = mxConstants.ARROW_SPACING;
+ var width = mxConstants.ARROW_WIDTH;
+ var arrow = mxConstants.ARROW_SIZE;
+
+ // Base vector (between end points)
+ var pts = state.absolutePoints;
+ var p0 = pts[0];
+ var pe = pts[pts.length - 1];
+ var dx = pe.x - p0.x;
+ var dy = pe.y - p0.y;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ var length = dist - 2 * spacing - arrow;
+
+ // Computes the norm and the inverse norm
+ var nx = dx / dist;
+ var ny = dy / dist;
+ var basex = length * nx;
+ var basey = length * ny;
+ var floorx = width * ny/3;
+ var floory = -width * nx/3;
+
+ // Computes points
+ var p0x = p0.x - floorx / 2 + spacing * nx;
+ var p0y = p0.y - floory / 2 + spacing * ny;
+ var p1x = p0x + floorx;
+ var p1y = p0y + floory;
+ var p2x = p1x + basex;
+ var p2y = p1y + basey;
+ var p3x = p2x + floorx;
+ var p3y = p2y + floory;
+ // p4 not necessary
+ var p5x = p3x - 3 * floorx;
+ var p5y = p3y - 3 * floory;
+
+ canvas.begin();
+ canvas.moveTo(p0x, p0y);
+ canvas.lineTo(p1x, p1y);
+ canvas.lineTo(p2x, p2y);
+ canvas.lineTo(p3x, p3y);
+ canvas.lineTo(pe.x - spacing * nx, pe.y - spacing * ny);
+ canvas.lineTo(p5x, p5y);
+ canvas.lineTo(p5x + floorx, p5y + floory);
+ canvas.close();
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+ this.shapes['cylinder'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ return false;
+ }
+ else
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+ var dy = Math.min(mxCylinder.prototype.maxHeight, Math.floor(h / 5));
+
+ canvas.begin();
+ canvas.moveTo(x, y + dy);
+ canvas.curveTo(x, y - dy / 3, x + w, y - dy / 3, x + w, y + dy);
+ canvas.lineTo(x + w, y + h - dy);
+ canvas.curveTo(x + w, y + h + dy / 3, x, y + h + dy / 3, x, y + h - dy);
+ canvas.close();
+ canvas.fillAndStroke();
+
+ canvas.begin();
+ canvas.moveTo(x, y + dy);
+ canvas.curveTo(x, y + 2 * dy, x + w, y + 2 * dy, x + w, y + dy);
+ canvas.stroke();
+ }
+ }
+ };
+
+ this.shapes['line'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ return false;
+ }
+ else
+ {
+ canvas.begin();
+
+ var mid = state.getCenterY();
+ canvas.moveTo(bounds.x, mid);
+ canvas.lineTo(bounds.x + bounds.width, mid);
+
+ canvas.stroke();
+ }
+ }
+ };
+
+ this.shapes['ellipse'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ canvas.ellipse(bounds.x, bounds.y, bounds.width, bounds.height);
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+ this.shapes['doubleEllipse'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ if (background)
+ {
+ canvas.ellipse(x, y, w, h);
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+
+ var inset = Math.min(4, Math.min(w / 5, h / 5));
+ x += inset;
+ y += inset;
+ w -= 2 * inset;
+ h -= 2 * inset;
+
+ if (w > 0 && h > 0)
+ {
+ canvas.ellipse(x, y, w, h);
+ }
+
+ canvas.stroke();
+ }
+ }
+ };
+
+ this.shapes['triangle'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+ canvas.begin();
+ canvas.moveTo(x, y);
+ canvas.lineTo(x + w, y + h / 2);
+ canvas.lineTo(x, y + h);
+ canvas.close();
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+ this.shapes['rhombus'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+ var hw = w / 2;
+ var hh = h / 2;
+
+ canvas.begin();
+ canvas.moveTo(x + hw, y);
+ canvas.lineTo(x + w, y + hh);
+ canvas.lineTo(x + hw, y + h);
+ canvas.lineTo(x, y + hh);
+ canvas.close();
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+
+ };
+
+ this.shapes['hexagon'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ canvas.begin();
+ canvas.moveTo(x + 0.25 * w, y);
+ canvas.lineTo(x + 0.75 * w, y);
+ canvas.lineTo(x + w, y + 0.5 * h);
+ canvas.lineTo(x + 0.75 * w, y + h);
+ canvas.lineTo(x + 0.25 * w, y + h);
+ canvas.lineTo(x, y + 0.5 * h);
+ canvas.close();
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+ this.shapes['actor'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+ var width = w * 2 / 6;
+
+ canvas.begin();
+ canvas.moveTo(x, y + h);
+ canvas.curveTo(x, y + 3 * h / 5, x, y + 2 * h / 5, x + w / 2, y + 2 * h
+ / 5);
+ canvas.curveTo(x + w / 2 - width, y + 2 * h / 5, x + w / 2 - width, y, x
+ + w / 2, y);
+ canvas.curveTo(x + w / 2 + width, y, x + w / 2 + width, y + 2 * h / 5, x
+ + w / 2, y + 2 * h / 5);
+ canvas.curveTo(x + w, y + 2 * h / 5, x + w, y + 3 * h / 5, x + w, y + h);
+ canvas.close();
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+ this.shapes['cloud'] =
+ {
+ drawShape: function(canvas, state, bounds, background)
+ {
+ if (background)
+ {
+ var x = bounds.x;
+ var y = bounds.y;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ canvas.begin();
+ canvas.moveTo(x + 0.25 * w, y + 0.25 * h);
+ canvas.curveTo(x + 0.05 * w, y + 0.25 * h, x,
+ y + 0.5 * h, x + 0.16 * w, y + 0.55 * h);
+ canvas.curveTo(x, y + 0.66 * h, x + 0.18 * w,
+ y + 0.9 * h, x + 0.31 * w, y + 0.8 * h);
+ canvas.curveTo(x + 0.4 * w, y + h, x + 0.7 * w,
+ y + h, x + 0.8 * w, y + 0.8 * h);
+ canvas.curveTo(x + w, y + 0.8 * h, x + w,
+ y + 0.6 * h, x + 0.875 * w, y + 0.5 * h);
+ canvas.curveTo(x + w, y + 0.3 * h, x + 0.8 * w,
+ y + 0.1 * h, x + 0.625 * w, y + 0.2 * h);
+ canvas.curveTo(x + 0.5 * w, y + 0.05 * h,
+ x + 0.3 * w, y + 0.05 * h,
+ x + 0.25 * w, y + 0.25 * h);
+ canvas.close();
+
+ return true;
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+ }
+ };
+
+};
+
+/**
+ * Function: initMarkers
+ *
+ * Initializes the built-in markers.
+ */
+mxImageExport.prototype.initMarkers = function()
+{
+ this.markers = [];
+
+ var tmp = function(canvas, state, type, pe, unitX, unitY, size, source, sw)
+ {
+ // The angle of the forward facing arrow sides against the x axis is
+ // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for
+ // only half the strokewidth is processed ).
+ var endOffsetX = unitX * sw * 1.118;
+ var endOffsetY = unitY * sw * 1.118;
+
+ pe.x -= endOffsetX;
+ pe.y -= endOffsetY;
+
+ unitX = unitX * (size + sw);
+ unitY = unitY * (size + sw);
+
+ canvas.begin();
+ canvas.moveTo(pe.x, pe.y);
+ canvas.lineTo(pe.x - unitX - unitY / 2, pe.y - unitY + unitX / 2);
+
+ if (type == mxConstants.ARROW_CLASSIC)
+ {
+ canvas.lineTo(pe.x - unitX * 3 / 4, pe.y - unitY * 3 / 4);
+ }
+
+ canvas.lineTo(pe.x + unitY / 2 - unitX, pe.y - unitY - unitX / 2);
+ canvas.close();
+
+ var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL;
+
+ if (state.style[key] == 0)
+ {
+ canvas.stroke();
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+
+ var f = (type != mxConstants.ARROW_CLASSIC) ? 1 : 3 / 4;
+ return new mxPoint(-unitX * f - endOffsetX, -unitY * f - endOffsetY);
+ };
+
+ this.markers['classic'] = tmp;
+ this.markers['block'] = tmp;
+
+ this.markers['open'] = function(canvas, state, type, pe, unitX, unitY, size, source, sw)
+ {
+ // The angle of the forward facing arrow sides against the x axis is
+ // 26.565 degrees, 1/sin(26.565) = 2.236 / 2 = 1.118 ( / 2 allows for
+ // only half the strokewidth is processed ).
+ var endOffsetX = unitX * sw * 1.118;
+ var endOffsetY = unitY * sw * 1.118;
+
+ pe.x -= endOffsetX;
+ pe.y -= endOffsetY;
+
+ unitX = unitX * (size + sw);
+ unitY = unitY * (size + sw);
+
+ canvas.begin();
+ canvas.moveTo(pe.x - unitX - unitY / 2, pe.y - unitY + unitX / 2);
+ canvas.lineTo(pe.x, pe.y);
+ canvas.lineTo(pe.x + unitY / 2 - unitX, pe.y - unitY - unitX / 2);
+ canvas.stroke();
+
+ return new mxPoint(-endOffsetX * 2, -endOffsetY * 2);
+ };
+
+ this.markers['oval'] = function(canvas, state, type, pe, unitX, unitY, size, source, sw)
+ {
+ var a = size / 2;
+
+ canvas.ellipse(pe.x - a, pe.y - a, size, size);
+
+ var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL;
+
+ if (state.style[key] == 0)
+ {
+ canvas.stroke();
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+
+ return new mxPoint(-unitX / 2, -unitY / 2);
+ };
+
+ var tmp_diamond = function(canvas, state, type, pe, unitX, unitY, size, source, sw)
+ {
+ // The angle of the forward facing arrow sides against the x axis is
+ // 45 degrees, 1/sin(45) = 1.4142 / 2 = 0.7071 ( / 2 allows for
+ // only half the strokewidth is processed ). Or 0.9862 for thin diamond.
+ // Note these values and the tk variable below are dependent, update
+ // both together (saves trig hard coding it).
+ var swFactor = (type == mxConstants.ARROW_DIAMOND) ? 0.7071 : 0.9862;
+ var endOffsetX = unitX * sw * swFactor;
+ var endOffsetY = unitY * sw * swFactor;
+
+ unitX = unitX * (size + sw);
+ unitY = unitY * (size + sw);
+
+ pe.x -= endOffsetX;
+ pe.y -= endOffsetY;
+
+ // thickness factor for diamond
+ var tk = ((type == mxConstants.ARROW_DIAMOND) ? 2 : 3.4);
+
+ canvas.begin();
+ canvas.moveTo(pe.x, pe.y);
+ canvas.lineTo(pe.x - unitX / 2 - unitY / tk, pe.y + unitX / tk - unitY / 2);
+ canvas.lineTo(pe.x - unitX, pe.y - unitY);
+ canvas.lineTo(pe.x - unitX / 2 + unitY / tk, pe.y - unitY / 2 - unitX / tk);
+ canvas.close();
+
+ var key = (source) ? mxConstants.STYLE_STARTFILL : mxConstants.STYLE_ENDFILL;
+
+ if (state.style[key] == 0)
+ {
+ canvas.stroke();
+ }
+ else
+ {
+ canvas.fillAndStroke();
+ }
+
+ return new mxPoint(-endOffsetX - unitX, -endOffsetY - unitY);
+ };
+
+ this.markers['diamond'] = tmp_diamond;
+ this.markers['diamondThin'] = tmp_diamond;
+};