/** * $Id: mxStencilShape.java,v 1.1 2012/11/15 13:26:44 gaudenz Exp $ * Copyright (c) 2010-2012, JGraph Ltd */ package com.mxgraph.shape; import org.w3c.dom.Node; import com.mxgraph.canvas.mxGraphics2DCanvas; import com.mxgraph.util.mxUtils; import com.mxgraph.util.mxXmlUtils; import com.mxgraph.util.svg.AWTPathProducer; import com.mxgraph.util.svg.AWTPolygonProducer; import com.mxgraph.util.svg.AWTPolylineProducer; import com.mxgraph.util.svg.CSSConstants; import com.mxgraph.util.svg.ExtendedGeneralPath; import com.mxgraph.view.mxCellState; import java.awt.Color; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Map; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * Stencil shape drawing that takes an XML definition of the shape and renders * it. * * See http://projects.gnome.org/dia/custom-shapes for specs. See * http://dia-installer.de/shapes_de.html for shapes. */ public class mxStencilShape extends mxBasicShape { public mxStencilShape() { super(); } protected GeneralPath shapePath; /** * Reference to the root node of the Dia shape description. */ protected Node root; protected svgShape rootShape; protected Rectangle2D boundingBox; protected String name; protected String iconPath; /** * Transform cached to save instance created. Used to scale the internal * path of shapes where possible */ protected AffineTransform cachedTransform = new AffineTransform(); /** * Constructs a new stencil for the given Dia shape description. */ public mxStencilShape(String shapeXml) { this(mxXmlUtils.parseXml(shapeXml)); } public mxStencilShape(Document document) { if (document != null) { NodeList nameList = document.getElementsByTagName("name"); if (nameList != null && nameList.getLength() > 0) { this.name = nameList.item(0).getTextContent(); } NodeList iconList = document.getElementsByTagName("icon"); if (iconList != null && iconList.getLength() > 0) { this.iconPath = iconList.item(0).getTextContent(); } NodeList svgList = document.getElementsByTagName("svg:svg"); if (svgList != null && svgList.getLength() > 0) { this.root = svgList.item(0); } else { svgList = document.getElementsByTagName("svg"); if (svgList != null && svgList.getLength() > 0) { this.root = svgList.item(0); } } if (this.root != null) { rootShape = new svgShape(null, null); createShape(this.root, rootShape); } } } /** * */ @Override public void paintShape(mxGraphics2DCanvas canvas, mxCellState state) { double x = state.getX(); double y = state.getY(); double w = state.getWidth(); double h = state.getHeight(); canvas.getGraphics().translate(x, y); double widthRatio = 1; double heightRatio = 1; if (boundingBox != null) { widthRatio = w / boundingBox.getWidth(); heightRatio = h / boundingBox.getHeight(); } this.paintNode(canvas, state, rootShape, widthRatio, heightRatio); canvas.getGraphics().translate(-x, -y); } /** * */ public void paintNode(mxGraphics2DCanvas canvas, mxCellState state, svgShape shape, double widthRatio, double heightRatio) { Shape associatedShape = shape.shape; boolean fill = false; boolean stroke = true; Color fillColor = null; Color strokeColor = null; Map<String, Object> style = shape.style; if (style != null) { String fillStyle = mxUtils.getString(style, CSSConstants.CSS_FILL_PROPERTY); String strokeStyle = mxUtils.getString(style, CSSConstants.CSS_STROKE_PROPERTY); if (strokeStyle != null && strokeStyle.equals(CSSConstants.CSS_NONE_VALUE)) { if (strokeStyle.equals(CSSConstants.CSS_NONE_VALUE)) { stroke = false; } else if (strokeStyle.trim().startsWith("#")) { int hashIndex = strokeStyle.indexOf("#"); strokeColor = mxUtils.parseColor(strokeStyle .substring(hashIndex + 1)); } } if (fillStyle != null) { if (fillStyle.equals(CSSConstants.CSS_NONE_VALUE)) { fill = false; } else if (fillStyle.trim().startsWith("#")) { int hashIndex = fillStyle.indexOf("#"); fillColor = mxUtils.parseColor(fillStyle .substring(hashIndex + 1)); fill = true; } else { fill = true; } } } if (associatedShape != null) { boolean wasScaled = false; if (widthRatio != 1 || heightRatio != 1) { transformShape(associatedShape, 0.0, 0.0, widthRatio, heightRatio); wasScaled = true; } // Paints the background if (fill && configureGraphics(canvas, state, true)) { if (fillColor != null) { canvas.getGraphics().setColor(fillColor); } canvas.getGraphics().fill(associatedShape); } // Paints the foreground if (stroke && configureGraphics(canvas, state, false)) { if (strokeColor != null) { canvas.getGraphics().setColor(strokeColor); } canvas.getGraphics().draw(associatedShape); } if (wasScaled) { transformShape(associatedShape, 0.0, 0.0, 1.0 / widthRatio, 1.0 / heightRatio); } } /* * If root is a group element, then we should add it's styles to the * children. */ for (svgShape subShape : shape.subShapes) { paintNode(canvas, state, subShape, widthRatio, heightRatio); } } /** * Scales the points composing this shape by the x and y ratios specified * * @param shape * the shape to scale * @param transX * the x translation * @param transY * the y translation * @param widthRatio * the x co-ordinate scale * @param heightRatio * the y co-ordinate scale */ protected void transformShape(Shape shape, double transX, double transY, double widthRatio, double heightRatio) { if (shape instanceof Rectangle2D) { Rectangle2D rect = (Rectangle2D) shape; if (transX != 0 || transY != 0) { rect.setFrame(rect.getX() + transX, rect.getY() + transY, rect.getWidth(), rect.getHeight()); } if (widthRatio != 1 || heightRatio != 1) { rect.setFrame(rect.getX() * widthRatio, rect.getY() * heightRatio, rect.getWidth() * widthRatio, rect.getHeight() * heightRatio); } } else if (shape instanceof Line2D) { Line2D line = (Line2D) shape; if (transX != 0 || transY != 0) { line.setLine(line.getX1() + transX, line.getY1() + transY, line.getX2() + transX, line.getY2() + transY); } if (widthRatio != 1 || heightRatio != 1) { line.setLine(line.getX1() * widthRatio, line.getY1() * heightRatio, line.getX2() * widthRatio, line.getY2() * heightRatio); } } else if (shape instanceof GeneralPath) { GeneralPath path = (GeneralPath) shape; cachedTransform.setToScale(widthRatio, heightRatio); cachedTransform.translate(transX, transY); path.transform(cachedTransform); } else if (shape instanceof ExtendedGeneralPath) { ExtendedGeneralPath path = (ExtendedGeneralPath) shape; cachedTransform.setToScale(widthRatio, heightRatio); cachedTransform.translate(transX, transY); path.transform(cachedTransform); } else if (shape instanceof Ellipse2D) { Ellipse2D ellipse = (Ellipse2D) shape; if (transX != 0 || transY != 0) { ellipse.setFrame(ellipse.getX() + transX, ellipse.getY() + transY, ellipse.getWidth(), ellipse.getHeight()); } if (widthRatio != 1 || heightRatio != 1) { ellipse.setFrame(ellipse.getX() * widthRatio, ellipse.getY() * heightRatio, ellipse.getWidth() * widthRatio, ellipse.getHeight() * heightRatio); } } } /** * */ public void createShape(Node root, svgShape shape) { Node child = root.getFirstChild(); /* * If root is a group element, then we should add it's styles to the * childrens... */ while (child != null) { if (isGroup(child.getNodeName())) { String style = ((Element) root).getAttribute("style"); Map<String, Object> styleMap = mxStencilShape .getStylenames(style); svgShape subShape = new svgShape(null, styleMap); createShape(child, subShape); } svgShape subShape = createElement(child); if (subShape != null) { shape.subShapes.add(subShape); } child = child.getNextSibling(); } for (svgShape subShape : shape.subShapes) { if (subShape != null && subShape.shape != null) { if (boundingBox == null) { boundingBox = subShape.shape.getBounds2D(); } else { boundingBox.add(subShape.shape.getBounds2D()); } } } // If the shape does not butt up against either or both axis, // ensure it is flush against both if (boundingBox != null && (boundingBox.getX() != 0 || boundingBox.getY() != 0)) { for (svgShape subShape : shape.subShapes) { if (subShape != null && subShape.shape != null) { transformShape(subShape.shape, -boundingBox.getX(), -boundingBox.getY(), 1.0, 1.0); } } } } /** * Forms an internal representation of the specified SVG element and returns * that representation * * @param root * the SVG element to represent * @return the internal representation of the element, or null if an error * occurs */ public svgShape createElement(Node root) { Element element = null; if (root instanceof Element) { element = (Element) root; String style = element.getAttribute("style"); Map<String, Object> styleMap = mxStencilShape.getStylenames(style); if (isRectangle(root.getNodeName())) { svgShape rectShape = null; try { String xString = element.getAttribute("x"); String yString = element.getAttribute("y"); String widthString = element.getAttribute("width"); String heightString = element.getAttribute("height"); // Values default to zero if not specified double x = 0; double y = 0; double width = 0; double height = 0; if (xString.length() > 0) { x = Double.valueOf(xString); } if (yString.length() > 0) { y = Double.valueOf(yString); } if (widthString.length() > 0) { width = Double.valueOf(widthString); if (width < 0) { return null; // error in SVG spec } } if (heightString.length() > 0) { height = Double.valueOf(heightString); if (height < 0) { return null; // error in SVG spec } } String rxString = element.getAttribute("rx"); String ryString = element.getAttribute("ry"); double rx = 0; double ry = 0; if (rxString.length() > 0) { rx = Double.valueOf(rxString); if (rx < 0) { return null; // error in SVG spec } } if (ryString.length() > 0) { ry = Double.valueOf(ryString); if (ry < 0) { return null; // error in SVG spec } } if (rx > 0 || ry > 0) { // Specification rules on rx and ry if (rx > 0 && ryString.length() == 0) { ry = rx; } else if (ry > 0 && rxString.length() == 0) { rx = ry; } if (rx > width / 2.0) { rx = width / 2.0; } if (ry > height / 2.0) { ry = height / 2.0; } rectShape = new svgShape(new RoundRectangle2D.Double(x, y, width, height, rx, ry), styleMap); } else { rectShape = new svgShape(new Rectangle2D.Double(x, y, width, height), styleMap); } } catch (Exception e) { // TODO log something useful } return rectShape; } else if (isLine(root.getNodeName())) { String x1String = element.getAttribute("x1"); String x2String = element.getAttribute("x2"); String y1String = element.getAttribute("y1"); String y2String = element.getAttribute("y2"); double x1 = 0; double x2 = 0; double y1 = 0; double y2 = 0; if (x1String.length() > 0) { x1 = Double.valueOf(x1String); } if (x2String.length() > 0) { x2 = Double.valueOf(x2String); } if (y1String.length() > 0) { y1 = Double.valueOf(y1String); } if (y2String.length() > 0) { y2 = Double.valueOf(y2String); } svgShape lineShape = new svgShape(new Line2D.Double(x1, y1, x2, y2), styleMap); return lineShape; } else if (isPolyline(root.getNodeName()) || isPolygon(root.getNodeName())) { String pointsString = element.getAttribute("points"); Shape shape; if (isPolygon(root.getNodeName())) { shape = AWTPolygonProducer.createShape(pointsString, GeneralPath.WIND_NON_ZERO); } else { shape = AWTPolylineProducer.createShape(pointsString, GeneralPath.WIND_NON_ZERO); } if (shape != null) { return new svgShape(shape, styleMap); } return null; } else if (isCircle(root.getNodeName())) { double cx = 0; double cy = 0; double r = 0; String cxString = element.getAttribute("cx"); String cyString = element.getAttribute("cy"); String rString = element.getAttribute("r"); if (cxString.length() > 0) { cx = Double.valueOf(cxString); } if (cyString.length() > 0) { cy = Double.valueOf(cyString); } if (rString.length() > 0) { r = Double.valueOf(rString); if (r < 0) { return null; // error in SVG spec } } return new svgShape(new Ellipse2D.Double(cx - r, cy - r, r * 2, r * 2), styleMap); } else if (isEllipse(root.getNodeName())) { double cx = 0; double cy = 0; double rx = 0; double ry = 0; String cxString = element.getAttribute("cx"); String cyString = element.getAttribute("cy"); String rxString = element.getAttribute("rx"); String ryString = element.getAttribute("ry"); if (cxString.length() > 0) { cx = Double.valueOf(cxString); } if (cyString.length() > 0) { cy = Double.valueOf(cyString); } if (rxString.length() > 0) { rx = Double.valueOf(rxString); if (rx < 0) { return null; // error in SVG spec } } if (ryString.length() > 0) { ry = Double.valueOf(ryString); if (ry < 0) { return null; // error in SVG spec } } return new svgShape(new Ellipse2D.Double(cx - rx, cy - ry, rx * 2, ry * 2), styleMap); } else if (isPath(root.getNodeName())) { String d = element.getAttribute("d"); Shape pathShape = AWTPathProducer.createShape(d, GeneralPath.WIND_NON_ZERO); return new svgShape(pathShape, styleMap); } } return null; } /* * */ private boolean isRectangle(String tag) { return tag.equals("svg:rect") || tag.equals("rect"); } /* * */ private boolean isPath(String tag) { return tag.equals("svg:path") || tag.equals("path"); } /* * */ private boolean isEllipse(String tag) { return tag.equals("svg:ellipse") || tag.equals("ellipse"); } /* * */ private boolean isLine(String tag) { return tag.equals("svg:line") || tag.equals("line"); } /* * */ private boolean isPolyline(String tag) { return tag.equals("svg:polyline") || tag.equals("polyline"); } /* * */ private boolean isCircle(String tag) { return tag.equals("svg:circle") || tag.equals("circle"); } /* * */ private boolean isPolygon(String tag) { return tag.equals("svg:polygon") || tag.equals("polygon"); } private boolean isGroup(String tag) { return tag.equals("svg:g") || tag.equals("g"); } protected class svgShape { public Shape shape; /** * Contains an array of key, value pairs that represent the style of the * cell. */ protected Map<String, Object> style; public List<svgShape> subShapes; /** * Holds the current value to which the shape is scaled in X */ protected double currentXScale; /** * Holds the current value to which the shape is scaled in Y */ protected double currentYScale; public svgShape(Shape shape, Map<String, Object> style) { this.shape = shape; this.style = style; subShapes = new ArrayList<svgShape>(); } public double getCurrentXScale() { return currentXScale; } public void setCurrentXScale(double currentXScale) { this.currentXScale = currentXScale; } public double getCurrentYScale() { return currentYScale; } public void setCurrentYScale(double currentYScale) { this.currentYScale = currentYScale; } } /** * Returns the stylenames in a style of the form stylename[;key=value] or an * empty array if the given style does not contain any stylenames. * * @param style * String of the form stylename[;stylename][;key=value]. * @return Returns the stylename from the given formatted string. */ protected static Map<String, Object> getStylenames(String style) { if (style != null && style.length() > 0) { Map<String, Object> result = new Hashtable<String, Object>(); if (style != null) { String[] pairs = style.split(";"); for (int i = 0; i < pairs.length; i++) { String[] keyValue = pairs[i].split(":"); if (keyValue.length == 2) { result.put(keyValue[0].trim(), keyValue[1].trim()); } } } return result; } return null; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getIconPath() { return iconPath; } public void setIconPath(String iconPath) { this.iconPath = iconPath; } public Rectangle2D getBoundingBox() { return boundingBox; } public void setBoundingBox(Rectangle2D boundingBox) { this.boundingBox = boundingBox; } }