/** * $Id: mxSvgCanvas.java,v 1.56 2010-08-02 13:14:43 david Exp $ * Copyright (c) 2007, Gaudenz Alder */ package com.mxgraph.canvas; import java.util.Hashtable; import java.util.List; import java.util.Map; import org.w3c.dom.Document; import org.w3c.dom.Element; 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; /** * An implementation of a canvas that uses SVG for painting. This canvas * ignores the STYLE_LABEL_BACKGROUNDCOLOR and * STYLE_LABEL_BORDERCOLOR styles due to limitations of SVG. */ public class mxSvgCanvas extends mxBasicCanvas { /** * Holds the HTML document that represents the canvas. */ protected Document document; /** * Constructs a new SVG canvas for the specified dimension and scale. */ public mxSvgCanvas() { this(null); } /** * Constructs a new SVG canvas for the specified bounds, scale and * background color. */ public mxSvgCanvas(Document document) { setDocument(document); } /** * */ public void appendSvgElement(Element node) { if (document != null) { document.getDocumentElement().appendChild(node); } } /** * */ public void setDocument(Document document) { this.document = document; } /** * Returns a reference to the document that represents the canvas. * * @return Returns the document. */ public Document getDocument() { return document; } /* * (non-Javadoc) * @see com.mxgraph.canvas.mxICanvas#drawCell() */ public Object drawCell(mxCellState state) { Map<String, Object> style = state.getStyle(); Element elem = null; if (state.getAbsolutePointCount() > 1) { List<mxPoint> pts = state.getAbsolutePoints(); // Transpose all points by cloning into a new array pts = mxUtils.translatePoints(pts, translate.x, translate.y); // Draws the line elem = drawLine(pts, style); // Applies opacity float opacity = mxUtils.getFloat(style, mxConstants.STYLE_OPACITY, 100); if (opacity != 100) { String value = String.valueOf(opacity / 100); elem.setAttribute("fill-opacity", value); elem.setAttribute("stroke-opacity", value); } } else { int x = (int) state.getX() + translate.x; int y = (int) state.getY() + translate.y; int w = (int) state.getWidth(); int h = (int) state.getHeight(); if (!mxUtils.getString(style, mxConstants.STYLE_SHAPE, "").equals( mxConstants.SHAPE_SWIMLANE)) { elem = drawShape(x, y, w, h, style); } else { int start = (int) Math.round(mxUtils.getInt(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE) * scale); // Removes some styles to draw the content area Map<String, Object> cloned = new Hashtable<String, Object>( style); cloned.remove(mxConstants.STYLE_FILLCOLOR); cloned.remove(mxConstants.STYLE_ROUNDED); if (mxUtils.isTrue(style, mxConstants.STYLE_HORIZONTAL, true)) { elem = drawShape(x, y, w, start, style); drawShape(x, y + start, w, h - start, cloned); } else { elem = drawShape(x, y, start, h, style); drawShape(x + start, y, w - start, h, cloned); } } } return elem; } /* * (non-Javadoc) * @see com.mxgraph.canvas.mxICanvas#drawLabel() */ public Object drawLabel(String label, mxCellState state, boolean html) { mxRectangle bounds = state.getLabelBounds(); if (drawLabels) { int x = (int) bounds.getX() + translate.x; int y = (int) bounds.getY() + translate.y; int w = (int) bounds.getWidth(); int h = (int) bounds.getHeight(); Map<String, Object> style = state.getStyle(); return drawText(label, x, y, w, h, style); } return null; } /** * Draws the shape specified with the STYLE_SHAPE key in the given style. * * @param x X-coordinate of the shape. * @param y Y-coordinate of the shape. * @param w Width of the shape. * @param h Height of the shape. * @param style Style of the the shape. */ public Element drawShape(int x, int y, int w, int h, Map<String, Object> style) { String fillColor = mxUtils.getString(style, mxConstants.STYLE_FILLCOLOR, "none"); String strokeColor = mxUtils.getString(style, mxConstants.STYLE_STROKECOLOR); float strokeWidth = (float) (mxUtils.getFloat(style, mxConstants.STYLE_STROKEWIDTH, 1) * scale); // Draws the shape String shape = mxUtils.getString(style, mxConstants.STYLE_SHAPE); Element elem = null; Element background = null; if (shape.equals(mxConstants.SHAPE_IMAGE)) { String img = getImageForStyle(style); if (img != null) { elem = document.createElement("image"); elem.setAttribute("x", String.valueOf(x)); elem.setAttribute("y", String.valueOf(y)); elem.setAttribute("width", String.valueOf(w)); elem.setAttribute("height", String.valueOf(h)); elem.setAttributeNS(mxConstants.NS_XLINK, "xlink:href", img); } } else if (shape.equals(mxConstants.SHAPE_LINE)) { String direction = mxUtils.getString(style, mxConstants.STYLE_DIRECTION, mxConstants.DIRECTION_EAST); String d = null; if (direction.equals(mxConstants.DIRECTION_EAST) || direction.equals(mxConstants.DIRECTION_WEST)) { int mid = (y + h / 2); d = "M " + x + " " + mid + " L " + (x + w) + " " + mid; } else { int mid = (x + w / 2); d = "M " + mid + " " + y + " L " + mid + " " + (y + h); } elem = document.createElement("path"); elem.setAttribute("d", d + " Z"); } else if (shape.equals(mxConstants.SHAPE_ELLIPSE)) { elem = document.createElement("ellipse"); elem.setAttribute("cx", String.valueOf(x + w / 2)); elem.setAttribute("cy", String.valueOf(y + h / 2)); elem.setAttribute("rx", String.valueOf(w / 2)); elem.setAttribute("ry", String.valueOf(h / 2)); } else if (shape.equals(mxConstants.SHAPE_DOUBLE_ELLIPSE)) { elem = document.createElement("g"); background = document.createElement("ellipse"); background.setAttribute("cx", String.valueOf(x + w / 2)); background.setAttribute("cy", String.valueOf(y + h / 2)); background.setAttribute("rx", String.valueOf(w / 2)); background.setAttribute("ry", String.valueOf(h / 2)); elem.appendChild(background); int inset = (int) ((3 + strokeWidth) * scale); Element foreground = document.createElement("ellipse"); foreground.setAttribute("fill", "none"); foreground.setAttribute("stroke", strokeColor); foreground .setAttribute("stroke-width", String.valueOf(strokeWidth)); foreground.setAttribute("cx", String.valueOf(x + w / 2)); foreground.setAttribute("cy", String.valueOf(y + h / 2)); foreground.setAttribute("rx", String.valueOf(w / 2 - inset)); foreground.setAttribute("ry", String.valueOf(h / 2 - inset)); elem.appendChild(foreground); } else if (shape.equals(mxConstants.SHAPE_RHOMBUS)) { elem = document.createElement("path"); String d = "M " + (x + w / 2) + " " + y + " L " + (x + w) + " " + (y + h / 2) + " L " + (x + w / 2) + " " + (y + h) + " L " + x + " " + (y + h / 2); elem.setAttribute("d", d + " Z"); } else if (shape.equals(mxConstants.SHAPE_TRIANGLE)) { elem = document.createElement("path"); String direction = mxUtils.getString(style, mxConstants.STYLE_DIRECTION, ""); String d = null; if (direction.equals(mxConstants.DIRECTION_NORTH)) { d = "M " + x + " " + (y + h) + " L " + (x + w / 2) + " " + y + " L " + (x + w) + " " + (y + h); } else if (direction.equals(mxConstants.DIRECTION_SOUTH)) { d = "M " + x + " " + y + " L " + (x + w / 2) + " " + (y + h) + " L " + (x + w) + " " + y; } else if (direction.equals(mxConstants.DIRECTION_WEST)) { d = "M " + (x + w) + " " + y + " L " + x + " " + (y + h / 2) + " L " + (x + w) + " " + (y + h); } else // east { d = "M " + x + " " + y + " L " + (x + w) + " " + (y + h / 2) + " L " + x + " " + (y + h); } elem.setAttribute("d", d + " Z"); } else if (shape.equals(mxConstants.SHAPE_HEXAGON)) { elem = document.createElement("path"); String direction = mxUtils.getString(style, mxConstants.STYLE_DIRECTION, ""); String d = null; if (direction.equals(mxConstants.DIRECTION_NORTH) || direction.equals(mxConstants.DIRECTION_SOUTH)) { d = "M " + (x + 0.5 * w) + " " + y + " L " + (x + w) + " " + (y + 0.25 * h) + " L " + (x + w) + " " + (y + 0.75 * h) + " L " + (x + 0.5 * w) + " " + (y + h) + " L " + x + " " + (y + 0.75 * h) + " L " + x + " " + (y + 0.25 * h); } else { d = "M " + (x + 0.25 * w) + " " + y + " L " + (x + 0.75 * w) + " " + y + " L " + (x + w) + " " + (y + 0.5 * h) + " L " + (x + 0.75 * w) + " " + (y + h) + " L " + (x + 0.25 * w) + " " + (y + h) + " L " + x + " " + (y + 0.5 * h); } elem.setAttribute("d", d + " Z"); } else if (shape.equals(mxConstants.SHAPE_CLOUD)) { elem = document.createElement("path"); String d = "M " + (x + 0.25 * w) + " " + (y + 0.25 * h) + " C " + (x + 0.05 * w) + " " + (y + 0.25 * h) + " " + x + " " + (y + 0.5 * h) + " " + (x + 0.16 * w) + " " + (y + 0.55 * h) + " C " + x + " " + (y + 0.66 * h) + " " + (x + 0.18 * w) + " " + (y + 0.9 * h) + " " + (x + 0.31 * w) + " " + (y + 0.8 * h) + " C " + (x + 0.4 * w) + " " + (y + h) + " " + (x + 0.7 * w) + " " + (y + h) + " " + (x + 0.8 * w) + " " + (y + 0.8 * h) + " C " + (x + w) + " " + (y + 0.8 * h) + " " + (x + w) + " " + (y + 0.6 * h) + " " + (x + 0.875 * w) + " " + (y + 0.5 * h) + " C " + (x + w) + " " + (y + 0.3 * h) + " " + (x + 0.8 * w) + " " + (y + 0.1 * h) + " " + (x + 0.625 * w) + " " + (y + 0.2 * h) + " C " + (x + 0.5 * w) + " " + (y + 0.05 * h) + " " + (x + 0.3 * w) + " " + (y + 0.05 * h) + " " + (x + 0.25 * w) + " " + (y + 0.25 * h); elem.setAttribute("d", d + " Z"); } else if (shape.equals(mxConstants.SHAPE_ACTOR)) { elem = document.createElement("path"); double width3 = w / 3; String d = " M " + x + " " + (y + h) + " C " + x + " " + (y + 3 * h / 5) + " " + x + " " + (y + 2 * h / 5) + " " + (x + w / 2) + " " + (y + 2 * h / 5) + " C " + (x + w / 2 - width3) + " " + (y + 2 * h / 5) + " " + (x + w / 2 - width3) + " " + y + " " + (x + w / 2) + " " + y + " C " + (x + w / 2 + width3) + " " + y + " " + (x + w / 2 + width3) + " " + (y + 2 * h / 5) + " " + (x + w / 2) + " " + (y + 2 * h / 5) + " C " + (x + w) + " " + (y + 2 * h / 5) + " " + (x + w) + " " + (y + 3 * h / 5) + " " + (x + w) + " " + (y + h); elem.setAttribute("d", d + " Z"); } else if (shape.equals(mxConstants.SHAPE_CYLINDER)) { elem = document.createElement("g"); background = document.createElement("path"); double dy = Math.min(40, Math.floor(h / 5)); String d = " M " + x + " " + (y + dy) + " C " + x + " " + (y - dy / 3) + " " + (x + w) + " " + (y - dy / 3) + " " + (x + w) + " " + (y + dy) + " L " + (x + w) + " " + (y + h - dy) + " C " + (x + w) + " " + (y + h + dy / 3) + " " + x + " " + (y + h + dy / 3) + " " + x + " " + (y + h - dy); background.setAttribute("d", d + " Z"); elem.appendChild(background); Element foreground = document.createElement("path"); d = "M " + x + " " + (y + dy) + " C " + x + " " + (y + 2 * dy) + " " + (x + w) + " " + (y + 2 * dy) + " " + (x + w) + " " + (y + dy); foreground.setAttribute("d", d); foreground.setAttribute("fill", "none"); foreground.setAttribute("stroke", strokeColor); foreground .setAttribute("stroke-width", String.valueOf(strokeWidth)); elem.appendChild(foreground); } else { elem = document.createElement("rect"); elem.setAttribute("x", String.valueOf(x)); elem.setAttribute("y", String.valueOf(y)); elem.setAttribute("width", String.valueOf(w)); elem.setAttribute("height", String.valueOf(h)); if (mxUtils.isTrue(style, mxConstants.STYLE_ROUNDED, false)) { elem.setAttribute("rx", String.valueOf(w * mxConstants.RECTANGLE_ROUNDING_FACTOR)); elem.setAttribute("ry", String.valueOf(h * mxConstants.RECTANGLE_ROUNDING_FACTOR)); } } Element bg = background; if (bg == null) { bg = elem; } bg.setAttribute("fill", fillColor); bg.setAttribute("stroke", strokeColor); bg.setAttribute("stroke-width", String.valueOf(strokeWidth)); // Adds the shadow element Element shadowElement = null; if (mxUtils.isTrue(style, mxConstants.STYLE_SHADOW, false) && !fillColor.equals("none")) { shadowElement = (Element) bg.cloneNode(true); shadowElement.setAttribute("transform", mxConstants.SVG_SHADOWTRANSFORM); shadowElement.setAttribute("fill", mxConstants.W3C_SHADOWCOLOR); shadowElement.setAttribute("stroke", mxConstants.W3C_SHADOWCOLOR); shadowElement.setAttribute("stroke-width", String .valueOf(strokeWidth)); appendSvgElement(shadowElement); } // Applies rotation double rotation = mxUtils.getDouble(style, mxConstants.STYLE_ROTATION); if (rotation != 0) { int cx = x + w / 2; int cy = y + h / 2; elem.setAttribute("transform", "rotate(" + rotation + "," + cx + "," + cy + ")"); if (shadowElement != null) { shadowElement.setAttribute("transform", "rotate(" + rotation + "," + cx + "," + cy + ") " + mxConstants.SVG_SHADOWTRANSFORM); } } // Applies opacity float opacity = mxUtils.getFloat(style, mxConstants.STYLE_OPACITY, 100); if (opacity != 100) { String value = String.valueOf(opacity / 100); elem.setAttribute("fill-opacity", value); elem.setAttribute("stroke-opacity", value); if (shadowElement != null) { shadowElement.setAttribute("fill-opacity", value); shadowElement.setAttribute("stroke-opacity", value); } } if (mxUtils.isTrue(style, mxConstants.STYLE_DASHED)) { elem.setAttribute("stroke-dasharray", "3, 3"); } appendSvgElement(elem); return elem; } /** * Draws the given lines as segments between all points of the given list * of mxPoints. * * @param pts List of points that define the line. * @param style Style to be used for painting the line. */ public Element drawLine(List<mxPoint> pts, Map<String, Object> style) { Element group = document.createElement("g"); Element path = document.createElement("path"); String strokeColor = mxUtils.getString(style, mxConstants.STYLE_STROKECOLOR); float tmpStroke = (mxUtils.getFloat(style, mxConstants.STYLE_STROKEWIDTH, 1)); float strokeWidth = (float) (tmpStroke * scale); if (strokeColor != null && strokeWidth > 0) { // Draws the start marker Object marker = style.get(mxConstants.STYLE_STARTARROW); mxPoint pt = pts.get(1); mxPoint p0 = pts.get(0); mxPoint offset = null; if (marker != null) { float size = (mxUtils.getFloat(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_MARKERSIZE)); offset = drawMarker(group, marker, pt, p0, size, tmpStroke, strokeColor); } else { double dx = pt.getX() - p0.getX(); double dy = pt.getY() - p0.getY(); double dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); double nx = dx * strokeWidth / dist; double ny = dy * strokeWidth / dist; offset = new mxPoint(nx / 2, ny / 2); } // Applies offset to the point if (offset != null) { p0 = (mxPoint) p0.clone(); p0.setX(p0.getX() + offset.getX()); p0.setY(p0.getY() + offset.getY()); offset = null; } // Draws the end marker marker = style.get(mxConstants.STYLE_ENDARROW); pt = pts.get(pts.size() - 2); mxPoint pe = pts.get(pts.size() - 1); if (marker != null) { float size = (mxUtils.getFloat(style, mxConstants.STYLE_ENDSIZE, mxConstants.DEFAULT_MARKERSIZE)); offset = drawMarker(group, marker, pt, pe, size, tmpStroke, strokeColor); } else { double dx = pt.getX() - p0.getX(); double dy = pt.getY() - p0.getY(); double dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); double nx = dx * strokeWidth / dist; double ny = dy * strokeWidth / dist; offset = new mxPoint(nx / 2, ny / 2); } // Applies offset to the point if (offset != null) { pe = (mxPoint) pe.clone(); pe.setX(pe.getX() + offset.getX()); pe.setY(pe.getY() + offset.getY()); offset = null; } // Draws the line segments pt = p0; String d = "M " + pt.getX() + " " + pt.getY(); for (int i = 1; i < pts.size() - 1; i++) { pt = pts.get(i); d += " L " + pt.getX() + " " + pt.getY(); } d += " L " + pe.getX() + " " + pe.getY(); path.setAttribute("d", d); path.setAttribute("stroke", strokeColor); path.setAttribute("fill", "none"); path.setAttribute("stroke-width", String.valueOf(strokeWidth)); if (mxUtils.isTrue(style, mxConstants.STYLE_DASHED)) { path.setAttribute("stroke-dasharray", "3, 3"); } group.appendChild(path); appendSvgElement(group); } return group; } /** * Draws the specified marker as a child path in the given parent. */ public mxPoint drawMarker(Element parent, Object type, mxPoint p0, mxPoint pe, float size, float strokeWidth, String color) { mxPoint offset = null; // Computes the norm and the inverse norm double dx = pe.getX() - p0.getX(); double dy = pe.getY() - p0.getY(); double dist = Math.max(1, Math.sqrt(dx * dx + dy * dy)); double absSize = size * scale; double nx = dx * absSize / dist; double ny = dy * absSize / dist; pe = (mxPoint) pe.clone(); pe.setX(pe.getX() - nx * strokeWidth / (2 * size)); pe.setY(pe.getY() - ny * strokeWidth / (2 * size)); nx *= 0.5 + strokeWidth / 2; ny *= 0.5 + strokeWidth / 2; Element path = document.createElement("path"); path.setAttribute("stroke-width", String.valueOf(strokeWidth * scale)); path.setAttribute("stroke", color); path.setAttribute("fill", color); String d = null; if (type.equals(mxConstants.ARROW_CLASSIC) || type.equals(mxConstants.ARROW_BLOCK)) { d = "M " + pe.getX() + " " + pe.getY() + " L " + (pe.getX() - nx - ny / 2) + " " + (pe.getY() - ny + nx / 2) + ((!type.equals(mxConstants.ARROW_CLASSIC)) ? "" : " L " + (pe.getX() - nx * 3 / 4) + " " + (pe.getY() - ny * 3 / 4)) + " L " + (pe.getX() + ny / 2 - nx) + " " + (pe.getY() - ny - nx / 2) + " z"; } else if (type.equals(mxConstants.ARROW_OPEN)) { nx *= 1.2; ny *= 1.2; d = "M " + (pe.getX() - nx - ny / 2) + " " + (pe.getY() - ny + nx / 2) + " L " + (pe.getX() - nx / 6) + " " + (pe.getY() - ny / 6) + " L " + (pe.getX() + ny / 2 - nx) + " " + (pe.getY() - ny - nx / 2) + " M " + pe.getX() + " " + pe.getY(); path.setAttribute("fill", "none"); } else if (type.equals(mxConstants.ARROW_OVAL)) { nx *= 1.2; ny *= 1.2; absSize *= 1.2; d = "M " + (pe.getX() - ny / 2) + " " + (pe.getY() + nx / 2) + " a " + (absSize / 2) + " " + (absSize / 2) + " 0 1,1 " + (nx / 8) + " " + (ny / 8) + " z"; } else if (type.equals(mxConstants.ARROW_DIAMOND)) { d = "M " + (pe.getX() + nx / 2) + " " + (pe.getY() + ny / 2) + " L " + (pe.getX() - ny / 2) + " " + (pe.getY() + nx / 2) + " L " + (pe.getX() - nx / 2) + " " + (pe.getY() - ny / 2) + " L " + (pe.getX() + ny / 2) + " " + (pe.getY() - nx / 2) + " z"; } if (d != null) { path.setAttribute("d", d); parent.appendChild(path); } return offset; } /** * Draws the specified text either using drawHtmlString or using drawString. * * @param text Text to be painted. * @param x X-coordinate of the text. * @param y Y-coordinate of the text. * @param w Width of the text. * @param h Height of the text. * @param style Style to be used for painting the text. */ public Object drawText(String text, int x, int y, int w, int h, Map<String, Object> style) { Element elem = null; String fontColor = mxUtils.getString(style, mxConstants.STYLE_FONTCOLOR, "black"); String fontFamily = mxUtils.getString(style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILIES); int fontSize = (int) (mxUtils.getInt(style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) * scale); if (text != null && text.length() > 0) { elem = document.createElement("text"); // Applies the opacity float opacity = mxUtils.getFloat(style, mxConstants.STYLE_TEXT_OPACITY, 100); if (opacity != 100) { String value = String.valueOf(opacity / 100); elem.setAttribute("fill-opacity", value); elem.setAttribute("stroke-opacity", value); } elem.setAttribute("text-anchor", "middle"); elem.setAttribute("font-weight", "normal"); elem.setAttribute("font-decoration", "none"); elem.setAttribute("font-size", String.valueOf(fontSize)); elem.setAttribute("font-family", fontFamily); elem.setAttribute("fill", fontColor); String[] lines = text.split("\n"); y += fontSize + (h - lines.length * (fontSize + mxConstants.LINESPACING)) / 2 - 2; for (int i = 0; i < lines.length; i++) { Element tspan = document.createElement("tspan"); tspan.setAttribute("x", String.valueOf(x + w / 2)); tspan.setAttribute("y", String.valueOf(y)); tspan.appendChild(document.createTextNode(lines[i])); elem.appendChild(tspan); y += fontSize + mxConstants.LINESPACING; } appendSvgElement(elem); } return elem; } }