
/**
 *  This class can be used as a basis for writing animated
 *  "Mosaic" applets.  The applet shows a grid of colored
 *  rectangles.  In this version, the sqares are given random 
 *  colors and the colors are changed every two seconds.
 *  This is mainly meant to be used as a base class for
 *  more interesting applets.  To use it, write a class that
 *  extends this one and include a new "public void program()"
 *  subroutine.  This "program" will be run when an applet
 *  is created.  It can use the subroutines for getting and
 *  setting colors of the squares.  It is advisable to include
 *  some delays in the program by calling the delay subroutine.
 *  It is impolite for an applet to run continuously.
 *     The applet is set up to use a grid with 40 rows and 40
 *  columns and a default color of black for the rectangles.
 *  To change this, include an init() method in your applet
 *  and use it to change the values.
 *     Hidden feature:  If you right-click on the applet,
 *  the program will be stopped and restarted, so that it
 *  runs again from the beginning.  (On Macintosh, right-click
 *  equals command-click.)
 */

import java.awt.*;
import java.awt.event.*;
import java.applet.Applet;

public class SimpleMosaicApplet extends Applet 
                 implements Runnable, MouseListener {

   protected int rows = 40;     // The number of rows in the grid.
                                // (This could be changed in the
                                // init() method in a subclass.)
                                
   protected int columns = 40;  // The number of columns in the grid.
                                // (This could be changed in the
                                // init() method in a subclass.)
                                
   protected Color defaultColor = Color.black; 
                                // Default color for rectangles in
                                // the grid.  (This could be changed
                                // in the init() method in a subclass.)


   /** 
    *  The program() subroutine is run by the applet (in a separate
    *  thread).  Override this in a subclass to change the behavior
    *  of the applet.  In this class, for something to do, the
    *  program fills the grid with random colors every second.
    */
   public void program() {
      while (true) {
         fillRandomly();
         delay(1000);
      }
   }
   
   /**
    *   Return the color of the retangle at the specified row and column.
    *   If the values of row and col are not in the grid, then the
    *   default color (black unless it is changed in a subclass) is returned.
    */
   public Color getColor(int row, int col) {
      checkStatus();
      if (row >=0 && row < rows && col >= 0 && col < columns)
         return grid[row][col];
      else
         return defaultColor;
   }

   /**
    *  Returns the amount of red in the color of the rectangle
    *  at the specified row and column.  The value is in the
    *  range 0 to 255, inclusive.
    */ 
   public int getRed(int row, int col) {
      return getColor(row,col).getRed();
   }

   /**
    *  Returns the amount of green in the color of the rectangle
    *  at the specified row and column.  The value is in the
    *  range 0 to 255, inclusive.
    */ 
   public int getGreen(int row, int col) {
      return getColor(row,col).getGreen();
   }

   /**
    *  Returns the amount of blue in the color of the rectangle
    *  at the specified row and column.  The value is in the
    *  range 0 to 255, inclusive.
    */ 
   public int getBlue(int row, int col) {
      return getColor(row,col).getBlue();
   }

   /**
    *  Set the color of the rectangle at the specified row and column.
    *  (This is ignored if the color, c, is null or if row and col
    *  do no lie within the grid.)
    */
   public void setColor(int row, int col, Color c) {
      if (row >=0 && row < rows && col >= 0 && col < columns && c != null) {
         grid[row][col] = c;
         blemished = true;
         drawSquare(row,col);
      }
      checkStatus();
   }

   /**
    *  Set the color of the rectangle at the specified row and column.
    *  The values for the red, green, and blue components should be
    *  in the range 0 to 255, inclusive.  If not, the values are
    *  clamped to lie within that range.  (This is ignored if
    *  row and col do no lie within the grid.)
    */
   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);
         blemished = true;
      }
      checkStatus();
   }

   /**
    *  Set all rectangles in the grid to the specified color.  (This
    *  is ignored if the color, c, is null.)
    */
   public void fill(Color c) {
      if (c == null)
         return;
      for (int i = 0; i < rows; i++)
         for (int j = 0; j < columns; j++)
            grid[i][j] = c;
      blemished = false;
      repaint();      
      checkStatus();
   }

   /**
    *  Set all rectangles in the grid to the specified color. 
    */
   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));
      checkStatus();
   }

   /**
    *  Set each rectangle in the grid to have a random color.
    */
   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);
      }
      blemished = true;
      repaint();
      checkStatus();
   }

   /**
    *  Pause for the given number of milliseconds.  (One second
    *  is 1000 milliseconds.)
    */
   public void delay(int milliseconds) {
      if (milliseconds > 0) {
         try { Thread.sleep(milliseconds); }
         catch (InterruptedException e) { }
      }
      checkStatus();
   }
   
   //---------------------------- implementation details --------------------------

   private Color[][] grid;  // Holds the colors of all the rectangles.\
   
   private boolean blemished = false;  // Set to true if any of the
                                       // rectangles have been changed
                                       // from the default color.
                                       
   private boolean listening = false;  // Set to true when mouse
                                       // listening is first set up.
                                       
   private Thread runner;    // A thread for running the program.
   private int status;       // Used to control the thread.
   private static final int  // Possible values of status.
            IDLE=0, 
            RUNNING=1, 
            SUSPENDED=2, 
            KILLED=3,
            RESTARTED=4;

   /**
    *  Override update() so it doen't clear the applet -- not
    *  meant to be called directly.
    */   
   public void update(Graphics g) {
      paint(g);
   }


   /**
    *  Repaint the applet -- not meant to be called directly.
    */
   public synchronized void paint(Graphics g) {
      if (status == RESTARTED) {
         g.setColor(Color.white);
         g.fillRect(0,0,getSize().width,getSize().height);
         g.setColor(Color.black);
         g.drawRect(0,0,getSize().width-1,getSize().height-1);
         g.drawString("Restarting...", 20, 30);
         return;
      }
      if (!blemished) {
         g.setColor(grid[0][0]);
         g.fillRect(0,0,getSize().width,getSize().height);
      }
      else {
         double rowHeight = (double)getSize().height / rows;
         double colWidth = (double)getSize().width / columns;
         for (int i = 0; i < rows; i++) {
            int y = (int)Math.round(rowHeight*i);
            int h = (int)Math.round(rowHeight*(i+1)) - y;
            for (int j = 0; j < columns; j++) {
               int x = (int)Math.round(colWidth*j);
               int w = (int)Math.round(colWidth*(j+1)) - x;
               g.setColor(grid[i][j]);
               g.fillRect(x,y,w,h);
            }
         }
         
      }
   }


   /** 
    *  Directly draw the square at the specifed row and column,
    *  using a newly created graphics context.
    */
   private synchronized void drawSquare(int row, int col) {
      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;
      Graphics g = getGraphics();
      g.setColor(grid[row][col]);
      g.fillRect(x,y,w,h);
      g.dispose();
   }

   /**
    *  The method that is run by the thread -- not meant to be
    *  called directly.
    */
   public void run() {
      if (status == RESTARTED) {
         repaint();
         delay(2000);
         status = RUNNING;
      }
      else {
         grid = new Color[rows][columns];
      }
      for (int i = 0; i < rows; i++)
         for (int j = 0; j < columns; j++)
            grid[i][j] = defaultColor;
      setBackground(defaultColor);
      if (!listening) {
         addMouseListener(this);
         listening = true;
      }
      repaint();
      delay(250);
      try {
         program();
      }
      catch (Exception e) {
        status = IDLE;
      }
   }

   /**
    *  This is called by various subroutines in this
    *  class to give the thread a chance to check for 
    *  changes in the status variable.
    */
   synchronized private void checkStatus() { 
      while (status == SUSPENDED) {
         try {
            wait();
         }
         catch (InterruptedException e) {
         }
      }
      if (status == KILLED)
         throw new RuntimeException();
   }
   
   /**
    *  Called by the system when the applet is started or restarted --
    *  not meant to be called directly.
    */
   synchronized public void start() {
      status = RUNNING;
      if (runner != null && runner.isAlive()) {
         notify();
      }
      else {
         runner = new Thread(this);
         runner.start();
      }
   }
   
   /**
    *  Called by the system when the applet is suspended -- not meant
    *  called directly.
    */
   synchronized public void stop() {
      if (runner != null && runner.isAlive()) {
         status = SUSPENDED;
         notify();
      }
   }
   
   /**
    *  Called by the system when the applet is destroyed -- not meant
    *  called directly.
    */
   synchronized public void destroy() {
      if (runner != null && runner.isAlive())
         runner.stop(); // bad form, but what can I do?
      status = IDLE;
      notify();  // in case checkStatus is waiting
   }

   /**
    *  Called by the system to get the preferred size of
    *  the applet -- not meant to be called directly.
    */
   public Dimension getPreferredSize() {
      return new Dimension(columns*10, rows*10);
   }
   
   /**
    *  Called when the user presses the mouse on the applet --
    *  not meant to be called directly.
    */
   synchronized public void mousePressed(MouseEvent evt) {
      if (!evt.isMetaDown())
         return;  // ignore unless right mouse button is pressed
      if (runner != null && runner.isAlive()) {
         status = IDLE;
         runner.stop();  // bad form, but what can I do?
         notify();
         try {
            runner.join(500);
         }
         catch (InterruptedException e) {
         }
         runner = new Thread(this);
         status = RESTARTED;
         runner.start();
      }
   }

   /**
    *  Called when the user releases the mouse on the applet --
    *  not meant to be called directly.
    */
   public void mouseReleased(MouseEvent evt) {
   }

   /**
    *  Called when the user presses and releases the mouse on the applet --
    *  not meant to be called directly.
    */
   public void mouseClicked(MouseEvent evt) {
   }

   /**
    *  Called when the mouse enters the applet --
    *  not meant to be called directly.
    */
   public void mouseEntered(MouseEvent evt) {
   }

   /**
    *  Called when the mouse leaves the applet --
    *  not meant to be called directly.
    */
   public void mouseExited(MouseEvent evt) {
   }

} // end class SimpleMosaicApplet
