package com.mxgraph.canvas;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxLightweightLabel;
import com.mxgraph.util.mxUtils;
import javax.swing.*;
import java.awt.*;
import java.awt.font.TextAttribute;
import java.awt.geom.*;
import java.text.AttributedString;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;
/**
* Used for exporting images. To render to an image from a given XML string,
* graph size and background color, the following code is used:
* <p/>
* <code>
* BufferedImage image = mxUtils.createBufferedImage(width, height, background);
* Graphics2D g2 = image.createGraphics();
* mxUtils.setAntiAlias(g2, true, true);
* XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
* reader.setContentHandler(new mxSaxOutputHandler(new mxGraphicsCanvas2D(g2)));
* reader.parse(new InputSource(new StringReader(xml)));
* </code>
* <p/>
* Text rendering is available for plain text and HTML markup, the latter with optional
* word wrapping. CSS support is limited to the following:
* http://docs.oracle.com/javase/6/docs/api/index.html?javax/swing/text/html/CSS.html
*/
public class mxGraphicsCanvas2D implements mxICanvas2D {
/**
* Specifies the image scaling quality. Default is Image.SCALE_SMOOTH.
* See {@link #scaleImage(Image, int, int)}
*/
public static int IMAGE_SCALING = Image.SCALE_SMOOTH;
/**
* Specifies the size of the cache used to store parsed colors
*/
public static int COLOR_CACHE_SIZE = 100;
/**
* Reference to the graphics instance for painting.
*/
protected Graphics2D graphics;
/**
* Specifies if text output should be rendered. Default is true.
*/
protected boolean textEnabled = true;
/**
* Represents the current state of the canvas.
*/
protected transient CanvasState state = new CanvasState();
/**
* Stack of states for save/restore.
*/
protected transient Stack<CanvasState> stack = new Stack<CanvasState>();
/**
* Holds the current path.
*/
protected transient GeneralPath currentPath;
/**
* Optional renderer pane to be used for HTML label rendering.
*/
protected CellRendererPane rendererPane;
/**
* Font caching.
*/
protected transient Font lastFont = null;
/**
* Font caching.
*/
protected transient int lastFontStyle = 0;
/**
* Font caching.
*/
protected transient int lastFontSize = 0;
/**
* Font caching.
*/
protected transient String lastFontFamily = "";
/**
* Stroke caching.
*/
protected transient Stroke lastStroke = null;
/**
* Stroke caching.
*/
protected transient float lastStrokeWidth = 0;
/**
* Stroke caching.
*/
protected transient int lastCap = 0;
/**
* Stroke caching.
*/
protected transient int lastJoin = 0;
/**
* Stroke caching.
*/
protected transient float lastMiterLimit = 0;
/**
* Stroke caching.
*/
protected transient boolean lastDashed = false;
/**
* Stroke caching.
*/
protected transient Object lastDashPattern = "";
/**
* Caches parsed colors.
*/
@SuppressWarnings("serial")
protected transient LinkedHashMap<String, Color> colorCache = new LinkedHashMap<String, Color>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Color> eldest) {
return size() > COLOR_CACHE_SIZE;
}
};
/**
* Constructs a new graphics export canvas.
*/
public mxGraphicsCanvas2D(Graphics2D g) {
setGraphics(g);
state.g = g;
// Initializes the cell renderer pane for drawing HTML markup
try {
rendererPane = new CellRendererPane();
}
catch (Exception e) {
// ignore
}
}
/**
* Sets the graphics instance.
*/
public void setGraphics(Graphics2D value) {
graphics = value;
}
/**
* Returns the graphics instance.
*/
public Graphics2D getGraphics() {
return graphics;
}
/**
* Returns true if text should be rendered.
*/
public boolean isTextEnabled() {
return textEnabled;
}
/**
* Disables or enables text rendering.
*/
public void setTextEnabled(boolean value) {
textEnabled = value;
}
/**
* Saves the current canvas state.
*/
public void save() {
stack.push(state);
state = cloneState(state);
state.g = (Graphics2D)state.g.create();
}
/**
* Restores the last canvas state.
*/
public void restore() {
state = stack.pop();
}
/**
* Returns a clone of thec given state.
*/
protected CanvasState cloneState(CanvasState state) {
try {
return (CanvasState)state.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
/**
*
*/
public void scale(double value) {
// This implementation uses custom scale/translate and built-in rotation
state.scale = state.scale * value;
}
/**
*
*/
public void translate(double dx, double dy) {
// This implementation uses custom scale/translate and built-in rotation
state.dx += dx;
state.dy += dy;
}
/**
*
*/
public void rotate(double theta, boolean flipH, boolean flipV, double cx, double cy) {
cx += state.dx;
cy += state.dy;
cx *= state.scale;
cy *= state.scale;
state.g.rotate(Math.toRadians(theta), cx, cy);
// This implementation uses custom scale/translate and built-in rotation
// Rotation state is part of the AffineTransform in state.transform
if (flipH && flipV) {
theta += 180;
}
else if (flipH ^ flipV) {
double tx = (flipH) ? cx : 0;
int sx = (flipH) ? -1 : 1;
double ty = (flipV) ? cy : 0;
int sy = (flipV) ? -1 : 1;
state.g.translate(tx, ty);
state.g.scale(sx, sy);
state.g.translate(-tx, -ty);
}
state.theta = theta;
state.rotationCx = cx;
state.rotationCy = cy;
state.flipH = flipH;
state.flipV = flipV;
}
/**
*
*/
public void setStrokeWidth(double value) {
// Lazy and cached instantiation strategy for all stroke properties
if (value != state.strokeWidth) {
state.strokeWidth = value;
}
}
/**
* Caches color conversion as it is expensive.
*/
public void setStrokeColor(String value) {
// Lazy and cached instantiation strategy for all stroke properties
if (state.strokeColorValue == null || !state.strokeColorValue.equals(value)) {
state.strokeColorValue = value;
state.strokeColor = null;
}
}
/**
*
*/
public void setDashed(boolean value) {
// Lazy and cached instantiation strategy for all stroke properties
if (value != state.dashed) {
state.dashed = value;
}
}
/**
*
*/
public void setDashPattern(String value) {
if (value != null && value.length() > 0) {
String[] tokens = value.split(" ");
float[] dashpattern = new float[tokens.length];
for (int i = 0; i < tokens.length; i++) {
dashpattern[i] = (float)(Float.parseFloat(tokens[i]));
}
state.dashPattern = dashpattern;
}
}
/**
*
*/
public void setLineCap(String value) {
if (!state.lineCap.equals(value)) {
state.lineCap = value;
}
}
/**
*
*/
public void setLineJoin(String value) {
if (!state.lineJoin.equals(value)) {
state.lineJoin = value;
}
}
/**
*
*/
public void setMiterLimit(double value) {
if (value != state.miterLimit) {
state.miterLimit = value;
}
}
/**
*
*/
public void setFontSize(double value) {
if (value != state.fontSize) {
state.fontSize = value;
}
}
/**
*
*/
public void setFontColor(String value) {
if (state.fontColorValue == null || !state.fontColorValue.equals(value)) {
state.fontColorValue = value;
state.fontColor = null;
}
}
/**
*
*/
public void setFontBackgroundColor(String value) {
if (state.fontBackgroundColorValue == null || !state.fontBackgroundColorValue.equals(value)) {
state.fontBackgroundColorValue = value;
state.fontBackgroundColor = null;
}
}
/**
*
*/
public void setFontBorderColor(String value) {
if (state.fontBorderColorValue == null || !state.fontBorderColorValue.equals(value)) {
state.fontBorderColorValue = value;
state.fontBorderColor = null;
}
}
/**
*
*/
public void setFontFamily(String value) {
if (!state.fontFamily.equals(value)) {
state.fontFamily = value;
}
}
/**
*
*/
public void setFontStyle(int value) {
if (value != state.fontStyle) {
state.fontStyle = value;
}
}
/**
*
*/
public void setAlpha(double value) {
if (state.alpha != value) {
state.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)(value)));
state.alpha = value;
}
}
/**
*
*/
public void setFillColor(String value) {
if (state.fillColorValue == null || !state.fillColorValue.equals(value)) {
state.fillColorValue = value;
state.fillColor = null;
// Setting fill color resets gradient paint
state.gradientPaint = null;
}
}
/**
*
*/
public void setGradient(String color1,
String color2,
double x,
double y,
double w,
double h,
String direction,
double alpha1,
double alpha2) {
// LATER: Add lazy instantiation and check if paint already created
float x1 = (float)((state.dx + x) * state.scale);
float y1 = (float)((state.dy + y) * state.scale);
float x2 = (float)x1;
float y2 = (float)y1;
h *= state.scale;
w *= state.scale;
if (direction == null || direction.length() == 0 || direction.equals(mxConstants.DIRECTION_SOUTH)) {
y2 = (float)(y1 + h);
}
else if (direction.equals(mxConstants.DIRECTION_EAST)) {
x2 = (float)(x1 + w);
}
else if (direction.equals(mxConstants.DIRECTION_NORTH)) {
y1 = (float)(y1 + h);
}
else if (direction.equals(mxConstants.DIRECTION_WEST)) {
x1 = (float)(x1 + w);
}
Color c1 = parseColor(color1);
if (alpha1 != 1) {
c1 = new Color(c1.getRed(), c1.getGreen(), c1.getBlue(), (int)(alpha1 * 255));
}
Color c2 = parseColor(color2);
if (alpha2 != 1) {
c2 = new Color(c2.getRed(), c2.getGreen(), c2.getBlue(), (int)(alpha2 * 255));
}
state.gradientPaint = new GradientPaint(x1, y1, c1, x2, y2, c2, true);
// Resets fill color
state.fillColorValue = null;
}
/**
* Helper method that uses {@link mxUtils#parseColor(String)}.
*/
protected Color parseColor(String hex) {
Color result = colorCache.get(hex);
if (result == null) {
result = mxUtils.parseColor(hex);
colorCache.put(hex, result);
}
return result;
}
/**
*
*/
public void rect(double x, double y, double w, double h) {
currentPath = new GeneralPath();
currentPath
.append(new Rectangle2D.Double((state.dx + x) * state.scale, (state.dy + y) * state.scale, w * state.scale, h * state.scale), false);
}
/**
* Implements a rounded rectangle using a path.
*/
public void roundrect(double x, double y, double w, double h, double dx, double dy) {
// LATER: Use arc here or quad in VML/SVG for exact match
begin();
moveTo(x + dx, y);
lineTo(x + w - dx, y);
quadTo(x + w, y, x + w, y + dy);
lineTo(x + w, y + h - dy);
quadTo(x + w, y + h, x + w - dx, y + h);
lineTo(x + dx, y + h);
quadTo(x, y + h, x, y + h - dy);
lineTo(x, y + dy);
quadTo(x, y, x + dx, y);
}
/**
*
*/
public void ellipse(double x, double y, double w, double h) {
currentPath = new GeneralPath();
currentPath
.append(new Ellipse2D.Double((state.dx + x) * state.scale, (state.dy + y) * state.scale, w * state.scale, h * state.scale), false);
}
/**
*
*/
public void image(double x, double y, double w, double h, String src, boolean aspect, boolean flipH, boolean flipV) {
if (src != null && w > 0 && h > 0) {
Image img = loadImage(src);
if (img != null) {
Rectangle bounds = getImageBounds(img, x, y, w, h, aspect);
img = scaleImage(img, bounds.width, bounds.height);
if (img != null) {
drawImage(createImageGraphics(bounds.x, bounds.y, bounds.width, bounds.height, flipH, flipV), img, bounds.x, bounds.y);
}
}
}
}
/**
*
*/
protected void drawImage(Graphics2D graphics, Image image, int x, int y) {
graphics.drawImage(image, x, y, null);
}
/**
* Hook for image caching.
*/
protected Image loadImage(String src) {
return mxUtils.loadImage(src);
}
/**
*
*/
protected final Rectangle getImageBounds(Image img, double x, double y, double w, double h, boolean aspect) {
x = (state.dx + x) * state.scale;
y = (state.dy + y) * state.scale;
w *= state.scale;
h *= state.scale;
if (aspect) {
Dimension size = getImageSize(img);
double s = Math.min(w / size.width, h / size.height);
int sw = (int)Math.round(size.width * s);
int sh = (int)Math.round(size.height * s);
x += (w - sw) / 2;
y += (h - sh) / 2;
w = sw;
h = sh;
}
else {
w = Math.round(w);
h = Math.round(h);
}
return new Rectangle((int)x, (int)y, (int)w, (int)h);
}
/**
* Returns the size for the given image.
*/
protected Dimension getImageSize(Image image) {
return new Dimension(image.getWidth(null), image.getHeight(null));
}
/**
* Uses {@link #IMAGE_SCALING} to scale the given image.
*/
protected Image scaleImage(Image img, int w, int h) {
Dimension size = getImageSize(img);
if (w == size.width && h == size.height) {
return img;
}
else {
return img.getScaledInstance(w, h, IMAGE_SCALING);
}
}
/**
* Creates a graphic instance for rendering an image.
*/
protected final Graphics2D createImageGraphics(double x, double y, double w, double h, boolean flipH, boolean flipV) {
Graphics2D g2 = state.g;
if (flipH || flipV) {
g2 = (Graphics2D)g2.create();
if (flipV && flipH) {
g2.rotate(Math.toRadians(180), x + w / 2, y + h / 2);
}
else {
int sx = 1;
int sy = 1;
int dx = 0;
int dy = 0;
if (flipH) {
sx = -1;
dx = (int)(-w - 2 * x);
}
if (flipV) {
sy = -1;
dy = (int)(-h - 2 * y);
}
g2.scale(sx, sy);
g2.translate(dx, dy);
}
}
return g2;
}
/**
* Creates a HTML document around the given markup.
*/
protected String createHtmlDocument(String text, String align, String valign, int w, int h, boolean wrap, String overflow, boolean clip) {
StringBuffer css = new StringBuffer();
css.append("display:inline;");
css.append("font-family:" + state.fontFamily + ";");
css.append("font-size:" + Math.round(state.fontSize) + " pt;");
css.append("color:" + state.fontColorValue + ";");
// KNOWN: Line-height ignored in JLabel
css.append("line-height:" + Math.round(state.fontSize * mxConstants.LINE_HEIGHT) + " px;");
boolean setWidth = false;
if ((state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) {
css.append("font-weight:bold;");
}
if ((state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) {
css.append("font-style:italic;");
}
if ((state.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) {
css.append("text-decoration:underline;");
}
if (align != null) {
if (align.equals(mxConstants.ALIGN_CENTER)) {
css.append("text-align:center;");
}
else if (align.equals(mxConstants.ALIGN_RIGHT)) {
css.append("text-align:right;");
}
}
if (state.fontBackgroundColorValue != null) {
css.append("background-color:" + state.fontBackgroundColorValue + ";");
}
// KNOWN: Border ignored in JLabel
if (state.fontBorderColorValue != null) {
css.append("border:1px solid " + state.fontBorderColorValue + ";");
}
// KNOWN: max-width/-height ignored in JLabel
if (clip) {
css.append("overflow:hidden;");
setWidth = true;
}
else if (overflow != null) {
if (overflow.equals("fill")) {
css.append("height:" + Math.round(h) + "px;");
setWidth = true;
}
else if (overflow.equals("width")) {
setWidth = true;
if (h > 0) {
css.append("height:" + Math.round(h) + "px;");
}
}
}
if (wrap) {
if (!clip) {
// NOTE: Max-width not available in Java
setWidth = true;
}
css.append("white-space:normal;");
}
else {
css.append("white-space:nowrap;");
}
if (setWidth && w > 0) {
css.append("width:" + Math.round(w) + "px;");
}
return "<html><div style=\"" + css.toString() + "\">" + text + "</div></html>";
}
/**
* Hook to return the renderer for HTML formatted text. This implementation returns
* the shared instance of mxLighweightLabel.
*/
protected JLabel getTextRenderer() {
return mxLightweightLabel.getSharedInstance();
}
/**
*
*/
protected Point2D getMargin(String align, String valign) {
double dx = 0;
double dy = 0;
if (align != null) {
if (align.equals(mxConstants.ALIGN_CENTER)) {
dx = -0.5;
}
else if (align.equals(mxConstants.ALIGN_RIGHT)) {
dx = -1;
}
}
if (valign != null) {
if (valign.equals(mxConstants.ALIGN_MIDDLE)) {
dy = -0.5;
}
else if (valign.equals(mxConstants.ALIGN_BOTTOM)) {
dy = -1;
}
}
return new Point2D.Double(dx, dy);
}
/**
* Draws the given HTML text.
*/
protected void htmlText(double x,
double y,
double w,
double h,
String str,
String align,
String valign,
boolean wrap,
String format,
String overflow,
boolean clip,
double rotation) {
x += state.dx;
y += state.dy;
JLabel textRenderer = getTextRenderer();
if (textRenderer != null && rendererPane != null) {
// Use native scaling for HTML
AffineTransform previous = state.g.getTransform();
state.g.scale(state.scale, state.scale);
double rad = rotation * (Math.PI / 180);
state.g.rotate(rad, x, y);
// Renders the scaled text with a correction factor of
// PX_PER_PIXEL for px in HTML vs pixels in the bitmap
boolean widthFill = false;
boolean fill = false;
String original = str;
if (overflow != null) {
widthFill = overflow.equals("width");
fill = overflow.equals("fill");
}
str = createHtmlDocument(str, align, valign, (widthFill || fill) ? (int)Math.round(w * mxConstants.PX_PER_PIXEL) : 0,
(fill) ? (int)Math.round(h * mxConstants.PX_PER_PIXEL) : 0, wrap, overflow, clip);
textRenderer.setText(str);
Dimension pref = textRenderer.getPreferredSize();
int prefWidth = pref.width;
int prefHeight = pref.height;
// Poor man's max-width
if (((clip || wrap) && prefWidth > w && w > 0) || (clip && prefHeight > h && h > 0)) {
// +2 is workaround for inconsistent word wrapping in Java
int cw = (int)Math.round(w * mxConstants.PX_PER_PIXEL + ((wrap) ? 2 : 0));
int ch = (int)Math.round(h * mxConstants.PX_PER_PIXEL);
str = createHtmlDocument(original, align, valign, cw, ch, wrap, overflow, clip);
textRenderer.setText(str);
pref = textRenderer.getPreferredSize();
prefWidth = pref.width;
prefHeight = pref.height + 2;
}
// Matches HTML output
if (clip && w > 0 && h > 0) {
prefWidth = Math.min(pref.width, (int)w);
prefHeight = Math.min(prefHeight, (int)h);
h = prefHeight;
}
else if (!clip && wrap && w > 0 && h > 0) {
prefWidth = Math.min(pref.width, (int)w);
w = prefWidth;
h = prefHeight;
prefHeight = Math.max(prefHeight, (int)h);
}
else if (!clip && !wrap) {
if (w > 0 && w < prefWidth) {
w = prefWidth;
}
if (h > 0 && h < prefHeight) {
h = prefHeight;
}
}
Point2D margin = getMargin(align, valign);
x += margin.getX() * prefWidth;
y += margin.getY() * prefHeight;
if (w == 0) {
w = prefWidth;
}
if (h == 0) {
h = prefHeight;
}
rendererPane
.paintComponent(state.g, textRenderer, rendererPane, (int)Math.round(x), (int)Math.round(y), (int)Math.round(w), (int)Math.round(h),
true);
state.g.setTransform(previous);
}
}
/**
* Draws the given text.
*/
public void text(double x,
double y,
double w,
double h,
String str,
String align,
String valign,
boolean wrap,
String format,
String overflow,
boolean clip,
double rotation) {
if (format != null && format.equals("html")) {
htmlText(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation);
}
else {
plainText(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation);
}
}
/**
* Draws the given text.
*/
public void plainText(double x,
double y,
double w,
double h,
String str,
String align,
String valign,
boolean wrap,
String format,
String overflow,
boolean clip,
double rotation) {
if (state.fontColor == null) {
state.fontColor = parseColor(state.fontColorValue);
}
if (state.fontColor != null) {
x = (state.dx + x) * state.scale;
y = (state.dy + y) * state.scale;
w *= state.scale;
h *= state.scale;
// Font-metrics needed below this line
Graphics2D g2 = createTextGraphics(x, y, w, h, rotation, clip, align, valign);
FontMetrics fm = g2.getFontMetrics();
String[] lines = str.split("\n");
int[] stringWidths = new int[lines.length];
int textWidth = 0;
for (int i = 0; i < lines.length; i++) {
stringWidths[i] = fm.stringWidth(lines[i]);
textWidth = Math.max(textWidth, stringWidths[i]);
}
int textHeight = (int)Math.round(lines.length * (fm.getFont().getSize() * mxConstants.LINE_HEIGHT));
if (clip && textHeight > h && h > 0) {
textHeight = (int)h;
}
Point2D margin = getMargin(align, valign);
x += margin.getX() * textWidth;
y += margin.getY() * textHeight;
if (state.fontBackgroundColorValue != null) {
if (state.fontBackgroundColor == null) {
state.fontBackgroundColor = parseColor(state.fontBackgroundColorValue);
}
if (state.fontBackgroundColor != null) {
g2.setColor(state.fontBackgroundColor);
g2.fillRect((int)Math.round(x), (int)Math.round(y - 1), textWidth + 1, textHeight + 2);
}
}
if (state.fontBorderColorValue != null) {
if (state.fontBorderColor == null) {
state.fontBorderColor = parseColor(state.fontBorderColorValue);
}
if (state.fontBorderColor != null) {
g2.setColor(state.fontBorderColor);
g2.drawRect((int)Math.round(x), (int)Math.round(y - 1), textWidth + 1, textHeight + 2);
}
}
g2.setColor(state.fontColor);
y += fm.getHeight() - fm.getDescent() - (margin.getY() + 0.5);
for (int i = 0; i < lines.length; i++) {
double dx = 0;
if (align != null) {
if (align.equals(mxConstants.ALIGN_CENTER)) {
dx = (textWidth - stringWidths[i]) / 2;
}
else if (align.equals(mxConstants.ALIGN_RIGHT)) {
dx = textWidth - stringWidths[i];
}
}
// Adds support for underlined text via attributed character iterator
if (!lines[i].isEmpty()) {
if ((state.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE) {
AttributedString as = new AttributedString(lines[i]);
as.addAttribute(TextAttribute.FONT, g2.getFont());
as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
g2.drawString(as.getIterator(), (int)Math.round(x + dx), (int)Math.round(y));
}
else {
g2.drawString(lines[i], (int)Math.round(x + dx), (int)Math.round(y));
}
}
y += (int)Math.round(fm.getFont().getSize() * mxConstants.LINE_HEIGHT);
}
}
}
/**
* Returns a new graphics instance with the correct color and font for
* text rendering.
*/
protected final Graphics2D createTextGraphics(double x,
double y,
double w,
double h,
double rotation,
boolean clip,
String align,
String valign) {
Graphics2D g2 = state.g;
updateFont();
if (rotation != 0) {
g2 = (Graphics2D)state.g.create();
double rad = rotation * (Math.PI / 180);
g2.rotate(rad, x, y);
}
if (clip && w > 0 && h > 0) {
if (g2 == state.g) {
g2 = (Graphics2D)state.g.create();
}
Point2D margin = getMargin(align, valign);
x += margin.getX() * w;
y += margin.getY() * h;
g2.clip(new Rectangle2D.Double(x, y, w, h));
}
return g2;
}
/**
*
*/
public void begin() {
currentPath = new GeneralPath();
}
/**
*
*/
public void moveTo(double x, double y) {
if (currentPath != null) {
currentPath.moveTo((float)((state.dx + x) * state.scale), (float)((state.dy + y) * state.scale));
}
}
/**
*
*/
public void lineTo(double x, double y) {
if (currentPath != null) {
currentPath.lineTo((float)((state.dx + x) * state.scale), (float)((state.dy + y) * state.scale));
}
}
/**
*
*/
public void quadTo(double x1, double y1, double x2, double y2) {
if (currentPath != null) {
currentPath
.quadTo((float)((state.dx + x1) * state.scale), (float)((state.dy + y1) * state.scale), (float)((state.dx + x2) * state.scale),
(float)((state.dy + y2) * state.scale));
}
}
/**
*
*/
public void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) {
if (currentPath != null) {
currentPath
.curveTo((float)((state.dx + x1) * state.scale), (float)((state.dy + y1) * state.scale), (float)((state.dx + x2) * state.scale),
(float)((state.dy + y2) * state.scale), (float)((state.dx + x3) * state.scale), (float)((state.dy + y3) * state.scale));
}
}
/**
* Closes the current path.
*/
public void close() {
if (currentPath != null) {
currentPath.closePath();
}
}
/**
*
*/
public void stroke() {
paintCurrentPath(false, true);
}
/**
*
*/
public void fill() {
paintCurrentPath(true, false);
}
/**
*
*/
public void fillAndStroke() {
paintCurrentPath(true, true);
}
/**
*
*/
protected void paintCurrentPath(boolean filled, boolean stroked) {
if (currentPath != null) {
if (stroked) {
if (state.strokeColor == null) {
state.strokeColor = parseColor(state.strokeColorValue);
}
if (state.strokeColor != null) {
updateStroke();
}
}
if (filled) {
if (state.gradientPaint == null && state.fillColor == null) {
state.fillColor = parseColor(state.fillColorValue);
}
}
if (state.shadow) {
paintShadow(filled, stroked);
}
if (filled) {
if (state.gradientPaint != null) {
state.g.setPaint(state.gradientPaint);
state.g.fill(currentPath);
}
else {
if (state.fillColor == null) {
state.fillColor = parseColor(state.fillColorValue);
}
if (state.fillColor != null) {
state.g.setColor(state.fillColor);
state.g.setPaint(null);
state.g.fill(currentPath);
}
}
}
if (stroked && state.strokeColor != null) {
state.g.setColor(state.strokeColor);
state.g.draw(currentPath);
}
}
}
/**
*
*/
protected void paintShadow(boolean filled, boolean stroked) {
if (state.shadowColor == null) {
state.shadowColor = parseColor(state.shadowColorValue);
}
if (state.shadowColor != null) {
double rad = -state.theta * (Math.PI / 180);
double cos = Math.cos(rad);
double sin = Math.sin(rad);
double dx = state.shadowOffsetX * state.scale;
double dy = state.shadowOffsetY * state.scale;
if (state.flipH) {
dx *= -1;
}
if (state.flipV) {
dy *= -1;
}
double tx = dx * cos - dy * sin;
double ty = dx * sin + dy * cos;
state.g.setColor(state.shadowColor);
state.g.translate(tx, ty);
double alpha = state.alpha * state.shadowAlpha;
Composite comp = state.g.getComposite();
state.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)(alpha)));
if (filled && (state.gradientPaint != null || state.fillColor != null)) {
state.g.fill(currentPath);
}
// FIXME: Overlaps with fill in composide mode
if (stroked && state.strokeColor != null) {
state.g.draw(currentPath);
}
state.g.translate(-tx, -ty);
state.g.setComposite(comp);
}
}
/**
*
*/
public void setShadow(boolean value) {
state.shadow = value;
}
/**
*
*/
public void setShadowColor(String value) {
state.shadowColorValue = value;
}
/**
*
*/
public void setShadowAlpha(double value) {
state.shadowAlpha = value;
}
/**
*
*/
public void setShadowOffset(double dx, double dy) {
state.shadowOffsetX = dx;
state.shadowOffsetY = dy;
}
/**
*
*/
protected void updateFont() {
int size = (int)Math.round(state.fontSize * state.scale);
int style = ((state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) ? Font.BOLD : Font.PLAIN;
style += ((state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) ? Font.ITALIC : Font.PLAIN;
if (lastFont == null || !lastFontFamily.equals(state.fontFamily) || size != lastFontSize || style != lastFontStyle) {
lastFont = createFont(state.fontFamily, style, size);
lastFontFamily = state.fontFamily;
lastFontStyle = style;
lastFontSize = size;
}
state.g.setFont(lastFont);
}
/**
* Hook for subclassers to implement font caching.
*/
protected Font createFont(String family, int style, int size) {
return new Font(getFontName(family), style, size);
}
/**
* Returns a font name for the given CSS values for font-family.
* This implementation returns the first entry for comma-separated
* lists of entries.
*/
protected String getFontName(String family) {
if (family != null) {
int comma = family.indexOf(',');
if (comma >= 0) {
family = family.substring(0, comma);
}
}
return family;
}
/**
*
*/
protected void updateStroke() {
float sw = (float)Math.max(1, state.strokeWidth * state.scale);
int cap = BasicStroke.CAP_BUTT;
if (state.lineCap.equals("round")) {
cap = BasicStroke.CAP_ROUND;
}
else if (state.lineCap.equals("square")) {
cap = BasicStroke.CAP_SQUARE;
}
int join = BasicStroke.JOIN_MITER;
if (state.lineJoin.equals("round")) {
join = BasicStroke.JOIN_ROUND;
}
else if (state.lineJoin.equals("bevel")) {
join = BasicStroke.JOIN_BEVEL;
}
float miterlimit = (float)state.miterLimit;
if (lastStroke == null ||
lastStrokeWidth != sw ||
lastCap != cap ||
lastJoin != join ||
lastMiterLimit != miterlimit ||
lastDashed != state.dashed ||
(state.dashed && lastDashPattern != state.dashPattern)) {
float[] dash = null;
if (state.dashed) {
dash = new float[state.dashPattern.length];
for (int i = 0; i < dash.length; i++) {
dash[i] = (float)(state.dashPattern[i] * sw);
}
}
lastStroke = new BasicStroke(sw, cap, join, miterlimit, dash, 0);
lastStrokeWidth = sw;
lastCap = cap;
lastJoin = join;
lastMiterLimit = miterlimit;
lastDashed = state.dashed;
lastDashPattern = state.dashPattern;
}
state.g.setStroke(lastStroke);
}
/**
*
*/
protected class CanvasState implements Cloneable {
/**
*
*/
protected double alpha = 1;
/**
*
*/
protected double scale = 1;
/**
*
*/
protected double dx = 0;
/**
*
*/
protected double dy = 0;
/**
*
*/
protected double theta = 0;
/**
*
*/
protected double rotationCx = 0;
/**
*
*/
protected double rotationCy = 0;
/**
*
*/
protected boolean flipV = false;
/**
*
*/
protected boolean flipH = false;
/**
*
*/
protected double miterLimit = 10;
/**
*
*/
protected int fontStyle = 0;
/**
*
*/
protected double fontSize = mxConstants.DEFAULT_FONTSIZE;
/**
*
*/
protected String fontFamily = mxConstants.DEFAULT_FONTFAMILIES;
/**
*
*/
protected String fontColorValue = "#000000";
/**
*
*/
protected Color fontColor;
/**
*
*/
protected String fontBackgroundColorValue;
/**
*
*/
protected Color fontBackgroundColor;
/**
*
*/
protected String fontBorderColorValue;
/**
*
*/
protected Color fontBorderColor;
/**
*
*/
protected String lineCap = "flat";
/**
*
*/
protected String lineJoin = "miter";
/**
*
*/
protected double strokeWidth = 1;
/**
*
*/
protected String strokeColorValue;
/**
*
*/
protected Color strokeColor;
/**
*
*/
protected String fillColorValue;
/**
*
*/
protected Color fillColor;
/**
*
*/
protected Paint gradientPaint;
/**
*
*/
protected boolean dashed = false;
/**
*
*/
protected float[] dashPattern = {3, 3};
/**
*
*/
protected boolean shadow = false;
/**
*
*/
protected String shadowColorValue = mxConstants.W3C_SHADOWCOLOR;
/**
*
*/
protected Color shadowColor;
/**
*
*/
protected double shadowAlpha = 1;
/**
*
*/
protected double shadowOffsetX = mxConstants.SHADOW_OFFSETX;
/**
*
*/
protected double shadowOffsetY = mxConstants.SHADOW_OFFSETY;
/**
* Stores the actual state.
*/
protected transient Graphics2D g;
/**
*
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}