/* 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);
}
}