package graphics;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import javax.imageio.ImageIO;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.svggen.DefaultExtensionHandler;
import org.apache.batik.svggen.DefaultImageHandler;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import com.jhlabs.image.GaussianGlowFilter;
public class GraphUtils {
public static enum TextAnchor {
BASELINE, CENTER;
}
private static interface Graphics2DHelper {
Graphics2D getGraphics2D();
BufferedImage getImage() throws Exception;
}
private static class DefaultGraphics2DHelper implements Graphics2DHelper {
private BufferedImage img;
public DefaultGraphics2DHelper(int width, int height) {
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
@Override
public Graphics2D getGraphics2D() {
return img.createGraphics();
}
@Override
public BufferedImage getImage() {
return img;
}
}
private static class SvgGraphics2DHelper implements Graphics2DHelper {
private SVGGraphics2D g2;
private int width;
private int height;
public SvgGraphics2DHelper(int width, int height) {
this.width = width;
this.height = height;
DOMImplementation impl = SVGDOMImplementation.getDOMImplementation();
String svgNS = SVGDOMImplementation.SVG_NAMESPACE_URI;
Document doc = impl.createDocument(svgNS, "svg", null);
g2 = new SVGGraphics2D(doc, new DefaultImageHandler(), new DefaultExtensionHandler(), true);
}
@Override
public Graphics2D getGraphics2D() {
return g2;
}
@Override
public BufferedImage getImage() throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
Writer out = new OutputStreamWriter(bytes, "UTF-8");
g2.stream(out, true);
PNGTranscoder t = new PNGTranscoder();
TranscoderInput input = new TranscoderInput(new ByteArrayInputStream(bytes.toByteArray()));
ByteArrayOutputStream ostream = new ByteArrayOutputStream();
TranscoderOutput output = new TranscoderOutput(ostream);
t.transcode(input, output);
BufferedImage img = ImageIO.read(new ByteArrayInputStream(ostream.toByteArray()));
img = img.getSubimage(0, 0, width, height);
return img;
}
}
private Graphics2D g;
public GraphUtils(Graphics2D g) {
this.g = g;
}
public void drawCircle(int x, int y, float radius) {
Rectangle r = getBoundingBoxCircle(x, y, radius);
g.drawOval(r.x, r.y, r.width, r.height);
}
public void fillCircle(Point2D center, float radius) {
fillCircle((int)center.getX(), (int)center.getY(), radius);
}
public void fillCircle(double x, double y, double radius) {
Rectangle2D r = getBoundingBoxCircle(x,y,radius);
g.fill(new Ellipse2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight()));
}
public void fillCircle(int x, int y, float radius) {
Rectangle r = getBoundingBoxCircle(x, y, radius);
g.fillOval(r.x, r.y, r.width, r.height);
}
public void fillArc(int x, int y, float radius, int startAngle, int arcAngle) {
Rectangle r = getBoundingBoxCircle(x, y, radius);
g.fillArc(r.x, r.y, r.width, r.height, startAngle, arcAngle);
}
public void drawArc(int x, int y, float radius, int startAngle, int arcAngle) {
this.drawArc(x,y,radius, startAngle, arcAngle, Arc2D.PIE);
}
public void drawArc(int x, int y, float radius, int startAngle, int arcAngle, int type) {
Rectangle r = getBoundingBoxCircle(x, y, radius);
Arc2D.Double arc = new Arc2D.Double(r.x, r.y, r.width, r.height, startAngle, arcAngle, type);
g.draw(arc);
}
public Rectangle getBoundingBoxCircle(int x, int y, float radius) {
Rectangle r = new Rectangle();
r.x = x - (int)radius;
r.y = y - (int)radius;
int diameter = (int)(2 * radius);
r.height = diameter;
r.width = diameter;
return r;
}
public Rectangle2D getBoundingBoxCircle(double x, double y, double radius) {
double rx = x-radius;
double ry = y-radius;
double w = 2*radius;
double h = 2*radius;
return new Rectangle2D.Double(rx, ry, w, h);
}
public static double toRadians(double degrees) {
return degrees * Math.PI / 180;
}
public static double toDegrees(double radians) {
return radians * 180 / Math.PI;
}
// copied from http://www.java.happycodings.com/Java2D/code11.html and modified to also
// support:
// - counter clockwise text rendering (in this case the text is turned upside down)
// - text alignment is centre
public void drawCircleText(String st, int x, int y, double radius, double a1,
boolean clockwise, TextAnchor anchor)
{
double theta = a1;
// align centre
if(clockwise) {
theta -= getCircleTextLengthInRadians(st, radius) / 2;
} else {
theta += getCircleTextLengthInRadians(st, radius) / 2;
}
char ch[] = st.toCharArray();
Font font = g.getFont();
FontRenderContext frc = g.getFontRenderContext();
FontMetrics fm = g.getFontMetrics();
for(int i = 0; i < ch.length; i++) {
GlyphVector gv = font.createGlyphVector(frc, Character.toString(ch[i]));
Shape glyph = gv.getGlyphOutline(0);
double posY = -radius;
if(anchor.equals(TextAnchor.CENTER)) {
posY+=getCapHeight()/2;
}
AffineTransform transform = new AffineTransform();
transform.concatenate(AffineTransform.getTranslateInstance(x, y));
transform.concatenate(AffineTransform.getRotateInstance(theta, 0.0, 0.0));
transform.concatenate(AffineTransform.getTranslateInstance(-getCharWidth(ch[i],fm)/2.0, posY));
// turn glyph by 180 degree and keep same baseline
if(!clockwise) {
transform.concatenate(AffineTransform.getRotateInstance(toRadians(180), getCharWidth(ch[i],fm)/2.0, -getCapHeight()/2));
}
glyph = transform.createTransformedShape(glyph);
g.fill(glyph);
if (i < (ch.length - 1)) {
double adv = getCharWidth(ch[i],fm)/2.0 + fm.getLeading() + getCharWidth(ch[i + 1],fm)/2.0;
if(clockwise) {
theta += Math.sin(adv / radius);
} else {
theta -= Math.sin(adv / radius);
}
}
}
}
private static int getCharWidth(char c, FontMetrics fm) {
if (c == ' ' || Character.isSpaceChar(c)) {
return fm.charWidth(' ');
// return fm.charWidth('n');
}
else {
return fm.charWidth(c);
}
}
public double getCapHeight() {
Font font = g.getFont();
FontRenderContext frc = g.getFontRenderContext();
GlyphVector gv = font.createGlyphVector(frc, "A");
Shape glyph = gv.getGlyphOutline(0);
Rectangle2D rec = glyph.getBounds2D();
return rec.getHeight();
}
public double getTextHeight(Font font, String text) {
FontRenderContext frc = g.getFontRenderContext();
GlyphVector gv = font.createGlyphVector(frc, text);
Shape glyph = gv.getGlyphOutline(0);
Rectangle2D rec = glyph.getBounds2D();
return rec.getHeight();
}
private double getCircleTextLengthInRadians(String st, double radius) {
double theta = 0;
char ch[] = st.toCharArray();
FontMetrics fm = g.getFontMetrics();
for(int i = 0; i < ch.length; i++) {
if (i < (ch.length - 1)) {
double adv = getCharWidth(ch[i],fm)/2.0 + fm.getLeading() + getCharWidth(ch[i + 1],fm)/2.0;
theta += Math.sin(adv / radius);
}
}
return theta;
}
public Graphics2D getGraphics() {
return g;
}
public void drawImage(Image image, int x, int y) {
g.drawImage(image, x-image.getWidth(null)/2, y-image.getHeight(null)/2, null);
}
public void setStroke(float width) {
g.setStroke(new BasicStroke(width));
}
public void setColor(Color color) {
g.setColor(color);
}
public void drawGlowString(String label, Color glowColor, float glowRadius, int x, int y) {
try {
Font font = g.getFont();
FontMetrics fm = g.getFontMetrics();
Rectangle2D bounds = getBounds(label);
Graphics2DHelper helper = newGraphics2D(g, (int)(bounds.getWidth()+4*glowRadius), fm.getHeight()+(int)(4*glowRadius));
Graphics2D g1 = helper.getGraphics2D();
g1.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g1.setColor(glowColor);
g1.setFont(font);
int xpad = (int)(2*glowRadius);
int ypad = fm.getAscent()+(int)(2*glowRadius);
g1.drawString(label, xpad, ypad);
BufferedImage img = helper.getImage();
new GaussianGlowFilter(glowRadius, Color.white).filter(img, img);
// align center right, should be passed in as a parameter really
x -= img.getWidth();
y -= img.getHeight()/2;
AffineTransformOp noopTransform = new AffineTransformOp(new AffineTransform(),
AffineTransformOp.TYPE_BILINEAR);
g.drawImage(img, noopTransform, x, y);
g.drawString(label, (int)x+xpad, (int)y+ypad);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public Rectangle2D getBounds(String s) {
Font font = g.getFont();
FontRenderContext frc = g.getFontRenderContext();
GlyphVector gv = font.createGlyphVector(frc, s);
return gv.getVisualBounds();
}
private Graphics2DHelper newGraphics2D(Graphics2D src, int width, int height) {
if(src instanceof SVGGraphics2D) {
return new SvgGraphics2DHelper(width, height);
} else {
return new DefaultGraphics2DHelper(width, height);
}
}
public static BufferedImage getImage(String name) throws IOException {
InputStream in = GraphUtils.class.getClassLoader().getResourceAsStream(name);
if(in == null) {
throw new IOException(String.format("Resource '%s' not found", name));
}
return ImageIO.read(in);
}
/**
* @deprecated use {@link HatchedRectangle} instead because it scales in svg.
*/
@Deprecated
public static BufferedImage createStripedTexture(Color color) {
return createHatchingTexture(color, 5, 5);
}
/**
* @deprecated use {@link HatchedRectangle} instead because it scales in svg.
*/
@Deprecated
public static BufferedImage createHatchingTexture(Paint paint, int dash1, int dash2) {
int size = dash1+dash2;
BufferedImage result = new BufferedImage(size,size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D)result.getGraphics();
g2.setPaint(paint);
for(int row =0;row<result.getHeight();row++) {
g2.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f,
new float[] {dash1,dash2}, row));
g2.drawLine(0, row, size, row);
}
return result;
}
/**
* Cubic bezier approximation of a circular arc centered at the origin,
* from (radians) a1 to a2, where a2-a1 < pi/2. The arc's radius is r.
*
* Returns an array of four points, where x1,y1 and x4,y4 are the arc's end points
* and x2,y2 and x3,y3 are the cubic bezier's control points.
*
* This algorithm is based on the approach described in:
* A. Riškus, "Approximation of a Cubic Bezier Curve by Circular Arcs and Vice Versa,"
* Information Technology and Control, 35(4), 2006 pp. 371-378.
*
* Copied from PathArcUtils.as
* http://hansmuller-flex.blogspot.com.au/2011/10/more-about-approximating-circular-arcs.html
* and translated into java
*
* This work is licensed under the Creative Commons Attribution 3.0
* Unported License. To view a copy of this license, visit
* http://creativecommons.org/licenses/by/3.0/ or send a letter to
* Creative Commons, 444 Castro Street, Suite 900, Mountain View,
* California, 94041, USA.
*/
// also read: http://www.whizkidtech.redprince.net/bezier/circle/
public static Point2D.Double[] createSmallArc(double r, double a1, double a2) {
Point2D.Double[] result = new Point2D.Double[4];
// Compute all four points for an arc that subtends the same total angle
// but is centered on the X-axis
double a = (a2 - a1) / 2.0;
double x4 = r * Math.cos(a);
double y4 = r * Math.sin(a);
double x1 = x4;
double y1 = -y4;
double q1 = x1*x1 + y1*y1;
double q2 = q1 + x1*x4 + y1*y4;
double k2 = 4d/3d * (Math.sqrt(2 * q1 * q2) - q2) / (x1 * y4 - y1 * x4);
double x2 = x1 - k2 * y1;
double y2 = y1 + k2 * x1;
double x3 = x2;
double y3 = -y2;
// Find the arc points' actual locations by computing x1,y1 and x4,y4
// and rotating the control points by a + a1
double ar = a + a1;
double cos_ar = Math.cos(ar);
double sin_ar = Math.sin(ar);
result[0] = new Point2D.Double(r * Math.cos(a1), r * Math.sin(a1));
result[1] = new Point2D.Double(x2 * cos_ar - y2 * sin_ar, x2 * sin_ar + y2 * cos_ar);
result[2] = new Point2D.Double(x3 * cos_ar - y3 * sin_ar, x3 * sin_ar + y3 * cos_ar);
result[3] = new Point2D.Double(r * Math.cos(a2), r * Math.sin(a2));
return result;
}
/**
* Adds an arc to the path. The arc must be smaller or equal to pi/2 (90°)
* @param path - the path to add the arc to
* @param moveTo - if true adds a path.moveTo to the beginning of the arc
* @param r - the radius of the arc
* @param a1 - the start angle in radians
* @param a2 - the end angle in radians
* @param x - translate by x on the x axis
* @param y - translate by y on the y axis
*/
public static Path2D addArc(Path2D path, boolean moveTo, double r, double a1, double a2, double x, double y) {
Point2D.Double[] arc = createSmallArc(r, a1, a2);
if(moveTo) {
path.moveTo(arc[0].x+x, arc[0].y+y);
}
path.curveTo(arc[1].x+x, arc[1].y+y, arc[2].x+x, arc[2].y+y, arc[3].x+x, arc[3].y+y);
return path;
}
}