/** * $Id: mxStencil.java,v 1.3 2013/05/23 10:29:43 gaudenz Exp $ * Copyright (c) 2010-2012, JGraph Ltd */ package com.mxgraph.shape; import java.util.Map; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.mxgraph.canvas.mxGraphics2DCanvas; import com.mxgraph.canvas.mxGraphicsCanvas2D; import com.mxgraph.util.mxConstants; import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.util.mxUtils; import com.mxgraph.view.mxCellState; /** * Implements a stencil for the given XML definition. This class implements the mxGraph * stencil schema. */ public class mxStencil implements mxIShape { /** * Holds the top-level node of the stencil definition. */ protected Element desc; /** * Holds the aspect of the shape. Default is "auto". */ protected String aspect = null; /** * Holds the width of the shape. Default is 100. */ protected double w0 = 100; /** * Holds the height of the shape. Default is 100. */ protected double h0 = 100; /** * Holds the XML node with the stencil description. */ protected Element bgNode = null; /** * Holds the XML node with the stencil description. */ protected Element fgNode = null; /** * Holds the strokewidth direction from the description. */ protected String strokewidth = null; /** * Holds the last x-position of the cursor. */ protected double lastMoveX = 0; /** * Holds the last y-position of the cursor. */ protected double lastMoveY = 0; /** * Constructs a new stencil for the given mxGraph shape description. */ public mxStencil(Element description) { setDescription(description); } /** * Returns the description. */ public Element getDescription() { return desc; } /** * Sets the description. */ public void setDescription(Element value) { desc = value; parseDescription(); } /** * Creates the canvas for rendering the stencil. */ protected mxGraphicsCanvas2D createCanvas(mxGraphics2DCanvas gc) { return new mxGraphicsCanvas2D(gc.getGraphics()); } /** * Paints the stencil for the given state. */ public void paintShape(mxGraphics2DCanvas gc, mxCellState state) { Map<String, Object> style = state.getStyle(); mxGraphicsCanvas2D canvas = createCanvas(gc); double rotation = mxUtils.getDouble(style, mxConstants.STYLE_ROTATION, 0); String direction = mxUtils.getString(style, mxConstants.STYLE_DIRECTION, null); // Default direction is east (ignored if rotation exists) if (direction != null) { if (direction.equals("north")) { rotation += 270; } else if (direction.equals("west")) { rotation += 180; } else if (direction.equals("south")) { rotation += 90; } } // New styles for shape flipping the stencil boolean flipH = mxUtils.isTrue(style, mxConstants.STYLE_STENCIL_FLIPH, false); boolean flipV = mxUtils.isTrue(style, mxConstants.STYLE_STENCIL_FLIPV, false); if (flipH && flipV) { rotation += 180; flipH = false; flipV = false; } // Saves the global state for each cell canvas.save(); // Adds rotation and horizontal/vertical flipping 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) double scale = state.getView().getScale(); double sw = mxUtils.getDouble(style, mxConstants.STYLE_STROKEWIDTH, 1) * scale; canvas.setStrokeWidth(sw); double alpha = mxUtils.getDouble(style, mxConstants.STYLE_OPACITY, 100) / 100; String gradientColor = mxUtils.getString(style, mxConstants.STYLE_GRADIENTCOLOR, null); // Converts colors with special keyword none to null if (gradientColor != null && gradientColor.equals(mxConstants.NONE)) { gradientColor = null; } String fillColor = mxUtils.getString(style, mxConstants.STYLE_FILLCOLOR, null); if (fillColor != null && fillColor.equals(mxConstants.NONE)) { fillColor = null; } String strokeColor = mxUtils.getString(style, mxConstants.STYLE_STROKECOLOR, null); if (strokeColor != null && strokeColor.equals(mxConstants.NONE)) { strokeColor = null; } // Draws the shadow if the fillColor is not transparent if (mxUtils.isTrue(style, mxConstants.STYLE_SHADOW, false)) { drawShadow(canvas, state, rotation, flipH, flipV, state, alpha, fillColor != null); } canvas.setAlpha(alpha); // Sets the dashed state if (mxUtils.isTrue(style, mxConstants.STYLE_DASHED, false)) { canvas.setDashed(true); } // Draws background and foreground if (strokeColor != null || fillColor != null) { if (strokeColor != null) { canvas.setStrokeColor(strokeColor); } if (fillColor != null) { if (gradientColor != null && !gradientColor.equals("transparent")) { canvas.setGradient(fillColor, gradientColor, state.getX(), state.getY(), state.getWidth(), state.getHeight(), direction, 1, 1); } else { canvas.setFillColor(fillColor); } } // Draws background and foreground of shape drawShape(canvas, state, state, true); drawShape(canvas, state, state, false); } } /** * Draws the shadow. */ protected void drawShadow(mxGraphicsCanvas2D canvas, mxCellState state, double rotation, boolean flipH, boolean flipV, mxRectangle bounds, double alpha, boolean 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 double rad = rotation * Math.PI / 180; double cos = Math.cos(-rad); double sin = Math.sin(-rad); mxPoint offset = mxUtils.getRotatedPoint(new mxPoint(mxConstants.SHADOW_OFFSETX, mxConstants.SHADOW_OFFSETY), cos, sin); if (flipH) { offset.setX(offset.getX() * -1); } if (flipV) { offset.setY(offset.getY() * -1); } // TODO: Use save/restore instead of negative offset to restore (requires fix for HTML canvas) canvas.translate(offset.getX(), offset.getY()); // Returns true if a shadow has been painted (path has been created) if (drawShape(canvas, state, bounds, true)) { canvas.setAlpha(mxConstants.STENCIL_SHADOW_OPACITY * alpha); // TODO: Implement new shadow //canvas.shadow(mxConstants.STENCIL_SHADOWCOLOR, filled); } canvas.translate(-offset.getX(), -offset.getY()); } /** * Draws this stencil inside the given bounds. */ public boolean drawShape(mxGraphicsCanvas2D canvas, mxCellState state, mxRectangle bounds, boolean background) { Element elt = (background) ? bgNode : fgNode; if (elt != null) { String direction = mxUtils.getString(state.getStyle(), mxConstants.STYLE_DIRECTION, null); mxRectangle aspect = computeAspect(state, bounds, direction); double minScale = Math.min(aspect.getWidth(), aspect.getHeight()); double sw = strokewidth.equals("inherit") ? mxUtils.getDouble( state.getStyle(), mxConstants.STYLE_STROKEWIDTH, 1) * state.getView().getScale() : Double .parseDouble(strokewidth) * minScale; lastMoveX = 0; lastMoveY = 0; canvas.setStrokeWidth(sw); Node tmp = elt.getFirstChild(); while (tmp != null) { if (tmp.getNodeType() == Node.ELEMENT_NODE) { drawElement(canvas, state, (Element) tmp, aspect); } tmp = tmp.getNextSibling(); } return true; } return false; } /** * 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 rectangle. */ protected mxRectangle computeAspect(mxCellState state, mxRectangle bounds, String direction) { double x0 = bounds.getX(); double y0 = bounds.getY(); double sx = bounds.getWidth() / w0; double sy = bounds.getHeight() / h0; boolean inverse = (direction != null && (direction.equals("north") || direction .equals("south"))); if (inverse) { sy = bounds.getWidth() / h0; sx = bounds.getHeight() / w0; double delta = (bounds.getWidth() - bounds.getHeight()) / 2; x0 += delta; y0 -= delta; } if (aspect.equals("fixed")) { sy = Math.min(sx, sy); sx = sy; // Centers the shape inside the available space if (inverse) { x0 += (bounds.getHeight() - this.w0 * sx) / 2; y0 += (bounds.getWidth() - this.h0 * sy) / 2; } else { x0 += (bounds.getWidth() - this.w0 * sx) / 2; y0 += (bounds.getHeight() - this.h0 * sy) / 2; } } return new mxRectangle(x0, y0, sx, sy); } /** * Drawsthe given element. */ protected void drawElement(mxGraphicsCanvas2D canvas, mxCellState state, Element node, mxRectangle aspect) { String name = node.getNodeName(); double x0 = aspect.getX(); double y0 = aspect.getY(); double sx = aspect.getWidth(); double sy = aspect.getHeight(); double minScale = Math.min(sx, sy); // LATER: Move to lookup table if (name.equals("save")) { canvas.save(); } else if (name.equals("restore")) { canvas.restore(); } else if (name.equals("path")) { canvas.begin(); // Renders the elements inside the given path Node childNode = node.getFirstChild(); while (childNode != null) { if (childNode.getNodeType() == Node.ELEMENT_NODE) { drawElement(canvas, state, (Element) childNode, aspect); } childNode = childNode.getNextSibling(); } } else if (name.equals("close")) { canvas.close(); } else if (name.equals("move")) { lastMoveX = x0 + getDouble(node, "x") * sx; lastMoveY = y0 + getDouble(node, "y") * sy; canvas.moveTo(lastMoveX, lastMoveY); } else if (name.equals("line")) { lastMoveX = x0 + getDouble(node, "x") * sx; lastMoveY = y0 + getDouble(node, "y") * sy; canvas.lineTo(lastMoveX, lastMoveY); } else if (name.equals("quad")) { lastMoveX = x0 + getDouble(node, "x2") * sx; lastMoveY = y0 + getDouble(node, "y2") * sy; canvas.quadTo(x0 + getDouble(node, "x1") * sx, y0 + getDouble(node, "y1") * sy, lastMoveX, lastMoveY); } else if (name.equals("curve")) { lastMoveX = x0 + getDouble(node, "x3") * sx; lastMoveY = y0 + getDouble(node, "y3") * sy; canvas.curveTo(x0 + getDouble(node, "x1") * sx, y0 + getDouble(node, "y1") * sy, x0 + getDouble(node, "x2") * sx, y0 + getDouble(node, "y2") * sy, lastMoveX, lastMoveY); } else if (name.equals("arc")) { // Arc from stencil is turned into curves in image output double r1 = getDouble(node, "rx") * sx; double r2 = getDouble(node, "ry") * sy; double angle = getDouble(node, "x-axis-rotation"); double largeArcFlag = getDouble(node, "large-arc-flag"); double sweepFlag = getDouble(node, "sweep-flag"); double x = x0 + getDouble(node, "x") * sx; double y = y0 + getDouble(node, "y") * sy; double[] curves = mxUtils.arcToCurves(this.lastMoveX, this.lastMoveY, r1, r2, angle, largeArcFlag, sweepFlag, x, y); for (int i = 0; i < curves.length; i += 6) { canvas.curveTo(curves[i], curves[i + 1], curves[i + 2], curves[i + 3], curves[i + 4], curves[i + 5]); lastMoveX = curves[i + 4]; lastMoveY = curves[i + 5]; } } else if (name.equals("rect")) { canvas.rect(x0 + getDouble(node, "x") * sx, y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx, getDouble(node, "h") * sy); } else if (name.equals("roundrect")) { double arcsize = getDouble(node, "arcsize"); if (arcsize == 0) { arcsize = mxConstants.RECTANGLE_ROUNDING_FACTOR * 100; } double w = getDouble(node, "w") * sx; double h = getDouble(node, "h") * sy; double factor = arcsize / 100; double r = Math.min(w * factor, h * factor); canvas.roundrect(x0 + getDouble(node, "x") * sx, y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx, getDouble(node, "h") * sy, r, r); } else if (name.equals("ellipse")) { canvas.ellipse(x0 + getDouble(node, "x") * sx, y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx, getDouble(node, "h") * sy); } else if (name.equals("image")) { String src = evaluateAttribute(node, "src", state); canvas.image(x0 + getDouble(node, "x") * sx, y0 + getDouble(node, "y") * sy, getDouble(node, "w") * sx, getDouble(node, "h") * sy, src, false, getString(node, "flipH", "0").equals("1"), getString(node, "flipV", "0").equals("1")); } else if (name.equals("text")) { String str = evaluateAttribute(node, "str", state); double rotation = getString(node, "vertical", "0").equals("1") ? -90 : 0; canvas.text(x0 + getDouble(node, "x") * sx, y0 + getDouble(node, "y") * sy, 0, 0, str, node.getAttribute("align"), node.getAttribute("valign"), false, "", null, false, rotation); } else if (name.equals("include-shape")) { mxStencil stencil = mxStencilRegistry.getStencil(node .getAttribute("name")); if (stencil != null) { double x = x0 + getDouble(node, "x") * sx; double y = y0 + getDouble(node, "y") * sy; double w = getDouble(node, "w") * sx; double h = getDouble(node, "h") * sy; mxRectangle tmp = new mxRectangle(x, y, w, h); stencil.drawShape(canvas, state, tmp, true); stencil.drawShape(canvas, state, tmp, false); } } else if (name.equals("fillstroke")) { canvas.fillAndStroke(); } else if (name.equals("fill")) { canvas.fill(); } else if (name.equals("stroke")) { canvas.stroke(); } else if (name.equals("strokewidth")) { canvas.setStrokeWidth(getDouble(node, "width") * minScale); } else if (name.equals("dashed")) { canvas.setDashed(node.getAttribute("dashed") == "1"); } else if (name.equals("dashpattern")) { String value = node.getAttribute("pattern"); if (value != null) { String[] tmp = value.split(" "); StringBuffer pat = new StringBuffer(); for (int i = 0; i < tmp.length; i++) { if (tmp[i].length() > 0) { pat.append(Double.parseDouble(tmp[i]) * minScale); pat.append(" "); } } value = pat.toString(); } canvas.setDashPattern(value); } else if (name.equals("strokecolor")) { canvas.setStrokeColor(node.getAttribute("color")); } else if (name.equals("linecap")) { canvas.setLineCap(node.getAttribute("cap")); } else if (name.equals("linejoin")) { canvas.setLineJoin(node.getAttribute("join")); } else if (name.equals("miterlimit")) { canvas.setMiterLimit(getDouble(node, "limit")); } else if (name.equals("fillcolor")) { canvas.setFillColor(node.getAttribute("color")); } else if (name.equals("fontcolor")) { canvas.setFontColor(node.getAttribute("color")); } else if (name.equals("fontstyle")) { canvas.setFontStyle(getInt(node, "style", 0)); } else if (name.equals("fontfamily")) { canvas.setFontFamily(node.getAttribute("family")); } else if (name.equals("fontsize")) { canvas.setFontSize(getDouble(node, "size") * minScale); } } /** * Returns the given attribute or the default value. */ protected int getInt(Element elt, String attribute, int defaultValue) { String value = elt.getAttribute(attribute); if (value != null && value.length() > 0) { try { defaultValue = (int) Math.floor(Float.parseFloat(value)); } catch (NumberFormatException e) { // ignore } } return defaultValue; } /** * Returns the given attribute or 0. */ protected double getDouble(Element elt, String attribute) { return getDouble(elt, attribute, 0); } /** * Returns the given attribute or the default value. */ protected double getDouble(Element elt, String attribute, double defaultValue) { String value = elt.getAttribute(attribute); if (value != null && value.length() > 0) { try { defaultValue = Double.parseDouble(value); } catch (NumberFormatException e) { // ignore } } return defaultValue; } /** * Returns the given attribute or the default value. */ protected String getString(Element elt, String attribute, String defaultValue) { String value = elt.getAttribute(attribute); if (value != null && value.length() > 0) { defaultValue = value; } return defaultValue; } /** * Parses the description of this shape. */ protected void parseDescription() { // LATER: Preprocess nodes for faster painting fgNode = (Element) desc.getElementsByTagName("foreground").item(0); bgNode = (Element) desc.getElementsByTagName("background").item(0); w0 = getDouble(desc, "w", w0); h0 = getDouble(desc, "h", h0); // 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. aspect = getString(desc, "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). strokewidth = getString(desc, "strokewidth", "1"); } /** * 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 <state> as the only argument and the return * value is used as the attribute value to be returned. */ public String evaluateAttribute(Element elt, String attribute, mxCellState state) { String result = elt.getAttribute(attribute); if (result == null) { // JS functions as text content are currently not supported in Java } return result; } }