content: /*
   *    This program is free software; you can redistribute it and/or modify
   *    it under the terms of the GNU General Public License as published by
   *    the Free Software Foundation; either version 2 of the License, or
   *    (at your option) any later version.
   *
   *    This program is distributed in the hope that it will be useful,
   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   *    GNU General Public License for more details.
   *
   *    You should have received a copy of the GNU General Public License
   *    along with this program; if not, write to the Free Software
   *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
   */
  
  /*
   *   BoundaryPanel.java
   *   Copyright (C) 2002 University of Waikato, Hamilton, New Zealand
   *
   */
  
  package weka.gui.boundaryvisualizer;
  
  import weka.classifiers.Classifier;
  import weka.classifiers.AbstractClassifier;
  import weka.core.FastVector;
  import weka.core.Instance;
  import weka.core.DenseInstance;
  import weka.core.Instances;
  import weka.core.Utils;
  import weka.gui.visualize.JPEGWriter;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.Image;
  import java.awt.RenderingHints;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.image.BufferedImage;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.ObjectInputStream;
  import java.util.Iterator;
  import java.util.Locale;
  import java.util.Random;
  import java.util.Vector;
  
  import javax.imageio.IIOImage;
  import javax.imageio.ImageIO;
  import javax.imageio.ImageWriteParam;
  import javax.imageio.ImageWriter;
  import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
  import javax.imageio.stream.ImageOutputStream;
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.ToolTipManager;
  
  /**
   * BoundaryPanel. A class to handle the plotting operations
   * associated with generating a 2D picture of a classifier's decision
   * boundaries.
   *
   * @author <a href="mailto:mhall@cs.waikato.ac.nz">Mark Hall</a>
   * @version $Revision: 5987 $
   * @since 1.0
   * @see JPanel
   */
  public class BoundaryPanel
    extends JPanel {
  
    /** for serialization */
    private static final long serialVersionUID = -8499445518744770458L;
    
    /** default colours for classes */
    public static final Color [] DEFAULT_COLORS = {
      Color.red,
      Color.green,
      Color.blue,
      new Color(0, 255, 255), // cyan
      new Color(255, 0, 255), // pink
      new Color(255, 255, 0), // yellow
      new Color(255, 255, 255), //white
      new Color(0, 0, 0)};
      
    /** The distance we can click away from a point in the GUI and still remove it. */
    public static final double REMOVE_POINT_RADIUS = 7.0;
  
    protected FastVector m_Colors = new FastVector();
  
    /** training data */
    protected Instances m_trainingData;
  
    /** distribution classifier to use */
    protected Classifier m_classifier;
  
    /** data generator to use */
    protected DataGenerator m_dataGenerator;
  
    /** index of the class attribute */
    private int m_classIndex = -1;
  
    // attributes for visualizing on
    protected int m_xAttribute;
    protected int m_yAttribute;
  
    // min, max and ranges of these attributes
    protected double m_minX;
    protected double m_minY;
    protected double m_maxX;
    protected double m_maxY;
    private double m_rangeX;
    private double m_rangeY;
  
    // pixel width and height in terms of attribute values
    protected double m_pixHeight;
    protected double m_pixWidth;
  
    /** used for offscreen drawing */
    protected Image m_osi = null;
  
    // width and height of the display area
    protected int m_panelWidth;
    protected int m_panelHeight;
  
    // number of samples to take from each region in the fixed dimensions
    protected int m_numOfSamplesPerRegion = 2;
  
    // number of samples per kernel = base ^ (# non-fixed dimensions)
    protected int m_numOfSamplesPerGenerator;
    protected double m_samplesBase = 2.0;
  
    /** listeners to be notified when plot is complete */
    private Vector m_listeners = new Vector();
  
    /**
     * small inner class for rendering the bitmap on to
     */
    private class PlotPanel
      extends JPanel {
  
      /** for serialization */
      private static final long serialVersionUID = 743629498352235060L;
      
      public PlotPanel() {
        this.setToolTipText("");
      }
      
      public void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (m_osi != null) {
  	g.drawImage(m_osi,0,0,this);
        }
      }
      
      public String getToolTipText(MouseEvent event) {
        if (m_probabilityCache == null) {
  	return null;
        }
        
        if (m_probabilityCache[event.getY()][event.getX()] == null) {
  	return null;
        }
        
        String pVec = "(X: "
  	+Utils.doubleToString(convertFromPanelX((double)event.getX()), 2)
  	+" Y: "
  	+Utils.doubleToString(convertFromPanelY((double)event.getY()), 2)+") ";
        // construct a string holding the probability vector
        for (int i = 0; i < m_trainingData.classAttribute().numValues(); i++) {
  	pVec += 
  	  Utils.
  	  doubleToString(m_probabilityCache[event.getY()][event.getX()][i],
  			 3)+" ";
        }
        return pVec;
      }
    }
  
    /** the actual plotting area */
    private PlotPanel m_plotPanel = new PlotPanel();
  
    /** thread for running the plotting operation in */
    private Thread m_plotThread = null;
  
    /** Stop the plotting thread */
    protected boolean m_stopPlotting = false;
  
    /** Stop any replotting threads */
    protected boolean m_stopReplotting = false;
  
    // Used by replotting threads to pause and resume the main plot thread
    private Double m_dummy = new Double(1.0);
    private boolean m_pausePlotting = false;
    /** what size of tile is currently being plotted */
    private int m_size = 1;
    /** is the main plot thread performing the initial coarse tiling */
    private boolean m_initialTiling;
  
    /** A random number generator  */
    private Random m_random = null;
  
    /** cache of probabilities for fast replotting */
    protected double [][][] m_probabilityCache;
  
    /** plot the training data */
    protected boolean m_plotTrainingData = true;
  
    /**
     * Creates a new <code>BoundaryPanel</code> instance.
     *
     * @param panelWidth the width in pixels of the panel
     * @param panelHeight the height in pixels of the panel
     */
    public BoundaryPanel(int panelWidth, int panelHeight) {
      ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
      m_panelWidth = panelWidth;
      m_panelHeight = panelHeight;
      setLayout(new BorderLayout());
      m_plotPanel.setMinimumSize(new Dimension(m_panelWidth, m_panelHeight));
      m_plotPanel.setPreferredSize(new Dimension(m_panelWidth, m_panelHeight));
      m_plotPanel.setMaximumSize(new Dimension(m_panelWidth, m_panelHeight));
      add(m_plotPanel, BorderLayout.CENTER);
      setPreferredSize(m_plotPanel.getPreferredSize());
      setMaximumSize(m_plotPanel.getMaximumSize());
      setMinimumSize(m_plotPanel.getMinimumSize());
  
      m_random = new Random(1);
      for (int i = 0; i < DEFAULT_COLORS.length; i++) {
        m_Colors.addElement(new Color(DEFAULT_COLORS[i].getRed(),
  				    DEFAULT_COLORS[i].getGreen(),
  				    DEFAULT_COLORS[i].getBlue()));
      }
      m_probabilityCache = new double[m_panelHeight][m_panelWidth][];
      
    }
  
    /**
     * Set the number of points to uniformly sample from a region (fixed
     * dimensions).
     *
     * @param num an <code>int</code> value
     */
    public void setNumSamplesPerRegion(int num) {
      m_numOfSamplesPerRegion = num;
    }
  
    /**
     * Get the number of points to sample from a region (fixed dimensions).
     *
     * @return an <code>int</code> value
     */
    public int getNumSamplesPerRegion() {
      return m_numOfSamplesPerRegion;
    }
  
    /**
     * Set the base for computing the number of samples to obtain from each
     * generator. number of samples = base ^ (# non fixed dimensions)
     *
     * @param ksb a <code>double</code> value
     */
    public void setGeneratorSamplesBase(double ksb) {
      m_samplesBase = ksb;
    }
  
    /**
     * Get the base used for computing the number of samples to obtain from
     * each generator
     *
     * @return a <code>double</code> value
     */
    public double getGeneratorSamplesBase() {
      return m_samplesBase;
    }
  
    /**
     * Set up the off screen bitmap for rendering to
     */
    protected void initialize() {
      int iwidth = m_plotPanel.getWidth();
      int iheight = m_plotPanel.getHeight();
      //    System.err.println(iwidth+" "+iheight);
      m_osi = m_plotPanel.createImage(iwidth, iheight);
      Graphics m = m_osi.getGraphics();
      m.fillRect(0,0,iwidth,iheight);
    }
  
    /**
     * Stop the plotting thread
     */
    public void stopPlotting() {
      m_stopPlotting = true;
      try {
      	m_plotThread.join(100);
      } catch (Exception e){};
    }
  
    /** Set up the bounds of our graphic based by finding the smallest reasonable
        area in the instance space to surround our data points.
    */
    public void computeMinMaxAtts() {
      m_minX = Double.MAX_VALUE;
      m_minY = Double.MAX_VALUE;
      m_maxX = Double.MIN_VALUE;
      m_maxY = Double.MIN_VALUE;
      
      boolean allPointsLessThanOne = true;
      
      if (m_trainingData.numInstances() == 0) {
        m_minX = m_minY = 0.0;
        m_maxX = m_maxY = 1.0;
      }
      else
      {
  	for (int i = 0; i < m_trainingData.numInstances(); i++) {
  		Instance inst = m_trainingData.instance(i);
  		double x = inst.value(m_xAttribute);
  		double y = inst.value(m_yAttribute);
  		if (!Utils.isMissingValue(x) && !Utils.isMissingValue(y)) {
  			if (x < m_minX) {
  			m_minX = x;
  			}
  			if (x > m_maxX) {
  			m_maxX = x;
  			}
  		
  			if (y < m_minY) {
  			m_minY = y;
  			}
  			if (y > m_maxY) {
  			m_maxY = y;
  			}
  			if (x > 1.0 || y > 1.0)
  				allPointsLessThanOne = false;
  		}
  	}
      }
      
      if (m_minX == m_maxX)
      	m_minX = 0;
      if (m_minY == m_maxY)
      	m_minY = 0;
      if (m_minX == Double.MAX_VALUE)
      	m_minX = 0;
      if (m_minY == Double.MAX_VALUE)
      	m_minY = 0;
      if (m_maxX == Double.MIN_VALUE)
      	m_maxX = 1;
      if (m_maxY == Double.MIN_VALUE)
      	m_maxY = 1;
      if (allPointsLessThanOne) {
      	m_minX = m_minY = 0.0;
      	m_maxX = m_maxY = 1.0;
      }
      
      
      
      m_rangeX = (m_maxX - m_minX);
      m_rangeY = (m_maxY - m_minY);
      
      m_pixWidth = m_rangeX / (double)m_panelWidth;
      m_pixHeight = m_rangeY / (double) m_panelHeight;
    }
  
    /**
     * Return a random x attribute value contained within
     * the pix'th horizontal pixel
     *
     * @param pix the horizontal pixel number
     * @return a value in attribute space
     */
    private double getRandomX(int pix) {
  
      double minPix =  m_minX + (pix * m_pixWidth);
  
      return minPix + m_random.nextDouble() * m_pixWidth;
    }
  
    /**
     * Return a random y attribute value contained within
     * the pix'th vertical pixel
     *
     * @param pix the vertical pixel number
     * @return a value in attribute space
     */
    private double getRandomY(int pix) {
      
      double minPix = m_minY + (pix * m_pixHeight);
      
      return minPix +  m_random.nextDouble() * m_pixHeight;
    }
    
    /**
     * Start the plotting thread
     *
     * @exception Exception if an error occurs
     */
    public void start() throws Exception {
      m_numOfSamplesPerGenerator = 
        (int)Math.pow(m_samplesBase, m_trainingData.numAttributes()-3);
  
      m_stopReplotting = true;
      if (m_trainingData == null) {
        throw new Exception("No training data set (BoundaryPanel)");
      }
      if (m_classifier == null) {
        throw new Exception("No classifier set (BoundaryPanel)");
      }
      if (m_dataGenerator == null) {
        throw new Exception("No data generator set (BoundaryPanel)");
      }
      if (m_trainingData.attribute(m_xAttribute).isNominal() || 
  	m_trainingData.attribute(m_yAttribute).isNominal()) {
        throw new Exception("Visualization dimensions must be numeric "
  			  +"(BoundaryPanel)");
      }
      
      computeMinMaxAtts();
      
      startPlotThread();
      /*if (m_plotThread == null) {
        m_plotThread = new PlotThread();
        m_plotThread.setPriority(Thread.MIN_PRIORITY);
        m_plotThread.start();
      }*/
    }
    
    // Thread for main plotting operation
    protected class PlotThread extends Thread {
      double [] m_weightingAttsValues;
      boolean [] m_attsToWeightOn;
      double [] m_vals;
      double [] m_dist;
      Instance m_predInst;
      public void run() {
  
        m_stopPlotting = false;
        try {
          initialize();
          repaint();
          
          // train the classifier
          m_probabilityCache = new double[m_panelHeight][m_panelWidth][];
          m_classifier.buildClassifier(m_trainingData);
          
          // build DataGenerator
          m_attsToWeightOn = new boolean[m_trainingData.numAttributes()];
          m_attsToWeightOn[m_xAttribute] = true;
          m_attsToWeightOn[m_yAttribute] = true;
  	      
          m_dataGenerator.setWeightingDimensions(m_attsToWeightOn);
  	      
          m_dataGenerator.buildGenerator(m_trainingData);
  
          // generate samples
          m_weightingAttsValues = new double [m_attsToWeightOn.length];
          m_vals = new double[m_trainingData.numAttributes()];
          m_predInst = new DenseInstance(1.0, m_vals);
          m_predInst.setDataset(m_trainingData);
  
  	
          m_size = 1 << 4;  // Current sample region size
  	
  	m_initialTiling = true;
          // Display the initial coarse image tiling.
        abortInitial:
          for (int i = 0; i <= m_panelHeight; i += m_size) {   
            for (int j = 0; j <= m_panelWidth; j += m_size) {   
  	    if (m_stopPlotting) {
  	      break abortInitial;
  	    }
  	    if (m_pausePlotting) {
  	      synchronized (m_dummy) {
  		try {
  		  m_dummy.wait();
  		} catch (InterruptedException ex) {
  		  m_pausePlotting = false;
  		}
  	      }
  	    }
              plotPoint(j, i, m_size, m_size, 
  		      calculateRegionProbs(j, i), (j == 0));
            }
          }
  	if (!m_stopPlotting) {
  	  m_initialTiling = false;
  	}
          
          // Sampling and gridding loop
          int size2 = m_size / 2;
          abortPlot: 
          while (m_size > 1) { // Subdivide down to the pixel level
            for (int i = 0; i <= m_panelHeight; i += m_size) {
              for (int j = 0; j <= m_panelWidth; j += m_size) {
                if (m_stopPlotting) {
                  break abortPlot;
                }
  	      if (m_pausePlotting) {
  		synchronized (m_dummy) {
  		  try {
  		    m_dummy.wait();
  		  } catch (InterruptedException ex) {
  		    m_pausePlotting = false;
  		  }
  		}
  	      }
                boolean update = (j == 0 && i % 2 == 0);
                // Draw the three new subpixel regions
                plotPoint(j, i + size2, size2, size2, 
  			calculateRegionProbs(j, i + size2), update);
                plotPoint(j + size2, i + size2, size2, size2, 
  			calculateRegionProbs(j + size2, i + size2), update);
                plotPoint(j + size2, i, size2, size2, 
  			calculateRegionProbs(j + size2, i), update);
              }
            }
            // The new region edge length is half the old edge length
            m_size = size2;
            size2 = size2 / 2;
          }
  	update();
  	
  
  	/*
          // Old method without sampling.
          abortPlot: 
          for (int i = 0; i < m_panelHeight; i++) {
            for (int j = 0; j < m_panelWidth; j++) {
              if (m_stopPlotting) {
                break abortPlot;
              }
              plotPoint(j, i, calculateRegionProbs(j, i), (j == 0));
            }
          }
          */
  
  
          if (m_plotTrainingData) {
            plotTrainingData();
          }
  	      
        } catch (Exception ex) {
          ex.printStackTrace();
  	JOptionPane.showMessageDialog(null,"Error while plotting: \"" + ex.getMessage() + "\"");
        } finally {
          m_plotThread = null;
          // notify any listeners that we are finished
          Vector l;
          ActionEvent e = new ActionEvent(this, 0, "");
          synchronized(this) {
            l = (Vector)m_listeners.clone();
          }
          for (int i = 0; i < l.size(); i++) {
            ActionListener al = (ActionListener)l.elementAt(i);
            al.actionPerformed(e);
          }
        }
      }
      
      private double [] calculateRegionProbs(int j, int i) throws Exception {
        double [] sumOfProbsForRegion = 
  	new double [m_trainingData.classAttribute().numValues()];
  
        for (int u = 0; u < m_numOfSamplesPerRegion; u++) {
        
          double [] sumOfProbsForLocation = 
  	  new double [m_trainingData.classAttribute().numValues()];
        
          m_weightingAttsValues[m_xAttribute] = getRandomX(j);
          m_weightingAttsValues[m_yAttribute] = getRandomY(m_panelHeight-i-1);
        
          m_dataGenerator.setWeightingValues(m_weightingAttsValues);
        
          double [] weights = m_dataGenerator.getWeights();
          double sumOfWeights = Utils.sum(weights);
          int [] indices = Utils.sort(weights);
        
          // Prune 1% of weight mass
          int [] newIndices = new int[indices.length];
          double sumSoFar = 0; 
          double criticalMass = 0.99 * sumOfWeights;
          int index = weights.length - 1; int counter = 0;
          for (int z = weights.length - 1; z >= 0; z--) {
            newIndices[index--] = indices[z];
            sumSoFar += weights[indices[z]];
            counter++;
            if (sumSoFar > criticalMass) {
              break;
            }
          }
          indices = new int[counter];
          System.arraycopy(newIndices, index + 1, indices, 0, counter);
        
          for (int z = 0; z < m_numOfSamplesPerGenerator; z++) {
          
            m_dataGenerator.setWeightingValues(m_weightingAttsValues);
            double [][] values = m_dataGenerator.generateInstances(indices);
          
            for (int q = 0; q < values.length; q++) {
              if (values[q] != null) {
                System.arraycopy(values[q], 0, m_vals, 0, m_vals.length);
                m_vals[m_xAttribute] = m_weightingAttsValues[m_xAttribute];
                m_vals[m_yAttribute] = m_weightingAttsValues[m_yAttribute];
              
                // classify the instance
                m_dist = m_classifier.distributionForInstance(m_predInst);
                for (int k = 0; k < sumOfProbsForLocation.length; k++) {
                  sumOfProbsForLocation[k] += (m_dist[k] * weights[q]); 
                }
              }
            }
          }
        
          for (int k = 0; k < sumOfProbsForRegion.length; k++) {
            sumOfProbsForRegion[k] += (sumOfProbsForLocation[k] * 
  				     sumOfWeights); 
          }
        }
      
        // average
        Utils.normalize(sumOfProbsForRegion);
  
        // cache
        if ((i < m_panelHeight) && (j < m_panelWidth)) {
          m_probabilityCache[i][j] = new double[sumOfProbsForRegion.length];
          System.arraycopy(sumOfProbsForRegion, 0, m_probabilityCache[i][j], 
  			 0, sumOfProbsForRegion.length);
        }
  		
        return sumOfProbsForRegion;
      }
    }
  
    /** Render the training points on-screen.
    */
    public void plotTrainingData() {
      
      Graphics2D osg = (Graphics2D)m_osi.getGraphics();
      Graphics g = m_plotPanel.getGraphics();
      osg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                           RenderingHints.VALUE_ANTIALIAS_ON);
      double xval = 0; double yval = 0;
      
      for (int i = 0; i < m_trainingData.numInstances(); i++) {
        if (!m_trainingData.instance(i).isMissing(m_xAttribute) &&
            !m_trainingData.instance(i).isMissing(m_yAttribute)) {
  	  
  	if (m_trainingData.instance(i).isMissing(m_classIndex)) //jimmy.
  		continue; //don't plot if class is missing. TODO could we plot it differently instead?
  	
          xval = m_trainingData.instance(i).value(m_xAttribute);
          yval = m_trainingData.instance(i).value(m_yAttribute);
         
          int panelX = convertToPanelX(xval);
          int panelY = convertToPanelY(yval);
          Color ColorToPlotWith = 
            ((Color)m_Colors.elementAt((int)m_trainingData.instance(i).
                                       value(m_classIndex) % m_Colors.size()));
  	
          if (ColorToPlotWith.equals(Color.white)) {
            osg.setColor(Color.black);
          } else {
            osg.setColor(Color.white);
          }
          osg.fillOval(panelX-3, panelY-3, 7, 7);
          osg.setColor(ColorToPlotWith);
          osg.fillOval(panelX-2, panelY-2, 5, 5);
        }
      }
      g.drawImage(m_osi,0,0,m_plotPanel);
    }
    
    /** Convert an X coordinate from the instance space to the panel space.
    */
    private int convertToPanelX(double xval) {
      double temp = (xval - m_minX) / m_rangeX;
      temp = temp * (double) m_panelWidth;
  
      return (int)temp;
    }
  
    /** Convert a Y coordinate from the instance space to the panel space.
    */
    private int convertToPanelY(double yval) {
      double temp = (yval - m_minY) / m_rangeY;
      temp = temp * (double) m_panelHeight;
      temp = m_panelHeight - temp;
      
      return (int)temp;
    }
    
    /** Convert an X coordinate from the panel space to the instance space.
    */
    private double convertFromPanelX(double pX) {
      pX /= (double) m_panelWidth;
      pX *= m_rangeX;
      return pX + m_minX;
    }
  
    /** Convert a Y coordinate from the panel space to the instance space.
    */
    private double convertFromPanelY(double pY) {
      pY  = m_panelHeight - pY;
      pY /= (double) m_panelHeight;
      pY *= m_rangeY;
      
      return pY + m_minY;
    }
  
  
    /** Plot a point in our visualization on-screen.
    */
    protected  void plotPoint(int x, int y, double [] probs, boolean update) {
      plotPoint(x, y, 1, 1, probs, update);
    }
    
    /** Plot a point in our visualization on-screen.
    */
    private void plotPoint(int x, int y, int width, int height, 
  			 double [] probs, boolean update) {
  
      // draw a progress line
      Graphics osg = m_osi.getGraphics();
      if (update) {
        osg.setXORMode(Color.white);
        osg.drawLine(0, y, m_panelWidth-1, y);
        update();
        osg.drawLine(0, y, m_panelWidth-1, y);
      }
  
      // plot the point
      osg.setPaintMode();
      float [] colVal = new float[3];
      
      float [] tempCols = new float[3];
      for (int k = 0; k < probs.length; k++) {
        Color curr = (Color)m_Colors.elementAt(k % m_Colors.size());
  
        curr.getRGBColorComponents(tempCols);
        for (int z = 0 ; z < 3; z++) {
          colVal[z] += probs[k] * tempCols[z];
        }
      }
  
      for (int z = 0; z < 3; z++) {
        if (colVal[z] < 0) {
  	colVal[z] = 0;
        } else if (colVal[z] > 1) {
  	colVal[z] = 1;
        }
      }
      
      osg.setColor(new Color(colVal[0], 
                             colVal[1], 
                             colVal[2]));
      osg.fillRect(x, y, width, height);
    }
    
    /** Update the rendered image.
    */
    private void update() {
      Graphics g = m_plotPanel.getGraphics();
      g.drawImage(m_osi, 0, 0, m_plotPanel);
    }
  
    /**
     * Set the training data to use
     *
     * @param trainingData the training data
     * @exception Exception if an error occurs
     */
    public void setTrainingData(Instances trainingData) throws Exception {
  
      m_trainingData = trainingData;
      if (m_trainingData.classIndex() < 0) {
        throw new Exception("No class attribute set (BoundaryPanel)");
      }
      m_classIndex = m_trainingData.classIndex();
    }
    
    /** Adds a training instance to the visualization dataset.
    */
    public void addTrainingInstance(Instance instance) {
    	
    	if (m_trainingData == null) {
  		//TODO
  		System.err.println("Trying to add to a null training set (BoundaryPanel)");
  	}
    	
    	m_trainingData.add(instance);
    }
  
    /**
     * Register a listener to be notified when plotting completes
     *
     * @param newListener the listener to add
     */
    public void addActionListener(ActionListener newListener) {
      m_listeners.add(newListener);
    }
    
    /**
     * Remove a listener
     *
     * @param removeListener the listener to remove
     */
    public void removeActionListener(ActionListener removeListener) {
      m_listeners.removeElement(removeListener);
    }
    
    /**
     * Set the classifier to use.
     *
     * @param classifier the classifier to use
     */
    public void setClassifier(Classifier classifier) {
      m_classifier = classifier;
    }
    
    /**
     * Set the data generator to use for generating new instances
     *
     * @param dataGenerator the data generator to use
     */
    public void setDataGenerator(DataGenerator dataGenerator) {
      m_dataGenerator = dataGenerator;
    }
    
    /**
     * Set the x attribute index
     *
     * @param xatt index of the attribute to use on the x axis
     * @exception Exception if an error occurs
     */
    public void setXAttribute(int xatt) throws Exception {
      if (m_trainingData == null) {
        throw new Exception("No training data set (BoundaryPanel)");
      }
      if (xatt < 0 || 
          xatt > m_trainingData.numAttributes()) {
        throw new Exception("X attribute out of range (BoundaryPanel)");
      }
      if (m_trainingData.attribute(xatt).isNominal()) {
        throw new Exception("Visualization dimensions must be numeric "
                            +"(BoundaryPanel)");
      }
      /*if (m_trainingData.numDistinctValues(xatt) < 2) {
        throw new Exception("Too few distinct values for X attribute "
                            +"(BoundaryPanel)");
      }*/ //removed by jimmy. TESTING!
      m_xAttribute = xatt;
    }
  
    /**
     * Set the y attribute index
     *
     * @param yatt index of the attribute to use on the y axis
     * @exception Exception if an error occurs
     */
    public void setYAttribute(int yatt) throws Exception {
      if (m_trainingData == null) {
        throw new Exception("No training data set (BoundaryPanel)");
      }
      if (yatt < 0 || 
          yatt > m_trainingData.numAttributes()) {
        throw new Exception("X attribute out of range (BoundaryPanel)");
      }
      if (m_trainingData.attribute(yatt).isNominal()) {
        throw new Exception("Visualization dimensions must be numeric "
                            +"(BoundaryPanel)");
      }
      /*if (m_trainingData.numDistinctValues(yatt) < 2) {
        throw new Exception("Too few distinct values for Y attribute "
                            +"(BoundaryPanel)");
      }*/ //removed by jimmy. TESTING!
      m_yAttribute = yatt;
    }
    
    /**
     * Set a vector of Color objects for the classes
     *
     * @param colors a <code>FastVector</code> value
     */
    public void setColors(FastVector colors) {
      synchronized (m_Colors) {
        m_Colors = colors;
      }
      //replot(); //commented by jimmy
      update(); //added by jimmy
    }
  
    /**
     * Set whether to superimpose the training data
     * plot
     *
     * @param pg a <code>boolean</code> value
     */
    public void setPlotTrainingData(boolean pg) {
      m_plotTrainingData = pg;
    }
  
    /**
     * Returns true if training data is to be superimposed
     *
     * @return a <code>boolean</code> value
     */
    public boolean getPlotTrainingData() {
      return m_plotTrainingData;
    }
    
    /**
     * Get the current vector of Color objects used for the classes
     *
     * @return a <code>FastVector</code> value
     */
    public FastVector getColors() {
      return m_Colors;
    }
    
    /**
     * Quickly replot the display using cached probability estimates
     */
    public void replot() {
      if (m_probabilityCache[0][0] == null) {
        return;
      }
      m_stopReplotting = true;
      m_pausePlotting = true;
      // wait 300 ms to give any other replot threads a chance to halt
      try {
        Thread.sleep(300);
      } catch (Exception ex) {}
  
      final Thread replotThread = new Thread() {
          public void run() {
            m_stopReplotting = false;
  	  int size2 = m_size / 2;
            finishedReplot: for (int i = 0; i < m_panelHeight; i += m_size) {
              for (int j = 0; j < m_panelWidth; j += m_size) {
                if (m_probabilityCache[i][j] == null || m_stopReplotting) {
                  break finishedReplot;
                }
  
  	      boolean update = (j == 0 && i % 2 == 0);
  	      if (i < m_panelHeight && j < m_panelWidth) {
  		// Draw the three new subpixel regions or single course tiling
  		if (m_initialTiling || m_size == 1) {
  		  if (m_probabilityCache[i][j] == null) {
  		    break finishedReplot;
  		  }
  		  plotPoint(j, i, m_size, m_size, 
  			    m_probabilityCache[i][j], update);
  		} else {
  		  if (m_probabilityCache[i+size2][j] == null) {
  		    break finishedReplot;
  		  }
  		  plotPoint(j, i + size2, size2, size2, 
  			    m_probabilityCache[i + size2][j], update);
  		  if (m_probabilityCache[i+size2][j+size2] == null) {
  		    break finishedReplot;
  		  }
  		  plotPoint(j + size2, i + size2, size2, size2, 
  			    m_probabilityCache[i + size2][j + size2], update);
  		  if (m_probabilityCache[i][j+size2] == null) {
  		    break finishedReplot;
  		  }
  		  plotPoint(j + size2, i, size2, size2, 
  			    m_probabilityCache[i + size2][j], update);
  		}
  	      }
  	    }
            }
  	  update();
            if (m_plotTrainingData) {
              plotTrainingData();
            }
  	  m_pausePlotting = false;
  	  if (!m_stopPlotting) {
  	    synchronized (m_dummy) {
  	      m_dummy.notifyAll();
  	    }
  	  }
          }
        };
      
      replotThread.start();      
    }
  
    protected void saveImage(String fileName) {
      BufferedImage	bi;
      Graphics2D 		gr2;
      ImageWriter 	writer;
      Iterator 		iter;
      ImageOutputStream 	ios;
      ImageWriteParam 	param;
  
      try {
        // render image
        bi  = new BufferedImage(m_panelWidth, m_panelHeight, BufferedImage.TYPE_INT_RGB);
        gr2 = bi.createGraphics();
        gr2.drawImage(m_osi, 0, 0, m_panelWidth, m_panelHeight, null);
  
        // get jpeg writer
        writer = null;
        iter   = ImageIO.getImageWritersByFormatName("jpg");
        if (iter.hasNext())
  	writer = (ImageWriter) iter.next();
        else
  	throw new Exception("No JPEG writer available!");
  
        // prepare output file
        ios = ImageIO.createImageOutputStream(new File(fileName));
        writer.setOutput(ios);
  
        // set the quality
        param = new JPEGImageWriteParam(Locale.getDefault());
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) ;
        param.setCompressionQuality(1.0f);
  
        // write the image
        writer.write(null, new IIOImage(bi, null, null), param);
  
        // cleanup
        ios.flush();
        writer.dispose();
        ios.close();    
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }
    
    /** Adds a training instance to our dataset, based on the coordinates of the mouse on the panel.
        This method sets the x and y attributes and the class (as defined by classAttIndex), and sets
        all other values as Missing.
     *  @param mouseX the x coordinate of the mouse, in pixels.
     *  @param mouseY the y coordinate of the mouse, in pixels.
     *  @param classAttIndex the index of the attribute that is currently selected as the class attribute.
     *  @param classValue the value to set the class to in our new point.
     */
    public void addTrainingInstanceFromMouseLocation(int mouseX, int mouseY, int classAttIndex, double classValue) {
    	//convert to coordinates in the training instance space.
  	double x = convertFromPanelX(mouseX);
  	double y = convertFromPanelY(mouseY);
  	
  	//build the training instance
  	Instance newInstance = new DenseInstance(m_trainingData.numAttributes());
  	for (int i = 0; i < newInstance.numAttributes(); i++) {
  		if (i == classAttIndex) {
  			newInstance.setValue(i,classValue);
  		}
  		else if (i == m_xAttribute)
  			newInstance.setValue(i,x);
  		else if (i == m_yAttribute)
  			newInstance.setValue(i,y);
  		else newInstance.setMissing(i);
  	}
  	
  	//add it to our data set.
  	addTrainingInstance(newInstance);
    }
    
    /** Deletes all training instances from our dataset.
    */
    public void removeAllInstances() {
    	if (m_trainingData != null)
  	{
    		m_trainingData.delete();
  		try { initialize();} catch (Exception e) {};
  	}
  	
    }
    
    /** Removes a single training instance from our dataset, if there is one that is close enough
        to the specified mouse location.
    */
    public void removeTrainingInstanceFromMouseLocation(int mouseX, int mouseY) {
    	
  	//convert to coordinates in the training instance space.
  	double x = convertFromPanelX(mouseX);
  	double y = convertFromPanelY(mouseY);
  	
  	int bestIndex = -1;
  	double bestDistanceBetween = Integer.MAX_VALUE;
  	
  	//find the closest point.
  	for (int i = 0; i < m_trainingData.numInstances(); i++) {
  		Instance current = m_trainingData.instance(i);
  		double distanceBetween = (current.value(m_xAttribute) - x) * (current.value(m_xAttribute) - x) + (current.value(m_yAttribute) - y) * (current.value(m_yAttribute) - y); // won't bother to sqrt, just used square values.
  		
  		if (distanceBetween < bestDistanceBetween)
  		{
  			bestIndex = i;
  			bestDistanceBetween = distanceBetween;
  		}
  	}
  	if (bestIndex == -1)
  		return;
  	Instance best = m_trainingData.instance(bestIndex);
  	double panelDistance = (convertToPanelX(best.value(m_xAttribute)) - mouseX) * (convertToPanelX(best.value(m_xAttribute)) - mouseX)
  		+ (convertToPanelY(best.value(m_yAttribute)) - mouseY) * (convertToPanelY(best.value(m_yAttribute)) - mouseY);
  	if (panelDistance < REMOVE_POINT_RADIUS * REMOVE_POINT_RADIUS) {//the best point is close enough. (using squared distances)
  		m_trainingData.delete(bestIndex);
  	}
    }
    
    /** Starts the plotting thread.  Will also create it if necessary.
    */
    public void startPlotThread() {
    	if (m_plotThread == null) { //jimmy
        		m_plotThread = new PlotThread();
        		m_plotThread.setPriority(Thread.MIN_PRIORITY);
        		m_plotThread.start();
      	}
    }
    
    /** Adds a mouse listener.
    */
    public void addMouseListener(MouseListener l) {
    	m_plotPanel.addMouseListener(l);
    }
    
    /** Gets the minimum x-coordinate bound, in training-instance units (not mouse coordinates).
    */
    public double getMinXBound() {
    	return m_minX;
    }
    
    /** Gets the minimum y-coordinate bound, in training-instance units (not mouse coordinates).
    */
    public double getMinYBound() {
    	return m_minY;
    }
    
    /** Gets the maximum x-coordinate bound, in training-instance units (not mouse coordinates).
    */
    public double getMaxXBound() {
    	return m_maxX;
    }
    
    /** Gets the maximum x-coordinate bound, in training-instance units (not mouse coordinates).
    */
    public double getMaxYBound() {
    	return m_maxY;
    }
  
    /**
     * Main method for testing this class
     *
     * @param args a <code>String[]</code> value
     */
    public static void main (String [] args) {
      try {
        if (args.length < 8) {
  	System.err.println("Usage : BoundaryPanel <dataset> "
  			   +"<class col> <xAtt> <yAtt> "
  			   +"<base> <# loc/pixel> <kernel bandwidth> "
  			   +"<display width> "
  			   +"<display height> <classifier "
  			   +"[classifier options]>");
  	System.exit(1);
        }
        final javax.swing.JFrame jf = 
  	new javax.swing.JFrame("Weka classification boundary visualizer");
        jf.getContentPane().setLayout(new BorderLayout());
  
        System.err.println("Loading instances from : "+args[0]);
        java.io.Reader r = new java.io.BufferedReader(
  			 new java.io.FileReader(args[0]));
        final Instances i = new Instances(r);
        i.setClassIndex(Integer.parseInt(args[1]));
  
        //      bv.setClassifier(new Logistic());
        final int xatt = Integer.parseInt(args[2]);
        final int yatt = Integer.parseInt(args[3]);
        int base = Integer.parseInt(args[4]);
        int loc = Integer.parseInt(args[5]);
  
        int bandWidth = Integer.parseInt(args[6]);
        int panelWidth = Integer.parseInt(args[7]);
        int panelHeight = Integer.parseInt(args[8]);
  
        final String classifierName = args[9];
        final BoundaryPanel bv = new BoundaryPanel(panelWidth,panelHeight);
        bv.addActionListener(new ActionListener() {
  	  public void actionPerformed(ActionEvent e) {
  	    String classifierNameNew = 
  	      classifierName.substring(classifierName.lastIndexOf('.')+1, 
  				       classifierName.length());
  	    bv.saveImage(classifierNameNew+"_"+i.relationName()
  			 +"_X"+xatt+"_Y"+yatt+".jpg");
  	  }
  	});
  
        jf.getContentPane().add(bv, BorderLayout.CENTER);
        jf.setSize(bv.getMinimumSize());
        //      jf.setSize(200,200);
        jf.addWindowListener(new java.awt.event.WindowAdapter() {
  	  public void windowClosing(java.awt.event.WindowEvent e) {
  	    jf.dispose();
  	    System.exit(0);
  	  }
  	});
  
        jf.pack();
        jf.setVisible(true);
        //      bv.initialize();
        bv.repaint();
        
  
        String [] argsR = null;
        if (args.length > 10) {
  	argsR = new String [args.length-10];
  	for (int j = 10; j < args.length; j++) {
  	  argsR[j-10] = args[j];
  	}
        }
        Classifier c = AbstractClassifier.forName(args[9], argsR);
        KDDataGenerator dataGen = new KDDataGenerator();
        dataGen.setKernelBandwidth(bandWidth);
        bv.setDataGenerator(dataGen);
        bv.setNumSamplesPerRegion(loc);
        bv.setGeneratorSamplesBase(base);
        bv.setClassifier(c);
        bv.setTrainingData(i);
        bv.setXAttribute(xatt);
        bv.setYAttribute(yatt);
  
        try {
  	// try and load a color map if one exists
  	FileInputStream fis = new FileInputStream("colors.ser");
  	ObjectInputStream ois = new ObjectInputStream(fis);
  	FastVector colors = (FastVector)ois.readObject();
  	bv.setColors(colors);	
        } catch (Exception ex) {
  	System.err.println("No color map file");
        }
        bv.start();
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }
  }
  
  