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