package com.mxgraph.canvas;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Stack;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxUtils;
/**
*
*/
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;
/**
* Reference to the graphics instance for painting.
*/
protected Graphics2D graphics;
/**
* 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;
/**
* Holds the current stroke.
*/
protected transient Stroke currentStroke;
/**
* Holds the current font.
*/
protected transient Font currentFont;
/**
* Holds the current value for the shadow color. This is used to hold the
* input value of a shadow operation. The parsing result of this value is
* cached in the global scope as it should be repeating.
*/
protected transient String currentShadowValue;
/**
* Holds the current parsed shadow color. This holds the result of parsing
* the currentShadowValue, which is an expensive operation.
*/
protected transient Color currentShadowColor;
/**
* Constructs a new graphics export canvas.
*/
public mxGraphicsCanvas2D(Graphics2D g)
{
setGraphics(g);
state.g = g;
}
/**
* Sets the graphics instance.
*/
public void setGraphics(Graphics2D value)
{
graphics = value;
}
/**
* Returns the graphics instance.
*/
public Graphics2D getGraphics()
{
return graphics;
}
/**
* 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();
// TODO: Check if stroke is part of graphics state
currentStroke = state.g.getStroke();
currentFont = state.g.getFont();
}
/**
* 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;
// This implementation uses custom scale/translate and built-in rotation
// Rotation state is part of the AffineTransform in state.transform
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.g.rotate(Math.toRadians(theta), cx, cy);
}
/**
*
*/
public void setStrokeWidth(double value)
{
// Lazy and cached instantiation strategy for all stroke properties
if (value * state.scale != state.strokeWidth)
{
state.strokeWidth = value * state.scale;
// Invalidates cached stroke
currentStroke = null;
}
}
/**
* Caches color conversion as it is expensive.
*/
public void setStrokeColor(String value)
{
// Lazy and cached instantiation strategy for all stroke properties
if (!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;
// Invalidates cached stroke
currentStroke = null;
}
}
/**
*
*/
public void setDashPattern(String value)
{
// FIXME: Initial dash pattern (3, 3) isn't scaled
if (!state.dashPattern.equals(value))
{
float[] dashpattern = null;
if (state.dashed && state.dashPattern != null)
{
String[] tokens = value.split(" ");
dashpattern = new float[tokens.length];
for (int i = 0; i < tokens.length; i++)
{
dashpattern[i] = (float) (Float.parseFloat(tokens[i]) * state.scale);
}
}
state.dashPattern = dashpattern;
currentStroke = null;
}
}
/**
*
*/
public void setLineCap(String value)
{
if (!state.lineCap.equals(value))
{
state.lineCap = value;
currentStroke = null;
}
}
/**
*
*/
public void setLineJoin(String value)
{
if (!state.lineJoin.equals(value))
{
state.lineJoin = value;
currentStroke = null;
}
}
/**
*
*/
public void setMiterLimit(double value)
{
if (value != state.miterLimit)
{
state.miterLimit = value;
currentStroke = null;
}
}
/**
*
*/
public void setFontSize(double value)
{
if (value != state.fontSize)
{
state.fontSize = value * state.scale;
currentFont = null;
}
}
/**
*
*/
public void setFontColor(String value)
{
if (!state.fontColorValue.equals(value))
{
state.fontColorValue = value;
state.fontColor = null;
}
}
/**
*
*/
public void setFontFamily(String value)
{
if (!state.fontFamily.equals(value))
{
state.fontFamily = value;
currentFont = null;
}
}
/**
*
*/
public void setFontStyle(int value)
{
if (value != state.fontStyle)
{
state.fontStyle = value;
currentFont = null;
}
}
/**
*
*/
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.equals(value))
{
state.fillColorValue = value;
state.fillColor = null;
// Setting fill color resets paint color
state.paint = null;
}
}
/**
*
*/
public void setGradient(String color1, String color2, double x, double y,
double w, double h, String direction)
{
// 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);
}
state.paint = new GradientPaint(x1, y1, parseColor(color1), x2, y2,
parseColor(color2), true);
}
/**
* Helper method that uses {@link mxUtils#parseColor(String)}. Subclassers
* can override this to implement caching for frequently used colors.
*/
protected Color parseColor(String hex)
{
return mxUtils.parseColor(hex);
}
/**
*
*/
public void setGlassGradient(double x, double y, double w, double h)
{
double size = 0.4;
x = state.dx + x * state.scale;
y = state.dy + y * state.scale;
h *= state.scale;
w *= state.scale;
state.paint = new GradientPaint((float) x, (float) y, new Color(1, 1,
1, 0.9f), (float) (x), (float) (y + h * size), new Color(1, 1,
1, 0.3f));
}
/**
*
*/
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)
{
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);
end();
}
/**
*
*/
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)
{
mxRectangle bounds = getImageBounds(img, x, y, w, h, aspect);
img = scaleImage(img, (int) bounds.getWidth(),
(int) bounds.getHeight());
if (img != null)
{
createImageGraphics(bounds.getX(), bounds.getY(),
bounds.getWidth(), bounds.getHeight(), flipH, flipV)
.drawImage(img, (int) bounds.getX(),
(int) bounds.getY(), null);
}
}
}
}
/**
* Hook for image caching.
*/
protected Image loadImage(String src)
{
return mxUtils.loadImage(src);
}
/**
*
*/
protected final mxRectangle 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)
{
double iw = img.getWidth(null);
double ih = img.getHeight(null);
double s = Math.min(w / iw, h / ih);
int sw = (int) Math.round(iw * s);
int sh = (int) Math.round(ih * s);
x += (w - sw) / 2;
y += (h - sh) / 2;
w = sw;
h = sh;
}
else
{
w = Math.round(w);
h = Math.round(h);
}
return new mxRectangle(x, y, w, h);
}
/**
* Uses {@link #IMAGE_SCALING} to scale the given image.
*/
protected Image scaleImage(Image img, int w, int h)
{
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();
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;
}
/**
* Draws the given text.
*/
public void text(double x, double y, double w, double h, String str,
String align, String valign, boolean vertical)
{
if (!state.fontColorValue.equals(mxConstants.NONE))
{
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, vertical);
FontMetrics fm = g2.getFontMetrics();
String[] lines = str.split("\n");
y = getVerticalTextPosition(x, y, w, h, align, valign, vertical,
fm, lines);
x = getHorizontalTextPosition(x, y, w, h, align, valign, vertical,
fm, lines);
for (int i = 0; i < lines.length; i++)
{
double dx = 0;
if (align.equals(mxConstants.ALIGN_CENTER))
{
int sw = fm.stringWidth(lines[i]);
dx = (w - sw) / 2;
}
else if (align.equals(mxConstants.ALIGN_RIGHT))
{
int sw = fm.stringWidth(lines[i]);
dx = w - sw;
}
g2.drawString(lines[i], (int) Math.round(x + dx),
(int) Math.round(y));
y += fm.getHeight() + mxConstants.LINESPACING;
}
}
}
/**
* Returns a new graphincs instance with the correct color and font for
* text rendering.
*/
protected final Graphics2D createTextGraphics(double x, double y, double w,
double h, boolean vertical)
{
Graphics2D g2 = state.g;
updateFont();
if (vertical)
{
g2 = (Graphics2D) state.g.create();
g2.rotate(-Math.PI / 2, x + w / 2, y + h / 2);
}
if (state.fontColor == null)
{
state.fontColor = parseColor(state.fontColorValue);
}
g2.setColor(state.fontColor);
return g2;
}
/**
*
*/
protected double getVerticalTextPosition(double x, double y, double w,
double h, String align, String valign, boolean vertical,
FontMetrics fm, String[] lines)
{
double lineHeight = fm.getHeight() + mxConstants.LINESPACING;
double textHeight = lines.length * lineHeight;
double dy = h - textHeight;
if (valign == null || valign.equals(mxConstants.ALIGN_MIDDLE))
{
y = y + dy / 2;
}
else if (valign.equals(mxConstants.ALIGN_TOP))
{
y = Math.max(y, y + dy / 2);
}
else if (valign.equals(mxConstants.ALIGN_BOTTOM))
{
y = Math.min(y, y + dy);
}
return y + fm.getHeight() * 0.75;
}
/**
*
*/
protected double getHorizontalTextPosition(double x, double y, double w,
double h, String align, String valign, boolean vertical,
FontMetrics fm, String[] lines)
{
if (align != null)
{
if (align.equals(mxConstants.ALIGN_LEFT))
{
x += mxConstants.LABEL_INSET * state.scale;
}
else if (align.equals(mxConstants.ALIGN_RIGHT))
{
x -= mxConstants.LABEL_INSET * state.scale;
}
}
return x;
}
/**
*
*/
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));
}
}
/**
*
*/
public void arcTo(double rx, double ry, double xAxisRotation,
boolean largeArc, boolean sweep, double x, double y)
{
if (currentPath != null)
{
rx *= state.scale;
ry *= state.scale;
x = x * state.scale + state.dx;
y = y * state.scale + state.dy;
Point2D currentPoint = currentPath.getCurrentPoint();
double x0 = currentPoint.getX();
double y0 = currentPoint.getY();
// Compute the half distance between the current and the final point
double dx2 = (x0 - x) / 2.0;
double dy2 = (y0 - y) / 2.0;
// Convert angle from degrees to radians
xAxisRotation = Math.toRadians(xAxisRotation % 360.0);
double cosAngle = Math.cos(xAxisRotation);
double sinAngle = Math.sin(xAxisRotation);
//
// Step 1 : Compute (x1, y1)
//
double x1 = (cosAngle * dx2 + sinAngle * dy2);
double y1 = (-sinAngle * dx2 + cosAngle * dy2);
// Ensure radii are large enough
rx = Math.abs(rx);
ry = Math.abs(ry);
double Prx = rx * rx;
double Pry = ry * ry;
double Px1 = x1 * x1;
double Py1 = y1 * y1;
// check that radii are large enough
double radiiCheck = Px1 / Prx + Py1 / Pry;
if (radiiCheck > 1)
{
rx = Math.sqrt(radiiCheck) * rx;
ry = Math.sqrt(radiiCheck) * ry;
Prx = rx * rx;
Pry = ry * ry;
}
//
// Step 2 : Compute (cx1, cy1)
//
double sign = (largeArc == sweep) ? -1 : 1;
double sq = ((Prx * Pry) - (Prx * Py1) - (Pry * Px1))
/ ((Prx * Py1) + (Pry * Px1));
sq = (sq < 0) ? 0 : sq;
double coef = (sign * Math.sqrt(sq));
double cx1 = coef * ((rx * y1) / ry);
double cy1 = coef * -((ry * x1) / rx);
//
// Step 3 : Compute (cx, cy) from (cx1, cy1)
//
double sx2 = (x0 + x) / 2.0;
double sy2 = (y0 + y) / 2.0;
double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);
//
// Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
//
double ux = (x1 - cx1) / rx;
double uy = (y1 - cy1) / ry;
double vx = (-x1 - cx1) / rx;
double vy = (-y1 - cy1) / ry;
double p, n;
// Compute the angle start
n = Math.sqrt((ux * ux) + (uy * uy));
p = ux; // (1 * ux) + (0 * uy)
sign = (uy < 0) ? -1.0 : 1.0;
double angleStart = Math.toDegrees(sign * Math.acos(p / n));
// Compute the angle extent
n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
if (!sweep && angleExtent > 0)
{
angleExtent -= 360f;
}
else if (sweep && angleExtent < 0)
{
angleExtent += 360f;
}
angleExtent %= 360f;
angleStart %= 360f;
Arc2D.Double arc = new Arc2D.Double();
arc.x = cx - rx;
arc.y = cy - ry;
arc.width = rx * 2.0;
arc.height = ry * 2.0;
arc.start = -angleStart;
arc.extent = -angleExtent;
currentPath.append(arc, true);
}
}
/**
* This implementation is typically empty.
*/
public void end()
{
// Do nothing
}
/**
* Closes the current path.
*/
public void close()
{
if (currentPath != null)
{
currentPath.closePath();
}
}
/**
*
*/
public void stroke()
{
if (currentPath != null
&& !state.strokeColorValue.equals(mxConstants.NONE))
{
if (state.strokeColor == null)
{
state.strokeColor = parseColor(state.strokeColorValue);
}
updateStroke();
state.g.setColor(state.strokeColor);
state.g.draw(currentPath);
}
}
/**
*
*/
public void fill()
{
if (currentPath != null
&& (!state.fillColorValue.equals(mxConstants.NONE) || state.paint != null))
{
if (state.paint != null)
{
state.g.setPaint(state.paint);
state.paint = null;
}
else
{
if (state.fillColor == null)
{
state.fillColor = parseColor(state.fillColorValue);
}
state.g.setColor(state.fillColor);
state.g.setPaint(null);
}
state.g.fill(currentPath);
}
}
/**
*
*/
public void fillAndStroke()
{
fill();
stroke();
}
/**
*
*/
public void shadow(String value)
{
if (value != null && currentPath != null)
{
if (currentShadowColor == null || currentShadowValue == null
|| !currentShadowValue.equals(value))
{
currentShadowColor = parseColor(value);
currentShadowValue = value;
}
updateStroke();
state.g.setColor(currentShadowColor);
state.g.fill(currentPath);
state.g.draw(currentPath);
}
}
/**
*
*/
public void clip()
{
if (currentPath != null)
{
state.g.clip(currentPath);
}
}
/**
*
*/
protected void updateFont()
{
// LATER: Make currentFont part of state
if (currentFont == null)
{
int size = (int) Math.round(state.fontSize);
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;
currentFont = createFont(state.fontFamily, style, size);
state.g.setFont(currentFont);
}
}
/**
* Hook for subclassers to implement font caching.
*/
protected Font createFont(String family, int style, int size)
{
return new Font(family, style, size);
}
/**
*
*/
protected void updateStroke()
{
if (currentStroke == null)
{
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;
currentStroke = new BasicStroke((float) state.strokeWidth, cap,
join, miterlimit,
(state.dashed) ? state.dashPattern : null, 0);
state.g.setStroke(currentStroke);
}
}
/**
*
*/
protected class CanvasState implements Cloneable
{
/**
*
*/
protected double alpha = 1;
/**
*
*/
protected double scale = 1;
/**
*
*/
protected double dx = 0;
/**
*
*/
protected double dy = 0;
/**
*
*/
protected double miterLimit = 10;
/**
*
*/
protected int fontStyle = 0;
/**
*
*/
protected double fontSize = mxConstants.DEFAULT_FONTSIZE;
/**
*
*/
protected String fontFamily = mxConstants.DEFAULT_FONTFAMILY;
/**
*
*/
protected String fontColorValue = "#000000";
/**
*
*/
protected Color fontColor;
/**
*
*/
protected String lineCap = "flat";
/**
*
*/
protected String lineJoin = "miter";
/**
*
*/
protected double strokeWidth = 1;
/**
*
*/
protected String strokeColorValue = mxConstants.NONE;
/**
*
*/
protected Color strokeColor;
/**
*
*/
protected String fillColorValue = mxConstants.NONE;
/**
*
*/
protected Color fillColor;
/**
*
*/
protected Paint paint;
/**
*
*/
protected boolean dashed = false;
/**
*
*/
protected float[] dashPattern = { 3, 3 };
/**
* Stores the actual state.
*/
protected transient Graphics2D g;
/**
*
*/
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}
}