
/**
 *  A MosaicCanvasX object represents a grid containing rows
 *  and columns of colored rectangles.  There can be "grouting"
 *  between the rectangles.  (The grouting is just drawn as a 
 *  one-pixel outline around each rectangle.)  The rectangles
 *  are drawn as raised 3D-style rectangles.  Methods are 
 *  provided for getting and setting the colors of the rectangles.
 */

import java.awt.*;

class MosaicCanvasX extends Canvas {


   //------------------ private instance variables --------------------


   private int rows;       // The number of rows of rectangles in the grid.
   private int columns;    // The number of columns of rectangles in the grid.
   private Color defaultColor;   // Color used for any rectangle whose color
                                 //    has not been set explicitly.  This
                                 //    can never be null.
   private Color groutingColor;  // The color for "grouting" between 
                                 //    rectangles.  If this is null, no
                                 //    grouting is drawn.
   private boolean alwaysDrawGrouting;  // Grouting is drawn around default-
                                        //    colored rects if this is true.
   private Color[][] grid; // An array that contains the rectangles' colors.
                           //   If a null occurs in this array, the rectangle
                           //   is drawn in the default color, and "grouting"
                           //   will be drawn around that rectangle only if
                           //   alwaysDrawGrouting is true.  Also, the 
                           //   rectangle is drawn as a flat rectangle rather
                           //   than as a 3D rectangle.
   

   //------------------------ constructors -----------------------------


   /**
    *  Construct a MosaicCanvasX with 20 rows and 20 columns of rectangles.
    */
   public MosaicCanvasX() {
      this(20,20);
   }


   /**
    *  Construct a MosaicCanvasX with the specified number of rows and
    *  columns of rectangles.  The defaults color is black, the
    *  grouting color is gray, and alwaysDrawGrouting is set to true.
    */
   public MosaicCanvasX(int rows, int columns) {
      this.rows = rows;
      this.columns = columns;
      grid = new Color[rows][columns];
      defaultColor = Color.black;
      groutingColor = Color.gray;
      alwaysDrawGrouting = false;
      setBackground(defaultColor);
   }
   
   
   //--------- methods for getting and setting grid properties ----------
   

   /**
    *  Set the defaultColor.  If c is null, this is ignored.
    *  When a mosaic is first created, the defaultColor is black.
    *  This is the color that is used for rectangles whose color
    *  value is null.  Such rectangles are drawn as flat rather
    *  than 3D rectangles.
    */
   public void setDefaultColor(Color c) {
      if (c != null) {
         defaultColor = c;
         setBackground(c);
         repaint();
      }
   }
   
   
   /**
    *  Return the defaultColor, which cannot be null.
    */
   public Color getDefaultColor() {
      return defaultColor;
   }
   
   
   /**
    *  Set the color of the "grouting" that is drawn between rectangles.
    *  If the value is null, no grouting is drawn and the rectangles
    *  fill the entire grid.   When a mosaic is first created, the
    *  groutingColor is gray.
    */
   public void setGroutingColor(Color c) {
      groutingColor = c;
      repaint();
   }
   
   
   /**
    *  Get the current groutingColor, which can be null.
    */
   public Color getGroutingColor(Color c) {
      return groutingColor;
   }
   

   /**
    *  Set the value of alwaysDrawGrouting.  If this is false, then
    *  no grouting is drawn around rectangles whose color value is null.
    *  When a mosaic is first created, the value is false.
    */
   public void setAlwaysDrawGrouting(boolean always) {
      if (alwaysDrawGrouting != always) {
         alwaysDrawGrouting = always;
         repaint();
      }
   }


   /**
    *  Get the value of the alwaysDrawGrouting property.
    */   
   public boolean getAlwaysDrawGrouting() {
      return alwaysDrawGrouting; 
   }
   

   /**
    *  Set the number of rows and columns in the grid.  If the value of
    *  the preserveData parameter is false, then the color values of all
    *  the rectangles in the new grid are set to null.  If it is true,
    *  then as much color data as will fit is copied from the old grid.
    */
   synchronized public void setGridSize(int rows, 
                                   int columns, boolean preserveData) {
      if (rows > 0 && columns > 0) {
         Color[][] newGrid = new Color[rows][columns];
         if (preserveData) {
            int rowMax = Math.min(rows,this.rows);
            int colMax = Math.min(columns,this.columns);
            for (int r = 0; r < rowMax; r++)
               for (int c = 0; c < colMax; c++)
                  newGrid[r][c] = grid[r][c];
         }
         grid = newGrid;
         this.rows = rows;
         this.columns = columns;
         repaint();
      }
   }
   
   
   /**
    *  Return the number of rows of rectangles in the grid.
    */
   public int getRowCount() {
      return rows;
   }
   

   /**
    *  Return the number of columns of rectangles in the grid.
    */
   public int getColumnCount() {
      return columns;
   }   
   
   
   //------------------ other useful public methods ---------------------
   
   
   /**
    *  Given an x-coordinate, x, return the number of the grid column that
    *  contains that x-coordinate.  Columns are numbered starting with zero.
    *  If x lies outside the boundaries of the canvas, then -1 is returned.
    */
   public int findColumn(int x) {
      if (x < 0)
         return -1;
      double colWidth = (double)getSize().width / columns;
      int col = (int)(x / colWidth);
      if (col >= columns)
         return -1;
      return col;
   }
   

   /**
    *  Given an y-coordinate, y, return the number of the grid row that
    *  contains that y-coordinate.  Rows are numbered starting with zero.
    *  If y lies outside the boundaries of the canvas, then -1 is returned.
    */
   public int findRow(int y) {
      if (y < 0)
         return -1;
      double rowHeight = (double)getSize().height / rows;
      int row = (int)(y / rowHeight);
      if (row >= rows)
         return -1;
      return row;
   }

   /**
    *  Get the color has been set for the rectangle in the specified
    *  row and column of the grid.  This value can be null if no
    *  color has been set for that rectangle.  (Such rectangles are
    *  actually displayed using the defaultColor.)  If the specified
    *  rectangle is outside the grid, then null is returned.
    */
   public Color getColor(int row, int col) {
      if (row >=0 && row < rows && col >= 0 && col < columns)
         return grid[row][col];
      else
         return null;
   }


   /**
    *  Return the red component of color of the rectangle in the
    *  specified row and column.  If that rectangle lies outside 
    *  the grid or if no color has been specified for the rectangle,
    *  then the red component of the defaultColor is returned.
    */
   public int getRed(int row, int col) {
      if (row >=0 && row < rows && col >= 0 && col < columns)
         return grid[row][col].getRed();
      else
         return defaultColor.getRed();
   }


   /**
    *  Return the green component of color of the rectangle in the
    *  specified row and column.  If that rectangle lies outside 
    *  the grid or if no color has been specified for the rectangle,
    *  then the green component of the defaultColor is returned.
    */
   public int getGreen(int row, int col) {
      if (row >=0 && row < rows && col >= 0 && col < columns)
         return grid[row][col].getGreen();
      else
         return defaultColor.getGreen();
   }


   /**
    *  Return the blue component of color of the rectangle in the
    *  specified row and column.  If that rectangle lies outside 
    *  the grid or if no color has been specified for the rectangle,
    *  then the blue component of the defaultColor is returned.
    */
   public int getBlue(int row, int col) {
      if (row >=0 && row < rows && col >= 0 && col < columns)
         return grid[row][col].getBlue();
      else
         return defaultColor.getBlue();
   }


   /**
    *  Set the color of the rectangle in the specified row and column.
    *  If the rectangle lies outside the grid, this is simply ignored.
    *  The color can be null.  Rectangles for which the color is null
    *  will be displayed in the defaultColor, and they will be shown
    *  as flat rather than 3D rects.
    */
   public void setColor(int row, int col, Color c) {
      if (row >=0 && row < rows && col >= 0 && col < columns) {
         grid[row][col] = c;
         drawSquare(row,col);
      }
   }


   /**
    *  Set the color of the rectangle in the specified row and column.
    *  The color is specified by giving red, green, and blue components
    *  of the color.  These values should be in the range from 0 to
    *  255, inclusive, and they are clamped to lie in that range.
    *  If the rectangle lies outside the grid, this is simply ignored.
    */
   public void setColor(int row, int col, int red, int green, int blue) {
      if (row >=0 && row < rows && col >= 0 && col < columns) {
         red = (red < 0)? 0 : ( (red > 255)? 255 : red);
         green = (green < 0)? 0 : ( (green > 255)? 255 : green);
         blue = (blue < 0)? 0 : ( (blue > 255)? 255 : blue);
         grid[row][col] = new Color(red,green,blue);
         drawSquare(row,col);
      }
   }


   /**
    *  Set the color of the rectangle in the specified row and column.
    *  The color is specified by giving hue, saturation, and brightness
    *  components of the color.  These values should be in the range from 
    *  0.0 to 1.0, inclusive, and they are clamped to lie in that range.
    *  If the rectangle lies outside the grid, this is simply ignored.
    */
   public void setHSBColor(int row, int col, 
                  double hue, double saturation, double brightness) {
      if (row >=0 && row < rows && col >= 0 && col < columns) {
         grid[row][col] = makeHSBColor(hue,saturation,brightness);
         drawSquare(row,col);
      }
   }
   
   
   /**
    *  A little utility routine that is provided for making a color
    *  from hue, saturation, and brightness values.  These values should
    *  be in the range from 0.0 to 1.0, inclusive, and they are clamped
    *  to lie in that range.  (This method is more convenient than
    *  Color.getHSBColor() since it use double values rather than float.)
    */
   public static Color makeHSBColor(
                  double hue, double saturation, double brightness) {
      float h = (float)hue;
      float s = (float)saturation;
      float b = (float)brightness;
      h = (h < 0)? 0 : ( (h > 1)? 1 : h );
      s = (s < 0)? 0 : ( (s > 1)? 1 : s );
      b = (b < 0)? 0 : ( (b > 1)? 1 : b );
      return Color.getHSBColor(h,s,b);
   }
   

   /**
    *  Set all rectangles of the grid to have the specified color.
    *  The color can be null.  In that case, the rectangles are
    *  drawn as flat rather than 3D rects in the defaultColor.
    */
   public void fill(Color c) {
      for (int i = 0; i < rows; i++)
         for (int j = 0; j < columns; j++)
            grid[i][j] = c;
      repaint();      
   }


   /**
    *  Set all rectangles of the grid to have the color specified by
    *  the given red, green, and blue components.  These components 
    *  should be integers in the range 0 to 255 and are clamped to lie
    *  in that range.
    */
   public void fill(int red, int green, int blue) {
      red = (red < 0)? 0 : ( (red > 255)? 255 : red);
      green = (green < 0)? 0 : ( (green > 255)? 255 : green);
      blue = (blue < 0)? 0 : ( (blue > 255)? 255 : blue);
      fill(new Color(red,green,blue));
   }

   
   /**
    *  Fill all the rectangles with randomly selected colors.
    */
   public void fillRandomly() {
      for (int i = 0; i < rows; i++)
         for (int j = 0; j < columns; j++) {
            int r = (int)(256*Math.random());
            int g = (int)(256*Math.random());
            int b = (int)(256*Math.random());
            grid[i][j] = new Color(r,g,b);
      }
      repaint();
   }
   
   
   /**
    *   Clear the mosaic by setting all the colors to null.
    */
   public void clear() {
      fill(null);
   }


   /**
    *   Return an object that contains the color data that
    *   is needed to redraw the mosaic.  This includes the 
    *   defaultColor, the groutingColor, the number of rows and
    *   columns, the color of each rectangle, and the
    *   value of alwaysDrawGrouting.
    */
   synchronized public Object copyColorData() {
        // Note:  This is a fudge.  The data about defaultColor,
        // groutingColor, and alwaysDrawGrouting is added to the
        // last row of the grid.  If alwaysDrawGrouting is true,
        // this is recorded by adding an extra empty space to
        // that row.
      Color[][] copy = new Color[rows][columns];
      // Replace the last row with a longer row.
      if (alwaysDrawGrouting)
         copy[rows-1] = new Color[columns+3];
      else
         copy[rows-1] = new Color[columns+2];
      for (int r = 0; r < rows; r++)
         for (int c = 0; c < columns; c++)
            copy[r][c] = grid[r][c];
      copy[rows-1][columns] = defaultColor;
      copy[rows-1][columns+1] = groutingColor;
      return copy;
   }
   

   /**
    *  The parameter to this method should be an Object that
    *  was created by the copyColorData() method.  This method
    *  will restore the data in the object to the grid.  This
    *  can change the size of the grid, the colors in the grid,
    *  the defaultColor, the groutingColor, and the value of
    *  alwaysDrawGrouting.  If the object is of the proper
    *  form, then the return value is true.  If not, the return
    *  value is false and no changes are made to the current data.
    */
   synchronized public boolean restoreColorData(Object data) {
      if (data == null || !(data instanceof Color[][]))
         return false;
      Color[][] newGrid = (Color[][])data;
      int newRows = newGrid.length;
      if (newRows == 0 || newGrid[0].length == 0)
         return false;
      int newColumns = newGrid[0].length;
      for (int r = 1; r < newRows-1; r++)
         if (newGrid[r].length != newColumns)
            return false;
      if (newGrid[newRows-1].length != newColumns+2
               && newGrid[newRows-1].length != newColumns+3)
         return false;
      if (newGrid[newRows-1][newColumns] == null)
         return false;
      rows = newRows;
      columns = newColumns;
      grid = newGrid;
      defaultColor = newGrid[newRows-1][newColumns];
      setBackground(defaultColor);
      groutingColor = newGrid[newRows-1][newColumns+1];
      alwaysDrawGrouting = newGrid[newRows-1].length == 3;
      repaint();
      return true;
   }
   
   
   //--------------- implementation details ------------------------
   //---------- (routines called internally or by the system) ------
      
   public synchronized void paint(Graphics g) {
          // draw all the rectangles.
      for (int i = 0; i < rows; i++) {
         for (int j = 0; j < columns; j++) {
            drawSquare(g,i,j);
         }
      }
   }
   
   public void update(Graphics g) {
         // Override, so it doesn't clear the applet before redrawing.
      paint(g);
   }

   public Dimension getPreferredSize() {
         // The preferred size allows for 10-by-10 pixel squares.
      return new Dimension(columns*10, rows*10);
   }

   private void drawSquare(Graphics g, int row, int col) {
         // Draw one of the rectangles in a specified graphics 
         // context.  g must be non-null and (row,col) must be
         // in the grid.
      double rowHeight = (double)getSize().height / rows;
      double colWidth = (double)getSize().width / columns;
      int y = (int)Math.round(rowHeight*row);
      int h = (int)Math.round(rowHeight*(row+1)) - y;
      int x = (int)Math.round(colWidth*col);
      int w = (int)Math.round(colWidth*(col+1)) - x;
      Color c = grid[row][col];
      g.setColor( (c == null)? defaultColor : c );
      if (groutingColor == null || (c == null && !alwaysDrawGrouting)) {
         if (c == null)
            g.fillRect(x,y,w,h);
         else
            g.fill3DRect(x,y,w,h,true);
      }
      else {
         if (c == null)
            g.fillRect(x+1,y+1,w-2,h-2);
         else
            g.fill3DRect(x+1,y+1,w-2,h-2,true);
         g.setColor(groutingColor);
         g.drawRect(x,y,w-1,h-1);
      }
   }

   private synchronized void drawSquare(int row, int col) {
        // Draw a specified rectangle directly on the applet in
        // a newly allocated graphics context.  (row,col) must be
        // within the grid.
      Graphics g = getGraphics();
      drawSquare(g,row,col);
      g.dispose();
   }


} // end class MosaicCanvasX
