/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4graph; import static java.awt.BasicStroke.CAP_ROUND; import static java.awt.BasicStroke.JOIN_ROUND; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.CubicCurve2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import edu.mit.csail.sdg.alloy4.OurPDFWriter; /** This class abstracts the drawing operations so that we can * draw the graph using different frameworks such as Java2D or PDF. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread. */ public final strictfp class Artist { /** The font name. */ private static final String fontName = "Lucida Grande"; /** The font size. */ private static final int fontSize = 12; /** The corresponding Graphics2D object. */ private Graphics2D gr; /** The corresponding OurPDFWriter. */ private OurPDFWriter pdf; /** Construct an artist that acts as a wrapper around the given Graphics2D object. */ public Artist(Graphics2D graphics2D) { this.gr=graphics2D; this.pdf=null; } /** Construct an artist that acts as a wrapper around the given OurPDFWriter object. */ public Artist(OurPDFWriter pdfWriter) { this.gr=null; this.pdf=pdfWriter; } /** Shifts the coordinate space by the given amount. */ public void translate(int x, int y) { if (gr!=null) gr.translate(x,y); else pdf.shiftCoordinateSpace(x, y); } /** Draws a circle of the given radius, centered at (0,0) */ public void drawCircle(int radius) { if (gr!=null) gr.drawArc(-radius, -radius, radius*2, radius*2, 0, 360); else pdf.drawCircle(radius, false); } /** Fills a circle of the given radius, centered at (0,0) */ public void fillCircle(int radius) { if (gr!=null) gr.fillArc(-radius, -radius, radius*2, radius*2, 0, 360); else pdf.drawCircle(radius, true); } /** Draws a line from (x1,y1) to (x2,y2) */ public void drawLine(int x1, int y1, int x2, int y2) { if (gr!=null) gr.drawLine(x1,y1,x2,y2); else pdf.drawLine(x1, y1, x2, y2); } /** Changes the current color. */ public void setColor(Color color) { if (gr!=null) gr.setColor(color); else pdf.setColor(color); } /** Returns true if left<=x<=right or right<=x<=left. */ private static boolean in(double left, double x, double right) { return (left<=x && x<=right) || (right<=x && x<=left); } /** Draws the given curve smoothly (assuming the curve is monotonic vertically) */ public void drawSmoothly(Curve curve) { final int smooth=15; double cx=0, cy=0, slope; for(int n=curve.list.size(), i=0; i<n; i++) { CubicCurve2D.Double c=new CubicCurve2D.Double(), c2=(i+1<n)?curve.list.get(i+1):null; c.setCurve(curve.list.get(i)); if (i>0) { c.ctrlx1=cx; c.ctrly1=cy; } if (c2==null) { draw(c,false); return; } if ((c.x1<c.x2 && c2.x2<c2.x1) || (c.x1>c.x2 && c2.x2>c2.x1)) slope=0; else slope=(c2.x2-c.x1)/(c2.y2-c.y1); double tmp=c.y2-smooth, tmpx=c.x2-smooth*slope; if (tmp>c.ctrly1 && tmp<c.y2 && in(c.x1, tmpx, c.x2)) { c.ctrly2=tmp; c.ctrlx2=tmpx; } double tmp2=c2.y1+smooth, tmp2x=c2.x1+smooth*slope; if (tmp2>c2.y1 && tmp2<c2.ctrly2 && in(c2.x1, tmp2x, c2.x2)) { cy=tmp2; cx=tmp2x; } else { cy=c2.ctrly1; cx=c2.ctrlx1; } draw(c,false); } } /** Draws the given curve. */ public void draw(Curve curve) { for(CubicCurve2D.Double c: curve.list) draw(c, false); } /** Draws the outline of the given shape. */ public void draw(Shape shape, boolean fillOrNot) { if (gr==null) pdf.drawShape(shape, fillOrNot); else if (fillOrNot) gr.fill(shape); else gr.draw(shape); } /** The pattern for dotted line. */ private static float[] dot = new float[]{1f,3f}; /** The pattern for dashed line. */ private static float[] dashed = new float[]{6f,3f}; /** Modifies the given Graphics2D object to use the line style representing by this object. * <p> NOTE: as a special guarantee, if gr2d==null, then this method returns immediately without doing anything. * <p> NOTE: just like the typical AWT and Swing methods, this method can be called only by the AWT event dispatching thread. */ public void set(DotStyle style, double scale) { if (gr!=null) { BasicStroke bs; switch(style) { case BOLD: bs=new BasicStroke(scale>1 ? (float)(2.6d/scale) : 2.6f); break; case DOTTED: bs=new BasicStroke(scale>1 ? (float)(1.3d/scale) : 1.3f, CAP_ROUND, JOIN_ROUND, 15f, dot, 0f); break; case DASHED: bs=new BasicStroke(scale>1 ? (float)(1.3d/scale) : 1.3f, CAP_ROUND, JOIN_ROUND, 15f, dashed, 5f); break; default: bs=new BasicStroke(scale>1 ? (float)(1.3d/scale) : 1.3f); } gr.setStroke(bs); return; } switch(style) { case BOLD: pdf.setBoldLine(); return; case DOTTED: pdf.setDottedLine(); return; case DASHED: pdf.setDashedLine(); return; default: pdf.setNormalLine(); return; } } /** Saves the current font boldness. */ private boolean fontBoldness = false; /** Changes the current font. */ public void setFont(boolean fontBoldness) { calc(); if (gr!=null) gr.setFont(fontBoldness ? cachedBoldFont : cachedPlainFont); else this.fontBoldness=fontBoldness; } /** Draws the given string at (x,y) */ public void drawString(String text, int x, int y) { if (text.length()==0) return; if (gr!=null) { gr.drawString(text,x,y); return; } calc(); Font font = (fontBoldness ? cachedBoldFont : cachedPlainFont); GlyphVector gv = font.createGlyphVector(new FontRenderContext(null,false,false), text); translate(x,y); draw(gv.getOutline(), true); translate(-x,-y); } /** If nonnull, it caches a Graphics2D object for calculating string bounds. */ private static Graphics2D cachedGraphics = null; /** If nonnull, it caches a FontMetrics object associated with the nonbold font. */ private static FontMetrics cachedPlainMetrics = null; /** If nonnull, it caches a FontMetrics object associated with the bold font. */ private static FontMetrics cachedBoldMetrics = null; /** If nonnull, it caches the nonbold Font. */ private static Font cachedPlainFont = null; /** If nonnull, it caches the bold Font. */ private static Font cachedBoldFont = null; /** If nonnegative, it caches the maximum ascent of the font. */ private static int cachedMaxAscent = -1; /** If nonnegative, it caches the maximum descent of the font. */ private static int cachedMaxDescent = -1; /** Allocates the nonbold and bold fonts, then calculates the max ascent and descent. */ private static void calc() { if (cachedMaxDescent >= 0) return; // already done BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); cachedGraphics = (Graphics2D)(image.getGraphics()); cachedPlainMetrics = cachedGraphics.getFontMetrics(cachedPlainFont = new Font(fontName, Font.PLAIN, fontSize)); cachedBoldMetrics = cachedGraphics.getFontMetrics(cachedBoldFont = new Font(fontName, Font.BOLD, fontSize)); cachedGraphics.setFont(cachedPlainFont); cachedMaxAscent = cachedPlainMetrics.getMaxAscent(); cachedMaxDescent = cachedPlainMetrics.getMaxDescent(); } /** Returns the max ascent when drawing text using the given font size and font boldness settings. */ public static int getMaxAscent() { calc(); return cachedMaxAscent; } /** Returns the sum of the max ascent and max descent when drawing text using the given font size and font boldness settings. */ public static int getMaxAscentAndDescent() { calc(); return cachedMaxAscent + cachedMaxDescent; } /** Returns the bounding box when drawing the given string using the given font size and font boldness settings. */ public static Rectangle2D getBounds(boolean fontBoldness, String string) { calc(); return (fontBoldness ? cachedBoldMetrics : cachedPlainMetrics).getStringBounds(string, cachedGraphics); } }