/** * $Id: mxUtils.java,v 1.10 2014/01/16 14:39:49 gaudenz Exp $ * Copyright (c) 2007-2012, JGraph Ltd */ package com.mxgraph.util; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Formatter; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.Stack; import java.util.TreeSet; import javax.imageio.ImageIO; import javax.swing.text.html.HTMLDocument; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import com.mxgraph.io.mxCodecRegistry; import com.mxgraph.model.mxCellPath; import com.mxgraph.model.mxICell; import com.mxgraph.model.mxIGraphModel; import com.mxgraph.view.mxCellState; /** * Contains various helper methods for use with mxGraph. */ public class mxUtils { /** * True if the machine is a Mac. */ public static boolean IS_MAC = System.getProperty("os.name").toLowerCase() .indexOf("mac") >= 0; /** * True if the machine is running a linux kernel. */ public static boolean IS_LINUX = System.getProperty("os.name") .toLowerCase().indexOf("linux") >= 0; /** * Static Graphics used for Font Metrics. */ protected static transient Graphics fontGraphics; // Creates a renderer for HTML markup (only possible in // non-headless environment) static { try { fontGraphics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) .getGraphics(); } catch (Exception e) { // ignore } } /** * Returns the size for the given label. If isHtml is true then any HTML * markup in the label is computed as HTML and all newlines inside the HTML * body are converted into linebreaks. */ public static mxRectangle getLabelSize(String label, Map<String, Object> style, boolean isHtml, double scale) { return getLabelSize(label, style, isHtml, scale, 0); } /** * Returns the size for the given label. If isHtml is true then any HTML * markup in the label is computed as HTML and all newlines inside the HTML * body are converted into linebreaks. */ public static mxRectangle getLabelSize(String label, Map<String, Object> style, boolean isHtml, double scale, double htmlWrapWidth) { mxRectangle size; if (isHtml) { size = getSizeForHtml(getBodyMarkup(label, true), style, scale, htmlWrapWidth); } else { size = getSizeForString(label, getFont(style), scale); } return size; } /** * Returns the body part of the given HTML markup. */ public static String getBodyMarkup(String markup, boolean replaceLinefeeds) { String lowerCase = markup.toLowerCase(); int bodyStart = lowerCase.indexOf("<body>"); if (bodyStart >= 0) { bodyStart += 7; int bodyEnd = lowerCase.lastIndexOf("</body>"); if (bodyEnd > bodyStart) { markup = markup.substring(bodyStart, bodyEnd).trim(); } } if (replaceLinefeeds) { markup = markup.replaceAll("\n", "<br>"); } return markup; } /** * Returns the paint bounds for the given label. */ public static mxRectangle getLabelPaintBounds(String label, Map<String, Object> style, boolean isHtml, mxPoint offset, mxRectangle vertexBounds, double scale) { return getLabelPaintBounds(label, style, isHtml, offset, vertexBounds, scale, false); } /** * Returns the paint bounds for the given label. */ public static mxRectangle getLabelPaintBounds(String label, Map<String, Object> style, boolean isHtml, mxPoint offset, mxRectangle vertexBounds, double scale, boolean isEdge) { double wrapWidth = 0; if (isHtml && vertexBounds != null && mxUtils.getString(style, mxConstants.STYLE_WHITE_SPACE, "nowrap").equals("wrap")) { wrapWidth = vertexBounds.getWidth(); } mxRectangle size = mxUtils.getLabelSize(label, style, isHtml, scale, wrapWidth); // Measures font with full scale and scales back size.setWidth(size.getWidth() / scale); size.setHeight(size.getHeight() / scale); double x = offset.getX(); double y = offset.getY(); double width = 0; double height = 0; if (vertexBounds != null) { x += vertexBounds.getX(); y += vertexBounds.getY(); if (mxUtils.getString(style, mxConstants.STYLE_SHAPE, "").equals( mxConstants.SHAPE_SWIMLANE)) { // Limits the label to the swimlane title boolean horizontal = mxUtils.isTrue(style, mxConstants.STYLE_HORIZONTAL, true); double start = mxUtils.getDouble(style, mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE) * scale; if (horizontal) { width += vertexBounds.getWidth(); height += start; } else { width += start; height += vertexBounds.getHeight(); } } else { width += (isEdge) ? 0 : vertexBounds.getWidth(); height += vertexBounds.getHeight(); } } return mxUtils.getScaledLabelBounds(x, y, size, width, height, style, scale); } /** * Returns the bounds for a label for the given location and size, taking * into account the alignment and spacing in the specified style, as well as * the width and height of the rectangle that contains the label. (For edge * labels this width and height is 0.) The scale is used to scale the given * size and the spacings in the specified style. */ public static mxRectangle getScaledLabelBounds(double x, double y, mxRectangle size, double outerWidth, double outerHeight, Map<String, Object> style, double scale) { double inset = mxConstants.LABEL_INSET * scale; // Scales the size of the label // FIXME: Correct rounded font size and not-rounded scale double width = size.getWidth() * scale + 2 * inset; double height = size.getHeight() * scale + 2 * inset; // Gets the global spacing and orientation boolean horizontal = isTrue(style, mxConstants.STYLE_HORIZONTAL, true); int spacing = (int) (getInt(style, mxConstants.STYLE_SPACING) * scale); // Gets the alignment settings Object align = getString(style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER); Object valign = getString(style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE); // Gets the vertical spacing int top = (int) (getInt(style, mxConstants.STYLE_SPACING_TOP) * scale); int bottom = (int) (getInt(style, mxConstants.STYLE_SPACING_BOTTOM) * scale); // Gets the horizontal spacing int left = (int) (getInt(style, mxConstants.STYLE_SPACING_LEFT) * scale); int right = (int) (getInt(style, mxConstants.STYLE_SPACING_RIGHT) * scale); // Applies the orientation to the spacings and dimension if (!horizontal) { int tmp = top; top = right; right = bottom; bottom = left; left = tmp; double tmp2 = width; width = height; height = tmp2; } // Computes the position of the label for the horizontal alignment if ((horizontal && align.equals(mxConstants.ALIGN_CENTER)) || (!horizontal && valign.equals(mxConstants.ALIGN_MIDDLE))) { x += (outerWidth - width) / 2 + left - right; } else if ((horizontal && align.equals(mxConstants.ALIGN_RIGHT)) || (!horizontal && valign.equals(mxConstants.ALIGN_BOTTOM))) { x += outerWidth - width - spacing - right; } else { x += spacing + left; } // Computes the position of the label for the vertical alignment if ((!horizontal && align.equals(mxConstants.ALIGN_CENTER)) || (horizontal && valign.equals(mxConstants.ALIGN_MIDDLE))) { y += (outerHeight - height) / 2 + top - bottom; } else if ((!horizontal && align.equals(mxConstants.ALIGN_LEFT)) || (horizontal && valign.equals(mxConstants.ALIGN_BOTTOM))) { y += outerHeight - height - spacing - bottom; } else { y += spacing + top; } return new mxRectangle(x, y, width, height); } /** * Returns the font metrics of the static font graphics instance * @param font The font whose metrics are to be returned * @return the font metrics of the specified font */ public static FontMetrics getFontMetrics(Font font) { if (fontGraphics != null) { return fontGraphics.getFontMetrics(font); } return null; } /** * Returns an <mxRectangle> with the size (width and height in pixels) of * the given string. * * @param text * String whose size should be returned. * @param font * Font to be used for the computation. */ public static mxRectangle getSizeForString(String text, Font font, double scale) { FontRenderContext frc = new FontRenderContext(null, false, false); font = font.deriveFont((float) (font.getSize2D() * scale)); FontMetrics metrics = null; if (fontGraphics != null) { metrics = fontGraphics.getFontMetrics(font); } double lineHeight = mxConstants.LINESPACING; if (metrics != null) { lineHeight += metrics.getHeight(); } else { lineHeight += font.getSize2D() * 1.27; } String[] lines = text.split("\n"); Rectangle2D boundingBox = null; if (lines.length == 0) { boundingBox = font.getStringBounds("", frc); } else { for (int i = 0; i < lines.length; i++) { Rectangle2D bounds = font.getStringBounds(lines[i], frc); if (boundingBox == null) { boundingBox = bounds; } else { boundingBox .setFrame( 0, 0, Math.max(boundingBox.getWidth(), bounds.getWidth()), boundingBox.getHeight() + lineHeight); } } } return new mxRectangle(boundingBox); } /** * Returns the specified text in lines that fit within the specified * width when the specified font metrics are applied to the text * @param text the text to wrap * @param metrics the font metrics to calculate the text size for * @param width the width that the text must fit within * @return the input text split in lines that fit the specified width */ public static String[] wordWrap(String text, FontMetrics metrics, double width) { List<String> result = new ArrayList<String>(); // First split the processing into lines already delimited by // newlines. We want the result to retain all newlines in position. String[] lines = text.split("\n"); for (int i = 0; i < lines.length; i++) { int lineWidth = 0; // the display width of the current line int charCount = 0; // keeps count of current position in the line StringBuilder currentLine = new StringBuilder(); // Split the words of the current line by spaces and tabs // The words are trimmed of tabs, space and newlines, therefore String[] words = lines[i].split("\\s+"); // Need to a form a stack of the words in reverse order // This is because if a word is split during the process // the remainder of the word is added to the front of the // stack and processed next Stack<String> wordStack = new Stack<String>(); for (int j = words.length - 1; j >= 0; j--) { wordStack.push(words[j]); } while (!wordStack.isEmpty()) { String word = wordStack.pop(); // Work out what whitespace exists before this word. // and add the width of the whitespace to the calculation int whitespaceCount = 0; if (word.length() > 0) { // Concatenate any preceding whitespace to the // word and calculate the number of characters of that // whitespace char firstWordLetter = word.charAt(0); int letterIndex = lines[i].indexOf(firstWordLetter, charCount); String whitespace = lines[i].substring(charCount, letterIndex); whitespaceCount = whitespace.length(); word = whitespace.concat(word); } double wordLength; // If the line width is zero, we are at the start of a newline // We don't proceed preceeding whitespace in the width // calculation if (lineWidth > 0) { wordLength = metrics.stringWidth(word); } else { wordLength = metrics.stringWidth(word.trim()); } // Does the width of line so far plus the width of the // current word exceed the allowed width? if (lineWidth + wordLength > width) { if (lineWidth > 0) { // There is already at least one word on this line // and the current word takes the overall width over // the allowed width. Because there is something on // the line, complete the current line, reset the width // counter, create a new line and put the current word // back on the stack for processing in the next round result.add(currentLine.toString()); currentLine = new StringBuilder(); wordStack.push(word.trim()); lineWidth = 0; } else if (mxConstants.SPLIT_WORDS) { // There are no words on the current line and the // current word does not fit on it. Find the maximum // number of characters of this word that just fit // in the available width word = word.trim(); for (int j = 1; j <= word.length(); j++) { wordLength = metrics.stringWidth(word.substring(0, j)); if (lineWidth + wordLength > width) { // The last character took us over the allowed // width, deducted it unless there is only one // character, in which case we have to use it // since we can't split it... j = j > 1 ? j - 1 : j; String chars = word.substring(0, j); currentLine = currentLine.append(chars); // Return the unprocessed part of the word // to the stack wordStack .push(word.substring(j, word.length())); result.add(currentLine.toString()); currentLine = new StringBuilder(); lineWidth = 0; // Increment char counter allowing for white // space in the original word charCount = charCount + chars.length() + whitespaceCount; break; } } } else { // There are no words on the current line, but // we are not splitting. word = word.trim(); result.add(word); currentLine = new StringBuilder(); lineWidth = 0; // Increment char counter allowing for white // space in the original word charCount = word.length() + whitespaceCount; } } else { // The current word does not take the total line width // over the allowed width. Append the word, removing // preceeding whitespace if it is the first word in the // line. if (lineWidth > 0) { currentLine = currentLine.append(word); } else { currentLine = currentLine.append(word.trim()); } lineWidth += wordLength; charCount += word.length(); } } result.add(currentLine.toString()); } return result.toArray(new String[result.size()]); } /** * Returns an mxRectangle with the size (width and height in pixels) of the * given HTML markup. * * @param markup * HTML markup whose size should be returned. */ public static mxRectangle getSizeForHtml(String markup, Map<String, Object> style, double scale, double wrapWidth) { mxLightweightLabel textRenderer = mxLightweightLabel .getSharedInstance(); if (textRenderer != null) { // First run measures size with no wrapping textRenderer.setText(createHtmlDocument(style, markup)); Dimension size = textRenderer.getPreferredSize(); // Second run measures size with wrapping if required. // Note that this is only required because max-width // is not supported and we can't get the width of an // inner HTML element (or is this possible?). if (wrapWidth > 0) { textRenderer.setText(createHtmlDocument( style, markup, 1, (int) Math.ceil(wrapWidth - mxConstants.LABEL_INSET * scale))); Dimension size2 = textRenderer.getPreferredSize(); // Uses wrapped text size if any text was actually wrapped if (size2.width < size.width) { size = size2; } } return new mxRectangle(0, 0, size.width * scale, size.height * scale); } else { return getSizeForString(markup, getFont(style), scale); } } /** * Function: arcToCurves * * Converts the given arc to a series of curves. */ public static double[] arcToCurves(double x0, double y0, double r1, double r2, double angle, double largeArcFlag, double sweepFlag, double x, double y) { x -= x0; y -= y0; if (r1 == 0 || r2 == 0) { return new double[0]; } double fS = sweepFlag; double psai = angle; r1 = Math.abs(r1); r2 = Math.abs(r2); double ctx = -x / 2; double cty = -y / 2; double cpsi = Math.cos(psai * Math.PI / 180); double spsi = Math.sin(psai * Math.PI / 180); double rxd = cpsi * ctx + spsi * cty; double ryd = -1 * spsi * ctx + cpsi * cty; double rxdd = rxd * rxd; double rydd = ryd * ryd; double r1x = r1 * r1; double r2y = r2 * r2; double lamda = rxdd / r1x + rydd / r2y; double sds; if (lamda > 1) { r1 = Math.sqrt(lamda) * r1; r2 = Math.sqrt(lamda) * r2; sds = 0; } else { double seif = 1; if (largeArcFlag == fS) { seif = -1; } sds = seif * Math.sqrt((r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd)); } double txd = sds * r1 * ryd / r2; double tyd = -1 * sds * r2 * rxd / r1; double tx = cpsi * txd - spsi * tyd + x / 2; double ty = spsi * txd + cpsi * tyd + y / 2; double rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1); double s1 = (rad >= 0) ? rad : 2 * Math.PI + rad; rad = Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) - Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1); double dr = (rad >= 0) ? rad : 2 * Math.PI + rad; if (fS == 0 && dr > 0) { dr -= 2 * Math.PI; } else if (fS != 0 && dr < 0) { dr += 2 * Math.PI; } double sse = dr * 2 / Math.PI; int seg = (int) Math.ceil(sse < 0 ? -1 * sse : sse); double segr = dr / seg; double t = 8 / 3 * Math.sin(segr / 4) * Math.sin(segr / 4) / Math.sin(segr / 2); double cpsir1 = cpsi * r1; double cpsir2 = cpsi * r2; double spsir1 = spsi * r1; double spsir2 = spsi * r2; double mc = Math.cos(s1); double ms = Math.sin(s1); double x2 = -t * (cpsir1 * ms + spsir2 * mc); double y2 = -t * (spsir1 * ms - cpsir2 * mc); double x3 = 0; double y3 = 0; double[] result = new double[seg * 6]; for (int n = 0; n < seg; ++n) { s1 += segr; mc = Math.cos(s1); ms = Math.sin(s1); x3 = cpsir1 * mc - spsir2 * ms + tx; y3 = spsir1 * mc + cpsir2 * ms + ty; double dx = -t * (cpsir1 * ms + spsir2 * mc); double dy = -t * (spsir1 * ms - cpsir2 * mc); // CurveTo updates x0, y0 so need to restore it int index = n * 6; result[index] = x2 + x0; result[index + 1] = y2 + y0; result[index + 2] = x3 - dx + x0; result[index + 3] = y3 - dy + y0; result[index + 4] = x3 + x0; result[index + 5] = y3 + y0; x2 = x3 + dx; y2 = y3 + dy; } return result; } /** * Returns the bounding box for the rotated rectangle. */ public static mxRectangle getBoundingBox(mxRectangle rect, double rotation) { mxRectangle result = null; if (rect != null && rotation != 0) { double rad = Math.toRadians(rotation); double cos = Math.cos(rad); double sin = Math.sin(rad); mxPoint cx = new mxPoint(rect.getX() + rect.getWidth() / 2, rect.getY() + rect.getHeight() / 2); mxPoint p1 = new mxPoint(rect.getX(), rect.getY()); mxPoint p2 = new mxPoint(rect.getX() + rect.getWidth(), rect.getY()); mxPoint p3 = new mxPoint(p2.getX(), rect.getY() + rect.getHeight()); mxPoint p4 = new mxPoint(rect.getX(), p3.getY()); p1 = getRotatedPoint(p1, cos, sin, cx); p2 = getRotatedPoint(p2, cos, sin, cx); p3 = getRotatedPoint(p3, cos, sin, cx); p4 = getRotatedPoint(p4, cos, sin, cx); Rectangle tmp = new Rectangle((int) p1.getX(), (int) p1.getY(), 0, 0); tmp.add(p2.getPoint()); tmp.add(p3.getPoint()); tmp.add(p4.getPoint()); result = new mxRectangle(tmp); } else if (rect != null) { result = (mxRectangle) rect.clone(); } return result; } /** * Find the first character matching the input character in the given * string where the character has no letter preceding it. * * @param text the string to test for the presence of the input character * @param inputChar the test character * @param fromIndex the index position of the string to start from * @return the position of the first character matching the input character * in the given string where the character has no letter preceding it. */ public static int firstCharAt(String text, int inputChar, int fromIndex) { int result = 0; while (result >= 0) { result = text.indexOf(inputChar, fromIndex); if (result == 0) { return result; } else if (result > 0) { // Check there is a whitespace or symbol before the hit character if (Character.isLetter(text.codePointAt(result - 1))) { // The pre-increment is used in if and else branches. if (++fromIndex >= text.length()) { return -1; } else { // Test again from next candidate character // This isn't the first letter of this word result = text.indexOf(inputChar, fromIndex); } } else { return result; } } } return result; } /** * Rotates the given point by the given cos and sin. */ public static mxPoint getRotatedPoint(mxPoint pt, double cos, double sin) { return getRotatedPoint(pt, cos, sin, new mxPoint()); } /** * Finds the index of the nearest segment on the given cell state for the * specified coordinate pair. */ public static int findNearestSegment(mxCellState state, double x, double y) { int index = -1; if (state.getAbsolutePointCount() > 0) { mxPoint last = state.getAbsolutePoint(0); double min = Double.MAX_VALUE; for (int i = 1; i < state.getAbsolutePointCount(); i++) { mxPoint current = state.getAbsolutePoint(i); double dist = new Line2D.Double(last.x, last.y, current.x, current.y).ptSegDistSq(x, y); if (dist < min) { min = dist; index = i - 1; } last = current; } } return index; } /** * Rotates the given point by the given cos and sin. */ public static mxPoint getRotatedPoint(mxPoint pt, double cos, double sin, mxPoint c) { double x = pt.getX() - c.getX(); double y = pt.getY() - c.getY(); double x1 = x * cos - y * sin; double y1 = y * cos + x * sin; return new mxPoint(x1 + c.getX(), y1 + c.getY()); } /** * Returns an integer mask of the port constraints of the given map * @param terminal the cached cell state of the cell to determine the * port constraints for * @param edge the edge connected to the constrained terminal * @param source whether or not the edge specified is connected to the * terminal specified at its source end * @return the mask of port constraint directions */ public static int getPortConstraints(mxCellState terminal, mxCellState edge, boolean source) { return getPortConstraints(terminal, edge, source, mxConstants.DIRECTION_MASK_ALL); } /** * Returns an integer mask of the port constraints of the given map * @param terminal the cached cell state of the cell to determine the * port constraints for * @param edge the edge connected to the constrained terminal * @param source whether or not the edge specified is connected to the * terminal specified at its source end * @param defaultValue Default value to return if the key is undefined. * @return the mask of port constraint directions */ public static int getPortConstraints(mxCellState terminal, mxCellState edge, boolean source, int defaultValue) { Object value = terminal.getStyle().get( mxConstants.STYLE_PORT_CONSTRAINT); if (value == null) { return defaultValue; } else { String directions = value.toString(); int returnValue = mxConstants.DIRECTION_MASK_NONE; if (directions.indexOf(mxConstants.DIRECTION_NORTH) >= 0) { returnValue |= mxConstants.DIRECTION_MASK_NORTH; } if (directions.indexOf(mxConstants.DIRECTION_WEST) >= 0) { returnValue |= mxConstants.DIRECTION_MASK_WEST; } if (directions.indexOf(mxConstants.DIRECTION_SOUTH) >= 0) { returnValue |= mxConstants.DIRECTION_MASK_SOUTH; } if (directions.indexOf(mxConstants.DIRECTION_EAST) >= 0) { returnValue |= mxConstants.DIRECTION_MASK_EAST; } return returnValue; } } public static int reversePortConstraints(int constraint) { int result = 0; result = (constraint & mxConstants.DIRECTION_MASK_WEST) << 3; result |= (constraint & mxConstants.DIRECTION_MASK_NORTH) << 1; result |= (constraint & mxConstants.DIRECTION_MASK_SOUTH) >> 1; result |= (constraint & mxConstants.DIRECTION_MASK_EAST) >> 3; return result; } /** * Draws the image inside the clip bounds to the given graphics object. */ public static void drawImageClip(Graphics g, BufferedImage image, ImageObserver observer) { Rectangle clip = g.getClipBounds(); if (clip != null) { int w = image.getWidth(); int h = image.getHeight(); int x = Math.max(0, Math.min(clip.x, w)); int y = Math.max(0, Math.min(clip.y, h)); w = Math.min(clip.width, w - x); h = Math.min(clip.height, h - y); if (w > 0 && h > 0) { // TODO: Support for normal images using fast subimage copies g.drawImage(image.getSubimage(x, y, w, h), clip.x, clip.y, observer); } } else { g.drawImage(image, 0, 0, observer); } } /** * */ public static void fillClippedRect(Graphics g, int x, int y, int width, int height) { Rectangle bg = new Rectangle(x, y, width, height); try { if (g.getClipBounds() != null) { bg = bg.intersection(g.getClipBounds()); } } catch (Exception e) { // FIXME: Getting clipbounds sometimes throws an NPE } g.fillRect(bg.x, bg.y, bg.width, bg.height); } /** * Creates a new list of new points obtained by translating the points in * the given list by the given vector. Elements that are not mxPoints are * added to the result as-is. */ public static List<mxPoint> translatePoints(List<mxPoint> pts, double dx, double dy) { List<mxPoint> result = null; if (pts != null) { result = new ArrayList<mxPoint>(pts.size()); Iterator<mxPoint> it = pts.iterator(); while (it.hasNext()) { mxPoint point = (mxPoint) it.next().clone(); point.setX(point.getX() + dx); point.setY(point.getY() + dy); result.add(point); } } return result; } /** * Returns the intersection of two lines as an mxPoint. * * @param x0 * X-coordinate of the first line's startpoint. * @param y0 * Y-coordinate of the first line's startpoint. * @param x1 * X-coordinate of the first line's endpoint. * @param y1 * Y-coordinate of the first line's endpoint. * @param x2 * X-coordinate of the second line's startpoint. * @param y2 * Y-coordinate of the second line's startpoint. * @param x3 * X-coordinate of the second line's endpoint. * @param y3 * Y-coordinate of the second line's endpoint. * @return Returns the intersection between the two lines. */ public static mxPoint intersection(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) { double denom = ((y3 - y2) * (x1 - x0)) - ((x3 - x2) * (y1 - y0)); double nume_a = ((x3 - x2) * (y0 - y2)) - ((y3 - y2) * (x0 - x2)); double nume_b = ((x1 - x0) * (y0 - y2)) - ((y1 - y0) * (x0 - x2)); double ua = nume_a / denom; double ub = nume_b / denom; if (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) { // Get the intersection point double intersectionX = x0 + ua * (x1 - x0); double intersectionY = y0 + ua * (y1 - y0); return new mxPoint(intersectionX, intersectionY); } // No intersection return null; } /** * Sorts the given cells according to the order in the cell hierarchy. */ public static Object[] sortCells(Object[] cells, final boolean ascending) { return sortCells(Arrays.asList(cells), ascending).toArray(); } /** * Sorts the given cells according to the order in the cell hierarchy. */ public static Collection<Object> sortCells(Collection<Object> cells, final boolean ascending) { SortedSet<Object> result = new TreeSet<Object>(new Comparator<Object>() { public int compare(Object o1, Object o2) { int comp = mxCellPath.compare(mxCellPath.create((mxICell) o1), mxCellPath.create((mxICell) o2)); return (comp == 0) ? 0 : (((comp > 0) == ascending) ? 1 : -1); } }); result.addAll(cells); return result; } /** * Returns true if the given array contains the given object. */ public static boolean contains(Object[] array, Object obj) { return indexOf(array, obj) >= 0; } /** * Returns the index of the given object in the given array of -1 if the * object is not contained in the array. */ public static int indexOf(Object[] array, Object obj) { if (obj != null && array != null) { for (int i = 0; i < array.length; i++) { if (array[i] == obj) { return i; } } } return -1; } /** * Returns the stylename in a style of the form stylename[;key=value] or an * empty string if the given style does not contain a stylename. * * @param style * String of the form stylename[;key=value]. * @return Returns the stylename from the given formatted string. * @deprecated Use <code>mxStyleUtils.getStylename(String)</code> (Jan 2012) */ public static String getStylename(String style) { return mxStyleUtils.getStylename(style); } /** * 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. * @deprecated Use <code>mxStyleUtils.getStylenames(String)</code> (Jan 2012) */ public static String[] getStylenames(String style) { return mxStyleUtils.getStylenames(style); } /** * Returns the index of the given stylename in the given style. This returns * -1 if the given stylename does not occur (as a stylename) in the given * style, otherwise it returns the index of the first character. * @deprecated Use <code>mxStyleUtils.indexOfStylename(String, String)</code> (Jan 2012) */ public static int indexOfStylename(String style, String stylename) { return mxStyleUtils.indexOfStylename(style, stylename); } /** * Removes all stylenames from the given style and returns the updated * style. * @deprecated Use <code>mxStyleUtils.removeAllStylenames(String)</code> (Jan 2012) */ public static String removeAllStylenames(String style) { return mxStyleUtils.removeAllStylenames(style); } /** * Assigns the value for the given key in the styles of the given cells, or * removes the key from the styles if the value is null. * * @param model * Model to execute the transaction in. * @param cells * Array of cells to be updated. * @param key * Key of the style to be changed. * @param value * New value for the given key. * @deprecated Use <code>mxStyleUtils.setCellStyles(mxIGraphModel, Object[], String, String)</code> (Jan 2012) */ public static void setCellStyles(mxIGraphModel model, Object[] cells, String key, String value) { mxStyleUtils.setCellStyles(model, cells, key, value); } /** * Adds or removes the given key, value pair to the style and returns the * new style. If value is null or zero length then the key is removed from * the style. * * @param style * String of the form <code>stylename[;key=value]</code>. * @param key * Key of the style to be changed. * @param value * New value for the given key. * @return Returns the new style. * @deprecated Use <code>mxStyleUtils.setStyle(String, String, String)</code> (Jan 2012) */ public static String setStyle(String style, String key, String value) { return mxStyleUtils.setStyle(style, key, value); } /** * Sets or toggles the flag bit for the given key in the cell's styles. If * value is null then the flag is toggled. * * <code> * mxUtils.setCellStyleFlags(graph.getModel(), * cells, * mxConstants.STYLE_FONTSTYLE, * mxConstants.FONT_BOLD, null); * </code> * * Toggles the bold font style. * * @param model * Model that contains the cells. * @param cells * Array of cells to change the style for. * @param key * Key of the style to be changed. * @param flag * Integer for the bit to be changed. * @param value * Optional boolean value for the flag. * @deprecated Use <code>mxStyleUtils.setCellStyleFlags(mxIGraphModel, Object[],String, int, Boolean)</code> (Jan 2012) */ public static void setCellStyleFlags(mxIGraphModel model, Object[] cells, String key, int flag, Boolean value) { mxStyleUtils.setCellStyleFlags(model, cells, key, flag, value); } /** * Sets or removes the given key from the specified style and returns the * new style. If value is null then the flag is toggled. * * @param style * String of the form stylename[;key=value]. * @param key * Key of the style to be changed. * @param flag * Integer for the bit to be changed. * @param value * Optional boolean value for the given flag. * @deprecated Use <code>mxStyleUtils.setStyleFlag(String, String, int, Boolean)</code> (Jan 2012) */ public static String setStyleFlag(String style, String key, int flag, Boolean value) { return mxStyleUtils.setStyleFlag(style, key, flag, value); } public static boolean intersectsHotspot(mxCellState state, int x, int y, double hotspot) { return intersectsHotspot(state, x, y, hotspot, 0, 0); } /** * Returns true if the given coordinate pair intersects the hotspot of the * given state. */ public static boolean intersectsHotspot(mxCellState state, int x, int y, double hotspot, int min, int max) { if (hotspot > 0) { int cx = (int) Math.round(state.getCenterX()); int cy = (int) Math.round(state.getCenterY()); int width = (int) Math.round(state.getWidth()); int height = (int) Math.round(state.getHeight()); if (mxUtils .getString(state.getStyle(), mxConstants.STYLE_SHAPE, "") .equals(mxConstants.SHAPE_SWIMLANE)) { int start = mxUtils.getInt(state.getStyle(), mxConstants.STYLE_STARTSIZE, mxConstants.DEFAULT_STARTSIZE); if (mxUtils.isTrue(state.getStyle(), mxConstants.STYLE_HORIZONTAL, true)) { cy = (int) Math.round(state.getY() + start / 2); height = start; } else { cx = (int) Math.round(state.getX() + start / 2); width = start; } } int w = (int) Math.max(min, width * hotspot); int h = (int) Math.max(min, height * hotspot); if (max > 0) { w = Math.min(w, max); h = Math.min(h, max); } Rectangle rect = new Rectangle(Math.round(cx - w / 2), Math.round(cy - h / 2), w, h); return rect.contains(x, y); } return true; } /** * Returns true if the dictionary contains true for the given key or false * if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @return Returns the boolean value for key in dict. */ public static boolean isTrue(Map<String, Object> dict, String key) { return isTrue(dict, key, false); } /** * Returns true if the dictionary contains true for the given key or the * given default value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the boolean value for key in dict. */ public static boolean isTrue(Map<String, Object> dict, String key, boolean defaultValue) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { return value.equals("1") || value.toString().toLowerCase().equals("true"); } } /** * Returns the value for key in dictionary as an int or 0 if no value is * defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @return Returns the integer value for key in dict. */ public static int getInt(Map<String, Object> dict, String key) { return getInt(dict, key, 0); } /** * Returns the value for key in dictionary as an int or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the integer value for key in dict. */ public static int getInt(Map<String, Object> dict, String key, int defaultValue) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { // Handles commas by casting them to an int return (int) Float.parseFloat(value.toString()); } } /** * Returns the value for key in dictionary as a float or 0 if no value is * defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @return Returns the float value for key in dict. */ public static float getFloat(Map<String, Object> dict, String key) { return getFloat(dict, key, 0); } /** * Returns the value for key in dictionary as a float or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the float value for key in dict. */ public static float getFloat(Map<String, Object> dict, String key, float defaultValue) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { return Float.parseFloat(value.toString()); } } /** * Returns the value for key in dictionary as a float array or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the float array value for key in dict. */ public static float[] getFloatArray(Map<String, Object> dict, String key, float[] defaultValue) { return getFloatArray(dict, key, defaultValue, ","); } /** * Returns the value for key in dictionary as a float array or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the float array value for key in dict. */ public static float[] getFloatArray(Map<String, Object> dict, String key, float[] defaultValue, String separator) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { String[] floatChars = value.toString().split(separator); float[] result = new float[floatChars.length]; for (int i = 0; i < floatChars.length; i++) { result[i] = Float.parseFloat(floatChars[i]); } return result; } } /** * Returns the value for key in dictionary as a double or 0 if no value is * defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @return Returns the double value for key in dict. */ public static double getDouble(Map<String, Object> dict, String key) { return getDouble(dict, key, 0); } /** * Returns the value for key in dictionary as a double or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the double value for key in dict. */ public static double getDouble(Map<String, Object> dict, String key, double defaultValue) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { return Double.parseDouble(value.toString()); } } /** * Returns the value for key in dictionary as a string or null if no value * is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @return Returns the string value for key in dict. */ public static String getString(Map<String, Object> dict, String key) { return getString(dict, key, null); } /** * Returns the value for key in dictionary as a string or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the string value for key in dict. */ public static String getString(Map<String, Object> dict, String key, String defaultValue) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { return value.toString(); } } /** * Returns the value for key in dictionary as a color or null if no value is * defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @return Returns the color value for key in dict. */ public static Color getColor(Map<String, Object> dict, String key) { return getColor(dict, key, null); } /** * Returns the value for key in dictionary as a color or the given default * value if no value is defined for the key. * * @param dict * Dictionary that contains the key, value pairs. * @param key * Key whose value should be returned. * @param defaultValue * Default value to return if the key is undefined. * @return Returns the color value for key in dict. */ public static Color getColor(Map<String, Object> dict, String key, Color defaultValue) { Object value = dict.get(key); if (value == null) { return defaultValue; } else { return parseColor(value.toString()); } } /** * */ public static Font getFont(Map<String, Object> style) { return getFont(style, 1); } /** * */ public static Font getFont(Map<String, Object> style, double scale) { String fontFamily = getString(style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY); int fontSize = getInt(style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE); int fontStyle = getInt(style, mxConstants.STYLE_FONTSTYLE); int swingFontStyle = ((fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) ? Font.BOLD : Font.PLAIN; swingFontStyle += ((fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) ? Font.ITALIC : Font.PLAIN; return new Font(fontFamily, swingFontStyle, (int) (fontSize * scale)); } /** * */ public static String hexString(Color color) { return mxHtmlColor.hexString(color); } /** * Convert a string representing a 24/32bit hex color value into a Color * object. The following color names are also supported: white, black, red, * green, blue, orange, yellow, pink, turquoise, gray and none (null). * Examples of possible hex color values are: #C3D9FF, #6482B9 and #774400, * but note that you do not include the "#" in the string passed in * * @param colorString * the 24/32bit hex string value (ARGB) * @return java.awt.Color (24bit RGB on JDK 1.1, 24/32bit ARGB on JDK1.2) * @exception NumberFormatException * if the specified string cannot be interpreted as a * hexidecimal integer */ public static Color parseColor(String colorString) throws NumberFormatException { return mxHtmlColor.parseColor(colorString); } /** * Returns a hex representation for the given color. * * @param color * Color to return the hex string for. * @return Returns a hex string for the given color. */ public static String getHexColorString(Color color) { return mxHtmlColor.getHexColorString(color); } /** * Reads the given filename into a string. * * @param filename * Name of the file to be read. * @return Returns a string representing the file contents. * @throws IOException */ public static String readFile(String filename) throws IOException { return readInputStream(new FileInputStream(filename)); } /** * Reads the given filename into a string. * * @param filename * Name of the file to be read. * @return Returns a string representing the file contents. * @throws IOException */ public static String readInputStream(InputStream stream) throws IOException { BufferedReader reader = new BufferedReader( new InputStreamReader(stream)); StringBuffer result = new StringBuffer(); String tmp = reader.readLine(); while (tmp != null) { result.append(tmp + "\n"); tmp = reader.readLine(); } reader.close(); return result.toString(); } /** * Writes the given string into the given file. * * @param contents * String representing the file contents. * @param filename * Name of the file to be written. * @throws IOException */ public static void writeFile(String contents, String filename) throws IOException { FileWriter fw = new FileWriter(filename); fw.write(contents); fw.flush(); fw.close(); } /** * Returns the Md5 hash for the given text. * * @param text * String whose Md5 hash should be returned. * @return Returns the Md5 hash for the given text. */ public static String getMd5Hash(String text) { StringBuffer result = new StringBuffer(32); try { MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(text.getBytes()); Formatter f = new Formatter(result); byte[] digest = md5.digest(); for (int i = 0; i < digest.length; i++) { f.format("%02x", new Object[] { new Byte(digest[i]) }); } } catch (NoSuchAlgorithmException ex) { ex.printStackTrace(); } return result.toString(); } /** * Returns true if the user object is an XML node with the specified type * and and the optional attribute has the specified value or is not * specified. * * @param value * Object that should be examined as a node. * @param nodeName * String that specifies the node name. * @return Returns true if the node name of the user object is equal to the * given type. */ public static boolean isNode(Object value, String nodeName) { return isNode(value, nodeName, null, null); } /** * Returns true if the given value is an XML node with the node name and if * the optional attribute has the specified value. * * @param value * Object that should be examined as a node. * @param nodeName * String that specifies the node name. * @param attributeName * Optional attribute name to check. * @param attributeValue * Optional attribute value to check. * @return Returns true if the value matches the given conditions. */ public static boolean isNode(Object value, String nodeName, String attributeName, String attributeValue) { if (value instanceof Element) { Element element = (Element) value; if (nodeName == null || element.getNodeName().equalsIgnoreCase(nodeName)) { String tmp = (attributeName != null) ? element .getAttribute(attributeName) : null; return attributeName == null || (tmp != null && tmp.equals(attributeValue)); } } return false; } /** * * @param g * @param antiAlias * @param textAntiAlias */ public static void setAntiAlias(Graphics2D g, boolean antiAlias, boolean textAntiAlias) { g.setRenderingHint(RenderingHints.KEY_RENDERING, (antiAlias) ? RenderingHints.VALUE_RENDER_QUALITY : RenderingHints.VALUE_RENDER_SPEED); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, (antiAlias) ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, (textAntiAlias) ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); } /** * Clears the given area of the specified graphics object with the given * color or makes the region transparent. */ public static void clearRect(Graphics2D g, Rectangle rect, Color background) { if (background != null) { g.setColor(background); g.fillRect(rect.x, rect.y, rect.width, rect.height); } else { g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f)); g.fillRect(rect.x, rect.y, rect.width, rect.height); g.setComposite(AlphaComposite.SrcOver); } } /** * Creates a buffered image for the given parameters. If there is not enough * memory to create the image then a OutOfMemoryError is thrown. */ public static BufferedImage createBufferedImage(int w, int h, Color background) { BufferedImage result = null; if (w > 0 && h > 0) { int type = (background != null) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; result = new BufferedImage(w, h, type); // Clears background if (background != null) { Graphics2D g2 = result.createGraphics(); clearRect(g2, new Rectangle(w, h), background); g2.dispose(); } } return result; } /** * Loads an image from the local filesystem, a data URI or any other URL. */ public static BufferedImage loadImage(String url) { BufferedImage img = null; if (url != null) { // Parses data URIs of the form data:image/format;base64,xxx if (url.startsWith("data:image/")) { try { int comma = url.indexOf(','); byte[] data = mxBase64.decode(url.substring(comma + 1)); ByteArrayInputStream is = new ByteArrayInputStream(data); img = ImageIO.read(is); } catch (Exception e1) { // ignore } } else { URL realUrl = null; try { realUrl = new URL(url); } catch (Exception e) { realUrl = mxUtils.class.getResource(url); } if (realUrl != null) { try { img = ImageIO.read(realUrl); } catch (Exception e1) { e1.printStackTrace(); } } } } return img; } /** * Creates a table for the given text using the given document to create the * DOM nodes. Returns the outermost table node. */ public static Element createTable(Document document, String text, int x, int y, int w, int h, double scale, Map<String, Object> style) { // Does not use a textbox as this must go inside another VML shape Element table = document.createElement("table"); if (text != null && text.length() > 0) { Element tr = document.createElement("tr"); Element td = document.createElement("td"); table.setAttribute("cellspacing", "0"); table.setAttribute("border", "0"); td.setAttribute("align", mxUtils.getString(style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER)); String fontColor = getString(style, mxConstants.STYLE_FONTCOLOR, "black"); String fontFamily = getString(style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILIES); int fontSize = (int) (getInt(style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) * scale); String s = "position:absolute;" + "left:" + String.valueOf(x) + "px;" + "top:" + String.valueOf(y) + "px;" + "width:" + String.valueOf(w) + "px;" + "height:" + String.valueOf(h) + "px;" + "font-size:" + String.valueOf(fontSize) + "px;" + "font-family:" + fontFamily + ";" + "color:" + fontColor + ";"; if (mxUtils.getString(style, mxConstants.STYLE_WHITE_SPACE, "nowrap").equals("wrap")) { s += "whiteSpace:wrap;"; } // Applies the background color String background = getString(style, mxConstants.STYLE_LABEL_BACKGROUNDCOLOR); if (background != null) { s += "background:" + background + ";"; } // Applies the border color String border = getString(style, mxConstants.STYLE_LABEL_BORDERCOLOR); if (border != null) { s += "border:" + border + " solid 1pt;"; } // Applies the opacity float opacity = getFloat(style, mxConstants.STYLE_TEXT_OPACITY, 100); if (opacity < 100) { // Adds all rules (first for IE) s += "filter:alpha(opacity=" + opacity + ");"; s += "opacity:" + (opacity / 100) + ";"; } td.setAttribute("style", s); String[] lines = text.split("\n"); for (int i = 0; i < lines.length; i++) { td.appendChild(document.createTextNode(lines[i])); td.appendChild(document.createElement("br")); } tr.appendChild(td); table.appendChild(tr); } return table; } /** * Returns a new, empty DOM document. * * @return Returns a new DOM document. * @deprecated Use <code>mxDomUtils.createDocument</code> (Jan 2012) */ public static Document createDocument() { return mxDomUtils.createDocument(); } /** * Creates a new SVG document for the given width and height. * @deprecated Use <code>mxDomUtils.createSvgDocument(int, int)</code> (Jan 2012) */ public static Document createSvgDocument(int width, int height) { return mxDomUtils.createSvgDocument(width, height); } /** * * @deprecated Use <code>mxDomUtils.createVmlDocument</code> (Jan 2012) */ public static Document createVmlDocument() { return mxDomUtils.createVmlDocument(); } /** * Returns a document with a HTML node containing a HEAD and BODY node. * @deprecated Use <code>mxDomUtils.createHtmlDocument</code> (Jan 2012) */ public static Document createHtmlDocument() { return mxDomUtils.createHtmlDocument(); } /** * Returns a new, empty DOM document. * * @return Returns a new DOM document. */ public static String createHtmlDocument(Map<String, Object> style, String text) { return createHtmlDocument(style, text, 1, 0); } /** * Returns a new, empty DOM document. * * @return Returns a new DOM document. */ public static String createHtmlDocument(Map<String, Object> style, String text, double scale) { return createHtmlDocument(style, text, scale, 0); } /** * Returns a new, empty DOM document. * * @return Returns a new DOM document. */ public static String createHtmlDocument(Map<String, Object> style, String text, double scale, int width) { return createHtmlDocument(style, text, scale, width, null); } /** * Returns a new, empty DOM document. The head argument can be used to * provide an optional HEAD section without the HEAD tags as follows: * * <pre> * mxUtils.createHtmlDocument(style, text, 1, 0, "<style type=\"text/css\">.classname { color:red; }</style>") * </pre> * * @return Returns a new DOM document. */ public static String createHtmlDocument(Map<String, Object> style, String text, double scale, int width, String head) { return createHtmlDocument(style, text, scale, width, null, null); }; /** * Returns a new, empty DOM document. The head argument can be used to * provide an optional HEAD section without the HEAD tags as follows: * * <pre> * mxUtils.createHtmlDocument(style, text, 1, 0, "<style type=\"text/css\">.classname { color:red; }</style>") * </pre> * * @return Returns a new DOM document. */ public static String createHtmlDocument(Map<String, Object> style, String text, double scale, int width, String head, String bodyCss) { StringBuffer css = (bodyCss != null) ? new StringBuffer(bodyCss) : new StringBuffer(); css.append("font-family:" + getString(style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILIES) + ";"); css.append("font-size:" + (int) (getInt(style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) * scale) + "pt;"); String color = mxUtils.getString(style, mxConstants.STYLE_FONTCOLOR); if (color != null) { css.append("color:" + color + ";"); } int fontStyle = mxUtils.getInt(style, mxConstants.STYLE_FONTSTYLE); if ((fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) { css.append("font-weight:bold;"); } if ((fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) { css.append("font-style:italic;"); } if ((fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) { css.append("text-decoration:underline;"); } String align = getString(style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); if (align.equals(mxConstants.ALIGN_CENTER)) { css.append("text-align:center;"); } else if (align.equals(mxConstants.ALIGN_RIGHT)) { css.append("text-align:right;"); } if (width > 0) { // LATER: With max-width support, wrapped text can be measured in 1 step css.append("width:" + width + "pt;"); } String result = "<html>"; if (head != null) { result += "<head>" + head + "</head>"; } return result + "<body style=\"" + css.toString() + "\">" + text + "</body></html>"; } /** * Returns a new, empty DOM document. * * @return Returns a new DOM document. */ public static HTMLDocument createHtmlDocumentObject( Map<String, Object> style, double scale) { // Applies the font settings HTMLDocument document = new HTMLDocument(); StringBuffer rule = new StringBuffer("body {"); rule.append("font-family:" + getString(style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILIES) + ";"); rule.append("font-size:" + (int) (getInt(style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) * scale) + "pt;"); String color = mxUtils.getString(style, mxConstants.STYLE_FONTCOLOR); if (color != null) { rule.append("color:" + color + ";"); } int fontStyle = mxUtils.getInt(style, mxConstants.STYLE_FONTSTYLE); if ((fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) { rule.append("font-weight:bold;"); } if ((fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) { rule.append("font-style:italic;"); } if ((fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) { rule.append("text-decoration:underline;"); } String align = getString(style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT); if (align.equals(mxConstants.ALIGN_CENTER)) { rule.append("text-align:center;"); } else if (align.equals(mxConstants.ALIGN_RIGHT)) { rule.append("text-align:right;"); } rule.append("}"); document.getStyleSheet().addRule(rule.toString()); return document; } /** * Returns a new DOM document for the given URI. * * @param uri * URI to parse into the document. * @return Returns a new DOM document for the given URI. */ public static Document loadDocument(String uri) { try { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory .newInstance(); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); return docBuilder.parse(uri); } catch (Exception e) { e.printStackTrace(); } return null; } /** * Returns a new document for the given XML string. * * @param xml * String that represents the XML data. * @return Returns a new XML document. * @deprecated Use <code>mxXmlUtils.parseXml</code> (Jan 2012) */ public static Document parseXml(String xml) { return mxXmlUtils.parseXml(xml); } /** * Evaluates a Java expression as a class member using mxCodecRegistry. The * range of supported expressions is limited to static class members such as * mxEdgeStyle.ElbowConnector. */ public static Object eval(String expression) { int dot = expression.lastIndexOf("."); if (dot > 0) { Class<?> clazz = mxCodecRegistry.getClassForName(expression .substring(0, dot)); if (clazz != null) { try { return clazz.getField(expression.substring(dot + 1)).get( null); } catch (Exception e) { // ignore } } } return expression; } /** * Returns the first node where attr equals value. This implementation does * not use XPath. */ public static Node findNode(Node node, String attr, String value) { String tmp = (node instanceof Element) ? ((Element) node) .getAttribute(attr) : null; if (tmp != null && tmp.equals(value)) { return node; } node = node.getFirstChild(); while (node != null) { Node result = findNode(node, attr, value); if (result != null) { return result; } node = node.getNextSibling(); } return null; } /** * Returns a single node that matches the given XPath expression. * * @param doc * Document that contains the nodes. * @param expression * XPath expression to be matched. * @return Returns a single node matching the given expression. */ public static Node selectSingleNode(Document doc, String expression) { try { XPath xpath = XPathFactory.newInstance().newXPath(); return (Node) xpath.evaluate(expression, doc, XPathConstants.NODE); } catch (XPathExpressionException e) { // ignore } return null; } /** * Converts the ampersand, quote, prime, less-than and greater-than * characters to their corresponding HTML entities in the given string. */ public static String htmlEntities(String text) { return text.replaceAll("&", "&").replaceAll("\"", """) .replaceAll("'", "′").replaceAll("<", "<") .replaceAll(">", ">"); } /** * Returns a string that represents the given node. * * @param node * Node to return the XML for. * @return Returns an XML string. * @deprecated Use <code>mxXmlUtils.getXml(Node)</code> (Jan 2012) */ public static String getXml(Node node) { return mxXmlUtils.getXml(node); } /** * Returns a pretty-printed XML string for the given node. * * @param node * Node to return the XML for. * @return Returns a formatted XML string. */ public static String getPrettyXml(Node node) { return getPrettyXml(node, " ", ""); } /** * Returns a pretty-printed XML string for the given node. Note that this * string should only be used for humans to read (eg. debug output) but not * for further processing as it does not use built-in mechanisms. * * @param node * Node to return the XML for. * @param tab * String to be used for indentation of inner nodes. * @param indent * Current indentation for the node. * @return Returns a formatted XML string. */ public static String getPrettyXml(Node node, String tab, String indent) { StringBuffer result = new StringBuffer(); if (node != null) { if (node.getNodeType() == Node.TEXT_NODE) { result.append(node.getNodeValue()); } else { result.append(indent + "<" + node.getNodeName()); NamedNodeMap attrs = node.getAttributes(); if (attrs != null) { for (int i = 0; i < attrs.getLength(); i++) { String value = attrs.item(i).getNodeValue(); value = mxUtils.htmlEntities(value); result.append(" " + attrs.item(i).getNodeName() + "=\"" + value + "\""); } } Node tmp = node.getFirstChild(); if (tmp != null) { result.append(">\n"); while (tmp != null) { result.append(getPrettyXml(tmp, tab, indent + tab)); tmp = tmp.getNextSibling(); } result.append(indent + "</" + node.getNodeName() + ">\n"); } else { result.append("/>\n"); } } } return result.toString(); } }