// G5BGRA Coursework
// 240x240 pixel JPEG chroma keying program
// @author Anthony Jaroslav Truhlar 
// 28/12/1998
// Email: ajt97c@cs.nott.ac.uk

/* Extra features:
   ===============
1. Ability to set values of the hue of chroma keying colour and the +/-
   deviation by typing them into the text boxes (return must be pressed)
   as well as by adjusting the scrollbars.

2. Canvas update methods overrided to only clear when necessary - this
   prevents 'flicker' when buttons are pressed and hence images redrawn.
   Note that the bounding rectangles are still drawn, and this has been left in
   to give some indication to the user that the given image is being drawn.

3. Size of main frame set to 732x400 since the layout method used only
   gave canvasses of 237x240 (measured with Paint Shop Pro); when the main 
   frame size was set at 720x400. The adjustment allows the required size of 
   canvas (240x240) to be displayed. */

// Import required classes
import java.awt.*;		// AWT's components
import java.awt.event.*;	// AWT's event handling class
import java.awt.image.*;	// AWT's image manipulaton/processing class


// Use innner classes
public class ajt97c extends Frame {

  // declare constants for frame size
  int mainWidth = 732;  // to ensure 240x240 panels 
  int mainHeight = 400; // (720x400 only gives 237x240 panels with layout used)

  public static void main(String args[]) {

    // create frame for application
    new ajt97c();

  } // end of main

  ajt97c() { // constructor for ajt97c

    // set title of main frame
    setTitle("Anthony Jaroslav Truhlar, ajt97c*");
    // set size of main frame 	
    setSize(mainWidth,mainHeight);  

    // center frame (if display big enough)
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension d = tk.getScreenSize();
    int screenHeight = d.height;
    int screenWidth = d.width;

    if ( screenWidth <= mainWidth ) { // deal with non-SVGA displays

      setLocation(0,((screenHeight-mainHeight)/2));

    } else {

      setLocation(((screenWidth-mainWidth)/2),((screenHeight-mainHeight)/2));

    } // end if 

    setResizable(false); // ensure frame isn't resized [jdk 1.1.7 or better]

    // add listener for window events
    addWindowListener (

      // pass an instance of WindowAdapter to save 
      // re-writing the 7 required methods
      new WindowAdapter() {
	
        // override windowClosing() method to exit on window close
        public void windowClosing(WindowEvent e) {

          System.exit(0); // terminate

        } // end of windowClosing

      } // end of instance of WindowAdapter

      ); // end of WindowListener declaration

    // add a panel
    add(new mainPanel());
    show();

  } // end of constructor for ajt97c


  public class mainPanel extends Panel {

    // declare variables for use in mainPanel
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // declare a panel for each image, and one for the buttons, etc
    Panel topMain = new Panel(new GridLayout(1,3));  // has next 3 panels
    Panel fgPanel = new Panel();       // panel for foreground image canvas
    Panel keyedPanel = new Panel();    // panel for keyed image canvas
    Panel bgPanel = new Panel();       // panel for background image canvas
    Panel uselessPanel = new Panel();  // this panel is for 'padding' only
    Panel bttmMain = new Panel(new GridLayout(2,4)); // contains widgets

    // delcare canvasses for the images
    Canvas fgCanvas = new fgndCanvas();     // canvas for foreground image
    Canvas keyedCanvas = new keyedCanvas(); // canvas for keyed image
    Canvas bgCanvas = new bkgndCanvas();    // canvas for background image
    
    // declare the images
    Image fgImage = Toolkit.getDefaultToolkit().getImage("fiona.jpg");
    Image bgImage = Toolkit.getDefaultToolkit().getImage("back.jpg");
    Image fgImage2, bgImage2, keyedImage; // images to be generated from memory

    // delcare integers for width & height of images
    int imgWidth = 240;
    int imgHeight = 240;

    // declare 1D arrays for pixels
    int[] fgPixels = new int [imgWidth*imgHeight];
    int[] keyedPixels = new int [imgWidth*imgHeight];
    int[] bgPixels = new int [imgWidth*imgHeight];

    // declare pixel grabbers for the images
    PixelGrabber fgPg = new PixelGrabber(fgImage,0,0,imgWidth,imgHeight,fgPixels,0,imgWidth);
    PixelGrabber bgPg = new PixelGrabber(bgImage,0,0,imgWidth,imgHeight,bgPixels,0,imgWidth);

    // declare the four buttons for loading, clearing and keying images 
    Button loadfg = new Button("LOAD FG");
    Button clear = new Button("CLEAR");  
    Button key = new Button("KEY"); 
    Button loadbg = new Button("LOAD BG");

    // scrollbars for selecting the hue and +/- deviation of chroma key colour
    Scrollbar chromaHue = new Scrollbar(Scrollbar.HORIZONTAL,0,0,0,361);
    Scrollbar hueRange = new Scrollbar(Scrollbar.HORIZONTAL,0,0,0,181);

    // text fields to display the value of the selected hue
    TextField chromaVal = new TextField("0",3); // set field length to 3 chars
    TextField rangeVal = new TextField("0",3);

    // values of the scrollbars (initialise to zero)
    int chroma = 0, deviation = 0;

    // booleans to denote whether or not image is loaded or cleared
    boolean fgloaded = false, justloadedfg = false, bgloaded = false;
    boolean clearfg = false, clearkeyed = false, clearbg = false;
    boolean keyed = false; 

    mainPanel() { // constructor for mainPanel

      setLayout(new BorderLayout()); // use border layout for main panels

      // add major panels to frame
      add("North", topMain);
      add("Center", uselessPanel);
      add("South", bttmMain);

      // add image panels to top panel
      topMain.add(fgPanel); 
      topMain.add(keyedPanel);
      topMain.add(bgPanel);    

      // set the size of the panels
      topMain.setSize(mainWidth,imgHeight);
      fgPanel.setSize(imgWidth,imgHeight);
      keyedPanel.setSize(imgWidth,imgHeight);
      bgPanel.setSize(imgWidth,imgHeight);
      uselessPanel.setSize(mainWidth,140);
      bttmMain.setSize(mainWidth,20);

      // set the size of the canvasses
      fgCanvas.setSize(imgWidth,imgHeight);
      keyedCanvas.setSize(imgWidth,imgHeight);
      bgCanvas.setSize(imgWidth,imgHeight);

      // add canvasses to appropriate panels
      fgPanel.add(fgCanvas);
      bgPanel.add(bgCanvas);
      keyedPanel.add(keyedCanvas);

      // add AWT widgets to bottom panel            
      bttmMain.add(chromaVal);// text field to display hue of chroma key colour
      bttmMain.add(chromaHue);// scrollbar to select hue of chroma key colour
      bttmMain.add(hueRange); // scrollbar to select +/- devaition of hue
      bttmMain.add(rangeVal); // text field to display +/- deviation of hue
      bttmMain.add(loadfg);   // button to load foreground image
      bttmMain.add(clear);    // button to clear canvasses
      bttmMain.add(key);      // button to activate chroma-keying
      bttmMain.add(loadbg);   // button to load background image

      // add action listeners to send events from above widgets to mainPanel
      chromaVal.addActionListener(new chromaTFAListener());
      chromaHue.addAdjustmentListener(new chromaADJListener()); 
      hueRange.addAdjustmentListener(new rangeADJListener());            
      rangeVal.addActionListener(new rangeTFAListener());  
      loadfg.addActionListener(new loadfgBAListener());
      clear.addActionListener(new clearBAListener());
      key.addActionListener(new keyBAListener());
      loadbg.addActionListener(new loadbgBAListener());                        

    } // end of constructor for mainPanel


    // return maximum of three doubles
    public double getMax(double a, double b, double c) {

      double temp = Math.max(a,b);
      return Math.max(temp,c);

    } // end of getMax


    // return minimum value of three doubles
    public double getMin(double a, double b, double c) {

      double temp = Math.min(a,b);
      return Math.min(temp,c);

    } // end of getMin


    // return the hue of a pixel as an integer value
    public int getH(int pixel) {

      // delcare vars
      double hue = 0, sat = 0, value = 0, maximum = 0, minimum = 0, diff = 0;

      // split RGB value into seperate doubles (one for each colour)
      // int alpha = (pixel & 0xff000000)>>24; (ignore alpha)
      double red = (pixel & 0x00ff0000)>>16;
      double green = (pixel & 0x0000ff00)>>8;
      double blue = (pixel & 0x000000ff);

      // determine the largest value out of the colours
      maximum = getMax(red, green, blue);

      // determine the smallest value out of the colours
      minimum = getMin(red, green, blue);

      // set value
      value = maximum;

      // set saturation, which is zero if r,g,b are all 0
      sat = (maximum != 0) ? ((maximum - minimum) / maximum):0;

      // when saturation is 0, hue is undefined
      if (sat == 0) {

	return 0;

      } // end if

      // determine difference between max & min
      diff = maximum - minimum;

      // calculate hue based on symmetry of hexagon (see RGB2HSV.PDF)
      if ( maximum == red ) {     // colour between magenta and yellow

	hue = ((green - blue) / diff) * 60;
             
      } else {

	if ( maximum == green ) { // colour between yellow and cyan

	  hue = (2 + ((blue - red) / diff)) * 60;

	} else {                  // colour between cyan and magenta

	  hue = (4 + ((red - green) / diff)) * 60;

	} // end if

      } // end if

      if ( hue < 0 ) {

	hue += 360;               // if hue is negative add 360 degrees

      } // end if

      return (int)hue;            // return the hue of the pixel

    } // end of getH


    // ##############
    // DISPLAY IMAGES
    // ##############


    public class fgndCanvas extends Canvas {

      public void paint(Graphics g) {

	Insets in = getInsets();
	Dimension d = getSize();

	// translate origin to top of drawable area
	g.translate(in.left,in.right);

	// draw bounding rectangle
	g.drawRect(0,0,d.width-1,d.height-1);

	// if loadfg pressed, display the image
	if ( fgloaded ) {

	  g.drawImage(fgImage2,0,0,this);

	} // end if

      } // end of paint method

      public void update(Graphics g) {

	// only wipe canvas if absolutely necessary
	if ( clearfg ) {

	  g.clearRect(0,0,imgWidth,imgHeight);
	  clearfg = false;

	} // end if

	paint(g);

      } // end of update method

    } // end of class fgndCanvas


    public class keyedCanvas extends Canvas {

      public void paint(Graphics g) {

	Insets in = getInsets();
	Dimension d = getSize();

	// translate origin to top of drawable area
	g.translate(in.left,in.right);

	// draw bounding rectangle
	g.drawRect(0,0,d.width-1,d.height-1);

	// if foreground just loaded, display it
	if ( justloadedfg ) {

	  g.drawImage(fgImage2,0,0,this);

	} // end if

	// if the image has been chroma-keyed, display it
	if ( keyed ) {

	  g.drawImage(keyedImage,0,0,this);

	} // end if

      } // end of paint method

      public void update(Graphics g) {

	// only wipe canvas if absolutely necessary
	if ( clearkeyed ) {

	    g.clearRect(0,0,imgWidth,imgHeight);
	    clearkeyed = false;

	} // end if

	paint(g);

      } // end of update method

    } // end of class keyedCanvas


    public class bkgndCanvas extends Canvas {

      public void paint(Graphics g) {

	Insets in = getInsets();
	Dimension d = getSize();

	// translate origin to top of drawable area
	g.translate(in.left,in.right);

	// draw bounding rectangle
	g.drawRect(0,0,d.width-1,d.height-1);

	// if loadfg pressed, display the image
	if ( bgloaded ) {

	  g.drawImage(bgImage2,0,0,this);
                 
	} // end if

      } // end of paint method

      public void update(Graphics g) {

	// only wipe canvas if absolutely necessary
	if ( clearbg ) {

	  g.clearRect(0,0,imgWidth,imgHeight);
	  clearbg = false; // all panels now clear

	} // end if

	paint(g);

      } // end of update method

    } // end of class bkgndCanvas


    // ##############
    // PROCESS EVENTS
    // ##############


    // update variables and scrollbars when values entered in text fields
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // get values from hue selection text field & update values
    public class chromaTFAListener implements ActionListener {

      public void actionPerformed(ActionEvent e) {

        // parse integer
	try {
	  int parsed1 = Integer.parseInt(chromaVal.getText());

	  // ensure values are valid (0<=parsed1<=360)
	  if ( parsed1 >= 0 && parsed1 <= 360 ) {

	    chroma = parsed1;                        // update variable
	    chromaHue.setValue(parsed1);             // update scrollbar

	  } else {

	    throw new NumberFormatException();       // invalid range

	  } // end if
	}
	catch (NumberFormatException ex) {

	  chromaVal.setText(String.valueOf(chroma)); // reset text field

	} // end of try/catch block

      } // end of actionPerformed

    } // end of class chromaTFAListener


    // get values from +/- deviation text field & update values
    public class rangeTFAListener implements ActionListener {

      public void actionPerformed(ActionEvent e) {

        // parse integer
	try {
	  int parsed2 = Integer.parseInt(rangeVal.getText());

	  // ensure values are valid (0<=parsed2<=180)
	  if ( parsed2 >= 0 && parsed2 <= 180 ) {

	    deviation = parsed2;                       // update variable
	    hueRange.setValue(parsed2);                // update scrollbar

	  } else {

	    throw new NumberFormatException();         // invalid range

	  } // end if
	}
	catch (NumberFormatException ex) {

	  rangeVal.setText(String.valueOf(deviation)); // reset text field

	} // end of try/catch block

      } // end of actionPerformed

    } // end of class rangeTFAListener


    // update variables as scrollbars are adjusted
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // get values from hue selection scrollbar & update variables
    public class chromaADJListener implements AdjustmentListener {

      public void adjustmentValueChanged(AdjustmentEvent e) {

	int val=(chromaHue.getValue());         // obtain value from scrollbar

	chroma = val;                           // update stored value
	chromaVal.setText(String.valueOf(val)); // update text field

      } // end of adjustmentValueChanged

    } // end of class chromaADJListener


    // get values from +/- deviation scrollbar & update variables
    public class rangeADJListener implements AdjustmentListener {

      public void adjustmentValueChanged(AdjustmentEvent e) {

	int val=(hueRange.getValue());         // obtain value from scrollbar

	deviation = val;                       // update stored value
	rangeVal.setText(String.valueOf(val)); // update text field

      } // end of adjustmentValueChanged

    } // end of class rangeADJListener


    // respond to button presses
    // ~~~~~~~~~~~~~~~~~~~~~~~~~

    // load foreground image
    public class loadfgBAListener implements ActionListener {

      public void actionPerformed(ActionEvent e) {

	try {

	  // grab pixels and refresh display
	  fgPg.grabPixels();

	}
	catch (InterruptedException ex) {

	  System.err.println("Pixel Grab Interrupted!!");
	  ex.printStackTrace();

	} // end of try/catch block

	fgImage2 = createImage(new MemoryImageSource(imgWidth,imgHeight,fgPixels,0,imgWidth));
	fgloaded = true;     // flag image as loaded
	justloadedfg = true; // flag keyed panel for update
	keyed = false;       // discard old keyed image, use new fgImage
	fgCanvas.repaint();  // update display
	keyedCanvas.repaint();

      } // end of actionPerformed

    } // end of class loadfgBAListener


    // return to original state
    public class clearBAListener implements ActionListener {

      public void actionPerformed(ActionEvent e) {

	fgloaded = false;   // flag image as unloaded
	bgloaded = false;
	keyed = false;
	justloadedfg = false;
	clearfg = true;     // flag image as cleared
	clearkeyed = true;
	clearbg = true;
	fgCanvas.repaint(); // update display
	keyedCanvas.repaint();
	bgCanvas.repaint();

      } // end of actionPerformed

    } // end of class clearBAListener


    // perform chroma-keying operation
    public class keyBAListener implements ActionListener {

      public void actionPerformed(ActionEvent e) {

	if ( fgloaded && bgloaded ) { // only key if images are loaded

	  // loop-invariant expressions to determine the minimum/maximum hue
	  int maxHue = chroma + deviation;
	  int minHue = chroma - deviation;

	  // chroma-key the image
	  for (int y = 0; y < imgHeight; y++ ) {
	    for (int x = 0; x < imgWidth; x++ ) {

	      int index = y*imgHeight+x; // calculate index position (1D array)
	      int currentHue=getH(fgPixels[index]); // get hue of current pixel

              // if hue of fgPixel in specified range, replace with bgPixel
              // consider case where hue - deviation < 0
              if (deviation > chroma) {

                if (currentHue <= maxHue || currentHue >= (minHue + 360)) {

                  keyedPixels[index] = bgPixels[index];

                } else {

                  keyedPixels[index] = fgPixels[index];

                } // end if

              } else { 

                // consider case where hue + deviation > 360
                if (maxHue > 360) {

                  if (currentHue <= (maxHue - 360) || currentHue >= minHue) {

                    keyedPixels[index] = bgPixels[index];

                  } else {

                    keyedPixels[index] = fgPixels[index];

                  } // end if

                } else {

                  // standard case
	          if (currentHue >= minHue && currentHue <= maxHue) {

                    keyedPixels[index] = bgPixels[index];

	          } else {

                    keyedPixels[index] = fgPixels[index];

                  } // end if
                } // end if
              } // end if
	    } // end for
	  } // end for

	  keyedImage = createImage(new MemoryImageSource(imgWidth,imgHeight,keyedPixels,0,imgWidth));
	  justloadedfg = false;  // We no longer want to display fgImage,
	  keyed = true;          // but the new keyed image instead.
	  keyedCanvas.repaint(); // update display

	} // end if

      } // end of actionPerformed

    } // end of class keyBAListener


    // load background image
    public class loadbgBAListener implements ActionListener {

      public void actionPerformed(ActionEvent e) {

	try {

	  // grab pixels and refresh display
	  bgPg.grabPixels();

	}
	catch (InterruptedException ex) {

	  System.err.println("Pixel Grab Interrupted!!");
	  ex.printStackTrace();

	} // end of try/catch block

	bgImage2 = createImage(new MemoryImageSource(imgWidth,imgHeight,bgPixels,0,imgWidth));
	bgloaded = true;    // flag image as loaded
	bgCanvas.repaint(); // update display

      } // end of actionPerformed

    } // end of class loadbgBAListener

  } // end of class mainPanel

} // end of class ajt97c

