Coverage Report - org.jfree.chart.plot.SOMPlot
 
Classes in this File Line Coverage Branch Coverage Complexity
SOMPlot
100%
275/275
100%
38/38
2.586
 
 1  
 /* ======================================================================= 
 2  
  * A visualisation library extension for JFreeChart. Please see JFreeChart
 3  
  * for further information.
 4  
  * =======================================================================
 5  
  * Copyright (C) 2006  University of Helsinki, Department of Computer Science
 6  
  *
 7  
  * This library is free software; you can redistribute it and/or
 8  
  * modify it under the terms of the GNU Lesser General Public
 9  
  * License as published by the Free Software Foundation; either
 10  
  * version 2.1 of the License, or (at your option) any later version.
 11  
  *
 12  
  * This library is distributed in the hope that it will be useful,
 13  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 15  
  * Lesser General Public License for more details.
 16  
  *
 17  
  * You should have received a copy of the GNU Lesser General Public
 18  
  * License along with this library; if not, write to the Free Software
 19  
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 20  
  * -----------------------------
 21  
  * Contact:  ohtu@cs.helsinki.fi
 22  
  * -----------------------------
 23  
  *
 24  
  */
 25  
 
 26  
 
 27  
 package org.jfree.chart.plot;
 28  
 
 29  
 import java.awt.AlphaComposite;
 30  
 import java.awt.BasicStroke;
 31  
 import java.awt.Color;
 32  
 import java.awt.Composite;
 33  
 import java.awt.Font;
 34  
 import java.awt.Graphics2D;
 35  
 import java.awt.GridBagConstraints;
 36  
 import java.awt.GridBagLayout;
 37  
 import java.awt.Insets;
 38  
 import java.awt.Paint;
 39  
 import java.awt.Polygon;
 40  
 import java.awt.Shape;
 41  
 import java.awt.Stroke;
 42  
 import java.io.IOException;
 43  
 import java.io.ObjectInputStream;
 44  
 import java.io.ObjectOutputStream;
 45  
 import java.io.Serializable;
 46  
 import java.util.Iterator;
 47  
 import java.util.LinkedList;
 48  
 import java.util.List;
 49  
 import java.util.ResourceBundle;
 50  
 import javax.swing.BorderFactory;
 51  
 import javax.swing.JLabel;
 52  
 import javax.swing.JPanel;
 53  
 import javax.swing.JSlider;
 54  
 import javax.swing.event.ChangeEvent;
 55  
 import javax.swing.event.ChangeListener;
 56  
 import org.jfree.chart.ChartMouseEvent;
 57  
 import org.jfree.chart.ChartMouseListener;
 58  
 import org.jfree.chart.entity.EntityCollection;
 59  
 import org.jfree.chart.entity.SOMItemEntity;
 60  
 import org.jfree.chart.event.PlotChangeEvent;
 61  
 import org.jfree.chart.labels.SOMToolTipGenerator;
 62  
 import org.jfree.data.general.DatasetUtilities;
 63  
 import org.jfree.data.som.SOMDataItem;
 64  
 import org.jfree.data.som.SOMDataset;
 65  
 import org.jfree.io.SerialUtilities;
 66  
 import org.jfree.text.G2TextMeasurer;
 67  
 import org.jfree.text.TextBlock;
 68  
 import org.jfree.text.TextBlockAnchor;
 69  
 import org.jfree.text.TextUtilities;
 70  
 import org.jfree.util.ObjectUtilities;
 71  
 
 72  
 
 73  
 /**
 74  
  * A plot that displays data in the form of a SOM-map. The plot uses
 75  
  * data from a {@link SOMDataset} -object.
 76  
  *
 77  
  * @author viski project.
 78  
  */
 79  
 public class SOMPlot extends Plot implements ChartMouseListener, ChangeListener, Cloneable, Serializable {
 80  
 
 81  
     /** The dataset. */
 82  
     private SOMDataset dataset;
 83  
 
 84  
     /** The tooltip generator. */
 85  
     private SOMToolTipGenerator toolTipGenerator;
 86  
     
 87  
     /** The cell color hue adjustment value. */
 88  
     private int colorHueAdjustment;
 89  
     
 90  
     /** The List of cells selected on-screen. */
 91  
     private List selectedCells;
 92  
     
 93  
     /** The cell color hue adjuster. */
 94  
     private JSlider colorHueSlider;
 95  
     
 96  
     /** The distance selector for 'select neighbors' */
 97  
     private JSlider distanceSlider;
 98  
     
 99  
     /** The cell information text is drawn with this font. */
 100  
     private Font descriptionFont;
 101  
 
 102  
     /** The default font used in the chart. */
 103  1
     private static final Font DEFAULT_DESCRIPTION_FONT = new Font("SansSerif", Font.PLAIN, 8);
 104  
 
 105  
     /** The default size of the cell description. */
 106  
     private static final float DESCRIPTION_WIDTH_RATIO = 0.9f;
 107  
 
 108  
     /** The number of empty pixels between cells.
 109  
      *  Use an odd number to achieve cell symmetry.
 110  
      */
 111  
     private static final int HORIZONTAL_GAP = 3;
 112  
     
 113  
     /** The number of empty pixels between cells */
 114  
     private static final int VERTICAL_GAP = 3;
 115  
     
 116  
     /** The resourceBundle for the localization. */
 117  1
     protected static ResourceBundle localizationResources 
 118  
         = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
 119  
     
 120  
     /**
 121  
      * Creates a new plot that will draw a SOM-map for the given dataset. Sets
 122  
      * the toolTipGenerator to null by default.
 123  
      *
 124  
      * @param dataset  the dataset.
 125  
      * @throws NullPointerException
 126  
      */
 127  27
     public SOMPlot(SOMDataset dataset) throws NullPointerException {
 128  27
         if (DatasetUtilities.isEmptyOrNull(dataset))
 129  1
             throw new NullPointerException("Dataset given to SOMPlot was null or empty.");
 130  
         
 131  26
         this.dataset = dataset;
 132  26
         this.toolTipGenerator = null;
 133  26
         this.colorHueAdjustment = 0;
 134  26
         this.selectedCells = new LinkedList();
 135  26
         this.distanceSlider = new JSlider(0,444,0);
 136  26
         this.colorHueSlider = new JSlider(0,359,this.colorHueAdjustment);
 137  26
         this.descriptionFont = DEFAULT_DESCRIPTION_FONT;
 138  26
     }
 139  
     
 140  
     /**
 141  
      * Returns the dataset of a plot.
 142  
      *
 143  
      * @return  The dataset.
 144  
      */
 145  
     public SOMDataset getDataset() {
 146  2
         return this.dataset;
 147  
     }
 148  
 
 149  
     /**
 150  
      * Sets the dataset of a plot.
 151  
      *
 152  
      * @param dataset   the dataset.
 153  
      * @throws NullPointerException
 154  
      */
 155  
     public void setDataset(SOMDataset dataset) throws NullPointerException {
 156  2
         if (dataset == null)
 157  1
             throw new NullPointerException("Dataset given to SOMPlot was null");
 158  1
         this.dataset = dataset;
 159  1
     }
 160  
 
 161  
     /** 
 162  
      * Returns the tooltip generator of a plot.
 163  
      *
 164  
      * @return  The tooltip generator.
 165  
      */
 166  
     public SOMToolTipGenerator getToolTipGenerator() {
 167  2
         return toolTipGenerator;
 168  
     }
 169  
 
 170  
     /**
 171  
      * Sets the tooltip generator of a plot.
 172  
      * Parameter value can be null.
 173  
      *
 174  
      * @param toolTipGenerator  the tooltip generator.
 175  
      */
 176  
     public void setToolTipGenerator(SOMToolTipGenerator toolTipGenerator) {
 177  
         // No null check.
 178  
         // Null value means that tooltips will not be displayed.
 179  8
         this.toolTipGenerator = toolTipGenerator;
 180  8
     }
 181  
 
 182  
     /**
 183  
      * Returns the name of the plot.
 184  
      * Thr name is used as the name of the SOM specific panel that can be 
 185  
      * opened from the context menu.
 186  
      *
 187  
      * @return  The name of the panel.
 188  
      */
 189  
     public String getPlotType() {
 190  2
         return localizationResources.getString("SOM_Plot");
 191  
     }
 192  
 
 193  
     /**
 194  
      * Returns the font used for cell descriptions.
 195  
      *
 196  
      * @return Description font.
 197  
      */
 198  
     public Font getDescriptionFont() {
 199  2
         return descriptionFont;
 200  
     }
 201  
     
 202  
     /**
 203  
      * Sets the font used for cell descriptions.
 204  
      *
 205  
      * @param descriptionFont  the font.
 206  
      * @throws NullPointerException
 207  
      */
 208  
     public void setDescriptionFont(Font descriptionFont) throws NullPointerException {
 209  3
         if (descriptionFont == null) {
 210  1
             throw new NullPointerException("Description font given to SOMPlot was null");
 211  
         }
 212  2
         this.descriptionFont = descriptionFont;
 213  2
     }
 214  
 
 215  
     public JSlider getColorHueSlider() {
 216  4
         return colorHueSlider;
 217  
     }
 218  
 
 219  
     public JSlider getDistanceSlider() {
 220  3
         return distanceSlider;
 221  
     }
 222  
 
 223  
     /**
 224  
      * Draws the plot on a Java2D graphics device (such as the screen or 
 225  
      * a printer).
 226  
      *
 227  
      * @param g2  the graphics device.
 228  
      * @param area  the area within which the plot should be drawn.
 229  
      * @param anchor  the anchor point (<code>null</code> permitted).
 230  
      * @param parentState  the state from the parent plot, if there is one.
 231  
      * @param info  collects info about the drawing (<code>null</code> permitted).
 232  
      */ 
 233  
     public void draw(java.awt.Graphics2D g2, java.awt.geom.Rectangle2D area, 
 234  
             java.awt.geom.Point2D anchor, PlotState parentState, 
 235  
             PlotRenderingInfo info) {
 236  
         
 237  
         // adjust for insets...
 238  3
         this.getInsets().trim(area);
 239  
 
 240  3
         if (info != null) {
 241  2
             info.setPlotArea(area);
 242  2
             info.setDataArea(area);
 243  
         }
 244  
 
 245  3
         drawBackground(g2, area);
 246  3
         drawOutline(g2, area);
 247  
 
 248  3
         Shape savedClip = g2.getClip();
 249  3
         g2.clip(area);
 250  
 
 251  3
         Composite originalComposite = g2.getComposite();
 252  3
         g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
 253  
                 getForegroundAlpha()));
 254  
 
 255  3
         drawSOM(g2, area, info);
 256  
         
 257  3
         g2.setClip(savedClip);
 258  3
         g2.setComposite(originalComposite);
 259  
 
 260  3
         drawOutline(g2, area);
 261  3
     }
 262  
 
 263  
     /**
 264  
      * Does the actual drawing for the SOMPlot.draw()-method
 265  
      * and initializes the entities associated with a cell.
 266  
      *
 267  
      * @param g2  the graphics device.
 268  
      * @param area  the area within which the plot should be drawn.
 269  
      * @param info  collects info about the drawing.
 270  
      */
 271  
     private void drawSOM(Graphics2D g2, java.awt.geom.Rectangle2D area, 
 272  
             PlotRenderingInfo info) {
 273  
         
 274  3
         double edgeLengthByWidth = ((area.getWidth() - this.dataset.getColumnCount() * (this.HORIZONTAL_GAP+1)) / 
 275  
                 ((1+2*this.dataset.getColumnCount())*Math.cos(Math.PI/6))
 276  
                 );
 277  3
         double edgeLengthByHeight = ((area.getHeight() - this.dataset.getRowCount() * (this.VERTICAL_GAP+1))/ 
 278  
                 (this.dataset.getRowCount() + (this.dataset.getRowCount() + 1) * Math.sin(Math.PI/6)
 279  
                 ));
 280  3
         double edgeLength = Math.min(edgeLengthByWidth, edgeLengthByHeight);
 281  
         
 282  3
         Polygon hexagon = createHexagon(edgeLength);
 283  3
         hexagon.translate((int)area.getMinX()+1, (int)area.getMinY()+1);
 284  
         
 285  3
         int deltaX = hexagon.xpoints[1]-hexagon.xpoints[5]+1+this.HORIZONTAL_GAP;
 286  3
         int deltaY = hexagon.ypoints[2]-hexagon.ypoints[0]+1+this.VERTICAL_GAP;
 287  12
         for (int y=0; y < this.dataset.getRowCount(); ++y) {
 288  27
             for (int x=0; x < this.dataset.getColumnCount(); ++x) {
 289  18
                 SOMDataItem item = this.dataset.getValue(x, y);
 290  18
                 if (item != null) {
 291  18
                     if (item.isSelected()) {
 292  3
                         g2.setPaint(item.getColor());
 293  3
                         g2.fillPolygon(hexagon);
 294  3
                         g2.setPaint(contrastingColor(item.getColor()));
 295  
                         
 296  
                         //Shape savedClip = g2.getClip();
 297  
                         //g2.clip(hexagon);
 298  3
                         Stroke stroke = g2.getStroke();
 299  3
                         g2.setStroke(new BasicStroke(1.5f));
 300  3
                         g2.drawPolygon(hexagon);
 301  3
                         g2.setStroke(stroke);
 302  
                         //g2.setClip(savedClip);
 303  3
                     } else {
 304  15
                         g2.setPaint(item.getColor());
 305  15
                         g2.fillPolygon(hexagon);
 306  
                     }
 307  
                     
 308  18
                     drawDescriptions(g2, 
 309  
                                      hexagon,
 310  
                                      item);
 311  18
                     createEntity(x, y, new Polygon(hexagon.xpoints,
 312  
                                                    hexagon.ypoints,
 313  
                                                    hexagon.npoints), info);
 314  18
                     hexagon.translate(deltaX , 0);
 315  
                 }
 316  
             }
 317  9
             if (y % 2 == 1) {
 318  3
                 hexagon.translate((int)(area.getMinX()-hexagon.xpoints[5]+1), deltaY);
 319  
                 //                                                        ^
 320  
                 // the +1 moves the lefth most edge into the visible/drawable area
 321  3
             } 
 322  
             else {
 323  6
                 hexagon.translate((int)(area.getMinX()-hexagon.xpoints[5]+1), deltaY);
 324  
                 //                                                        ^
 325  
                 // the +1 moves the lefth most edge into the visible/drawable area
 326  6
                 hexagon.translate(hexagon.xpoints[0]-hexagon.xpoints[5]+
 327  
                         (((this.HORIZONTAL_GAP>>1)+1)), 0);
 328  
                 // (this.HORIZONTAL_GAP/2)+1 centers the top vertex in
 329  
                 // the gap between the two hexagons above it.
 330  
             }
 331  
         }
 332  3
     }
 333  
     
 334  
    /**
 335  
     * Draws the textual descriptions associated with a SOM-cell.
 336  
     *
 337  
     * @param g2  the graphics device.
 338  
     * @param area  the area to draw the descriptions into.
 339  
     * @param descriptions  the descriptions.
 340  
     */
 341  
    private void drawDescriptions(Graphics2D g2, Polygon area, SOMDataItem item) {
 342  18
         Shape savedClip = g2.getClip();
 343  18
         g2.clip(area);
 344  18
         StringBuffer sb = new StringBuffer();
 345  42
         for (int i=0; i < item.getDescriptions().length; ++i) {
 346  24
             sb.append(item.getDescriptions()[i]);
 347  24
             if (i < item.getDescriptions().length - 1)
 348  6
                 sb.append(" ");
 349  
         }
 350  18
         String message = sb.toString();
 351  
         
 352  18
         float maxWidth = (float) (area.xpoints[1]-area.xpoints[5]);
 353  18
         int maxLines = (area.ypoints[2]-area.ypoints[1])/
 354  
                 g2.getFontMetrics(this.descriptionFont).getHeight();
 355  18
         maxLines = Math.max(maxLines, 1);
 356  
         
 357  
         // BUGFIX
 358  
         // If the following if-statement is removed, the description text
 359  
         // of the cell at (0,0) will be more narrow when compared to the
 360  
         // descriptions texts of the other cells.
 361  
         // The first if-statement draws the text in the same color
 362  
         // as the cell itself, thus doing nothing visible to the user.
 363  
         // A cleaner bugfix could probably be done by examining the inner
 364  
         // workings of createTextBlock(), but this one seems to do the job. 
 365  18
         if (message != null) {
 366  18
             TextBlock block = TextUtilities.createTextBlock(
 367  
                 message, 
 368  
                 this.descriptionFont, 
 369  
                 item.getColor(),
 370  
                 this.DESCRIPTION_WIDTH_RATIO * maxWidth,
 371  
                 maxLines,
 372  
                 new G2TextMeasurer(g2)
 373  
             );
 374  18
             block.draw(
 375  
                 g2, (float) area.xpoints[5] + maxWidth/2, 
 376  
                 (float) area.ypoints[5], TextBlockAnchor.TOP_CENTER
 377  
             );
 378  
         }
 379  
         
 380  18
         if (message != null) {
 381  18
             TextBlock block = TextUtilities.createTextBlock(
 382  
                 message, 
 383  
                 this.descriptionFont, 
 384  
                 contrastingColor(item.getColor()),
 385  
                 this.DESCRIPTION_WIDTH_RATIO * maxWidth,
 386  
                 maxLines,
 387  
                 new G2TextMeasurer(g2)
 388  
             );
 389  18
             block.draw(
 390  
                 g2, (float) area.xpoints[5] + maxWidth/2, 
 391  
                 (float) area.ypoints[5], TextBlockAnchor.TOP_CENTER
 392  
             );
 393  
         }
 394  18
         g2.setClip(savedClip);
 395  
 
 396  18
     }
 397  
     
 398  
     /**
 399  
      * Creates an entity that is associated with a certain area
 400  
      * in the SOM-map ie. a cell.
 401  
      *
 402  
      * @param x  the x-coordinate.
 403  
      * @param y  the y-coordinate.
 404  
      * @param shape  the shape in Java2D space.
 405  
      * @param info  collects info about the drawing.
 406  
      */
 407  
     private void createEntity(int x, int y, Shape shape, 
 408  
             PlotRenderingInfo info) {
 409  
         
 410  18
         if (info != null) {
 411  12
             EntityCollection entities = info.getOwner().getEntityCollection();
 412  12
             if (entities != null) {
 413  12
                 String tip = null;
 414  12
                 if (this.toolTipGenerator != null) {
 415  12
                     tip = this.toolTipGenerator.generateToolTip(
 416  
                             this.dataset, x, y);
 417  
                 }
 418  
                 
 419  
                 // SOMPlot does not have a text generator for html image maps
 420  12
                 String url = null;
 421  
                 
 422  12
                 entities.add(
 423  
                         new SOMItemEntity(shape, this.dataset, x, y, tip, url));
 424  
             }
 425  
         }
 426  18
     }
 427  
     
 428  
     /**
 429  
      * Creates a new hexagon cell for the SOM-map.
 430  
      * Vertices are numbered according the following picture.
 431  
      * Vertex 0 is on the x-axis.
 432  
      * Vertices 4 and 5 are on the y-axis.
 433  
      *
 434  
      *         0
 435  
      *        / \
 436  
      *       /   \
 437  
      *    5 /     \ 1
 438  
      *     |       |
 439  
      *     |       |
 440  
      *     |       |
 441  
      *    4 \     / 2
 442  
      *       \   /
 443  
      *        \ /
 444  
      *         3
 445  
      *
 446  
      * @param edgeLength  the length of the edges.
 447  
      * @return  The hexagon.
 448  
      * @throws IllegalArgumentException  If edgeLength <= 0.
 449  
      */
 450  
     private Polygon createHexagon(double edgeLength) throws IllegalArgumentException {
 451  3
         double sin30 = Math.sin(Math.PI/6);
 452  3
         double cos30 = Math.cos(Math.PI/6);
 453  3
         int[] xPoints = new int[6];
 454  3
         int[] yPoints = new int[6];
 455  3
         xPoints[0] = (int)(edgeLength*cos30);
 456  3
         yPoints[0] = 0;
 457  3
         xPoints[3] = xPoints[0];
 458  3
         yPoints[3] = (int)(edgeLength + 2 * edgeLength * sin30);
 459  3
         xPoints[1] = (int)(2 * edgeLength * cos30);
 460  3
         yPoints[1] = (int)(edgeLength * sin30);
 461  3
         xPoints[2] = xPoints[1];
 462  3
         yPoints[2] = (int)(yPoints[1] + edgeLength);
 463  3
         xPoints[4] = 0;
 464  3
         yPoints[4] = yPoints[2];
 465  3
         xPoints[5] = 0;
 466  3
         yPoints[5] = yPoints[1];
 467  
         
 468  3
         return new Polygon(xPoints, yPoints, 6);
 469  
     }
 470  
     
 471  
     /**
 472  
      * This method generates a new color that will contrast well
 473  
      * with the color given as the parameter.
 474  
      *
 475  
      * @return The new color.
 476  
      */
 477  
     private Color contrastingColor(Color color) {
 478  21
         Color background = (Color)getBackgroundPaint();
 479  21
         double distance = 0;
 480  
         double tmp;
 481  21
         Color returnValue = Color.BLACK;
 482  
         
 483  21
         tmp = colorDistance(Color.BLACK, background) + 
 484  
               colorDistance(Color.BLACK, color);
 485  21
         if (tmp > distance) {
 486  19
             distance = tmp;
 487  19
             returnValue = Color.BLACK;
 488  
         }
 489  21
         tmp = colorDistance(Color.WHITE, background) + 
 490  
               colorDistance(Color.WHITE, color);
 491  21
         if (tmp > distance) {
 492  12
             distance = tmp;
 493  12
             returnValue = Color.WHITE;
 494  
         }
 495  21
         tmp = colorDistance(Color.RED, background) + 
 496  
               colorDistance(Color.RED, color);
 497  21
         if (tmp > distance) {
 498  6
             distance = tmp;
 499  6
             returnValue = Color.RED;
 500  
         }
 501  21
         tmp = colorDistance(Color.GREEN, background) + 
 502  
               colorDistance(Color.GREEN, color);
 503  21
         if (tmp > distance) {
 504  1
             distance = tmp;
 505  1
             returnValue = Color.GREEN;
 506  
         }
 507  21
         tmp = colorDistance(Color.BLUE, background) + 
 508  
               colorDistance(Color.BLUE, color);
 509  21
         if (tmp > distance) {
 510  1
             distance = tmp;
 511  1
             returnValue = Color.BLUE;
 512  
         }
 513  
         
 514  21
         return returnValue;
 515  
     }
 516  
     
 517  
     /**
 518  
      * This method calculates the distance between two sets of integer color-values.
 519  
      *
 520  
      * @return  The difference in color-values.
 521  
      */
 522  
     private double colorDistance(Color color1, Color color2) {
 523  210
         int r1 = color1.getRed();
 524  210
         int g1 = color1.getGreen();
 525  210
         int b1 = color1.getBlue();
 526  
 
 527  210
         int r2 = color2.getRed();
 528  210
         int g2 = color2.getGreen();
 529  210
         int b2 = color2.getBlue();
 530  
 
 531  210
         return Math.sqrt((r1-r2)*(r1-r2)+(g1-g2)*(g1-g2)+(b1-b2)*(b1-b2));
 532  
     }
 533  
     
 534  
     /**
 535  
      * Creates a new JPanel for adjusting cell colors.
 536  
      *
 537  
      * @return  The panel.
 538  
      */
 539  
     public JPanel getPanel() {
 540  2
         ResourceBundle lr = ResourceBundle.getBundle("org.jfree.chart.editor.LocalizationBundle");
 541  2
         JPanel panel = new JPanel();
 542  2
         panel.setLayout(new GridBagLayout());
 543  2
         GridBagConstraints c = new GridBagConstraints();
 544  2
         c.fill = GridBagConstraints.HORIZONTAL;
 545  2
         panel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
 546  
 
 547  2
         this.colorHueSlider.setPaintLabels(true);
 548  2
         this.colorHueSlider.setPaintTicks(true);
 549  2
         this.colorHueSlider.setMajorTickSpacing(90);
 550  2
         this.colorHueSlider.setMinorTickSpacing(30);
 551  2
         this.colorHueSlider.setPaintTrack(false);
 552  2
         this.colorHueSlider.addChangeListener(this);
 553  
         
 554  2
         c.weightx = 0.5;
 555  2
         c.gridx = 0;
 556  2
         c.gridy = 0;
 557  2
         panel.add(new JLabel(lr.getString("Adjust_colors")), c);
 558  2
         c.gridx = 1;
 559  2
         c.gridy = 0;
 560  2
         panel.add(this.colorHueSlider,c);
 561  
         
 562  2
         this.distanceSlider.setPaintLabels(true);
 563  2
         this.distanceSlider.setPaintTicks(true);
 564  2
         this.distanceSlider.setMajorTickSpacing(100);
 565  2
         this.distanceSlider.setMinorTickSpacing(25);
 566  2
         this.distanceSlider.setPaintTrack(false);
 567  2
         this.distanceSlider.addChangeListener(this);
 568  
         
 569  2
         if (this.selectedCells.size() == 1) {
 570  1
             this.distanceSlider.setEnabled(true);
 571  1
         } else {
 572  1
             this.distanceSlider.setEnabled(false);
 573  
         }
 574  
         
 575  2
         c.insets = new Insets(30,0,0,0);
 576  2
         c.weightx = 0.5;
 577  2
         c.gridx = 0;
 578  2
         c.gridy = 1;
 579  2
         panel.add(new JLabel(lr.getString("Select_neighbors")), c);
 580  2
         c.weightx = 0.5;
 581  2
         c.gridx = 1;
 582  2
         c.gridy = 1;
 583  2
         panel.add(this.distanceSlider,c);
 584  
         
 585  2
         return panel;
 586  
     }
 587  
     
 588  
     /**
 589  
      * Returns a name for the SOM specific properties panel.
 590  
      *
 591  
      * @return  Panel name as a string.
 592  
      */
 593  
     public String getPanelName() {
 594  2
         ResourceBundle lr = ResourceBundle.getBundle("org.jfree.chart.editor.LocalizationBundle");
 595  2
         return lr.getString("SOM_panel");
 596  
     }
 597  
     
 598  
     /**
 599  
      * Implements the ChartMouseListener interface. This
 600  
      * method does nothing.
 601  
      *
 602  
      * @param event  the mouse event.
 603  
      */ 
 604  
     public void chartMouseMoved(ChartMouseEvent event) {
 605  
         ;
 606  1
     }
 607  
     
 608  
     /**
 609  
      * Implements the ChartMouseListener interface. Listens
 610  
      * for the mouse key and CTRL-key to be pressed on top of a 
 611  
      * SOMItemEntity.
 612  
      *
 613  
      * @param event  the mouse event.
 614  
      */
 615  
     public void chartMouseClicked(ChartMouseEvent event) {
 616  12
         if (event.getEntity() != null &&
 617  
             event.getEntity() instanceof SOMItemEntity) {
 618  12
             SOMItemEntity entity = (SOMItemEntity) event.getEntity();
 619  12
             SOMDataItem item =
 620  
                     entity.getDataset().getValue(entity.getX(), entity.getY());
 621  
             
 622  12
             if (event.getTrigger().isControlDown()) {
 623  3
                 handleCtrlClick(item);
 624  3
             } else if (event.getTrigger().isShiftDown()) {
 625  2
                 handleShiftClick(item);
 626  2
             } else {
 627  7
                 handleClick(item);
 628  
             }
 629  
         }
 630  12
     }
 631  
     
 632  
     /**
 633  
      * Handels the on-screen selection of multiple cells using the CTRL-key.
 634  
      *
 635  
      * @param item  the item that is being selected on screen.
 636  
      */
 637  
     private void handleCtrlClick(SOMDataItem item) {
 638  3
         if (item.isSelected()) {
 639  1
             this.selectedCells.remove(item);
 640  1
             item.setSelected(false);
 641  1
         } else {
 642  2
             this.selectedCells.add(item);
 643  2
             item.setSelected(true);
 644  
         }
 645  3
     }
 646  
 
 647  
     /**
 648  
      * Handels the on-screen selection of multiple cells using the SHIFT-key.
 649  
      *
 650  
      * @param cornerItem1  the dataitem.
 651  
      */
 652  
     private void handleShiftClick(SOMDataItem cornerItem1) {
 653  
         SOMDataItem cornerItem2;
 654  
         
 655  2
         if (this.selectedCells.isEmpty() == false) {
 656  
             // It is assumed that the cell which has been selected
 657  
             // for the longest time is first on the list
 658  1
             cornerItem2 = (SOMDataItem) this.selectedCells.get(0);
 659  1
         } else {
 660  1
             cornerItem2 = this.dataset.getValue(0, 0);
 661  
         }
 662  
         
 663  2
         List areaCells = dataset.getArea(cornerItem1, cornerItem2);
 664  
         // make sure the oldest selection stays first on the list
 665  
         // this might be unnecessary XXX
 666  2
         areaCells.remove(cornerItem2);
 667  2
         areaCells.add(0, cornerItem2);
 668  2
         this.dataset.deselectAll();
 669  2
         this.selectedCells = areaCells;
 670  
         
 671  2
         Iterator i = areaCells.iterator();
 672  14
         while (i.hasNext()) {
 673  12
             SOMDataItem item = (SOMDataItem) i.next();
 674  12
             item.setSelected(true);
 675  12
         }
 676  2
     }
 677  
 
 678  
 
 679  
     /**
 680  
      * Handels the on-screen selection of a single SOM-map cell.
 681  
      *
 682  
      * @param item  the dataitem.
 683  
      */    
 684  
     private void handleClick(SOMDataItem item) {
 685  7
         if (item.isSelected()) {
 686  3
             this.selectedCells.clear();
 687  3
             this.dataset.deselectAll();
 688  3
         } else {
 689  4
             this.selectedCells.clear();
 690  4
             this.dataset.deselectAll();
 691  4
             this.selectedCells.add(item);
 692  4
             item.setSelected(true);
 693  
         }
 694  7
     }
 695  
     
 696  
     /**
 697  
      * Listens for the hue-values slider to be moved and
 698  
      * then changes the hue-values accordingly.
 699  
      *
 700  
      * @param e  the event.
 701  
      */
 702  
     public void stateChanged(ChangeEvent e) {
 703  3
         if (e.getSource() == this.colorHueSlider) {
 704  2
             int value = this.colorHueSlider.getValue();
 705  2
             dataset.changeHueValues(this.colorHueAdjustment - value);
 706  2
             this.colorHueAdjustment = value;
 707  2
         }
 708  1
         else if (e.getSource() == this.distanceSlider) {
 709  1
             int value = this.distanceSlider.getValue();
 710  1
             SOMDataItem center = (SOMDataItem)this.selectedCells.get(0);
 711  1
             List l = this.dataset.getNeighbors(center, value , false);
 712  
             
 713  1
             this.dataset.deselectAll();
 714  1
             this.selectedCells.clear();
 715  
             
 716  1
             Iterator i = l.iterator();
 717  6
             while (i.hasNext()) {
 718  5
                 SOMDataItem item = (SOMDataItem)i.next();
 719  5
                 item.setSelected(true);
 720  5
                 this.selectedCells.add(item);
 721  5
             }
 722  
             
 723  1
             center.setSelected(true);
 724  1
             this.selectedCells.add(0, center);
 725  
         }
 726  
 
 727  3
         notifyListeners(new PlotChangeEvent(this));
 728  3
     }
 729  
     
 730  
     /**
 731  
      * Tests this plot for equality with an arbitrary object.  Note that the 
 732  
      * plot's dataset is NOT included in the test for equality.
 733  
      *
 734  
      * @param obj  the object to test against (<code>null</code> permitted).
 735  
      *
 736  
      * @return <code>true</code> or <code>false</code>.
 737  
      */
 738  
     public boolean equals(Object obj) {
 739  10
         if (obj == this) {
 740  1
             return true;
 741  
         }
 742  9
         if (!(obj instanceof SOMPlot)) {
 743  1
             return false;
 744  
         }
 745  8
         SOMPlot that = (SOMPlot) obj;
 746  
         
 747  8
         if (!ObjectUtilities.equal(this.toolTipGenerator, 
 748  
                 that.toolTipGenerator)) {
 749  2
             return false;
 750  
         }
 751  
 //        if (!ObjectUtilities.equal(this.urlGenerator, that.urlGenerator)) {
 752  
 //            return false;
 753  
 //        }
 754  
         
 755  6
         if (this.colorHueAdjustment != that.colorHueAdjustment) {
 756  1
             return false;
 757  
         }
 758  
         
 759  5
         if (!ObjectUtilities.equal(this.descriptionFont, 
 760  
                 that.descriptionFont)) {
 761  1
             return false;
 762  
         }
 763  
     
 764  
         // can't find any difference...
 765  4
         return true;
 766  
     }
 767  
 
 768  
     /**
 769  
      * Returns a clone of the plot.
 770  
      *
 771  
      * @return A clone.
 772  
      *
 773  
      * @throws CloneNotSupportedException if some component of the plot does 
 774  
      *         not support cloning.
 775  
      */
 776  
     public Object clone() throws CloneNotSupportedException {
 777  
 
 778  1
         SOMPlot clone = (SOMPlot) super.clone();
 779  1
         if (clone.dataset != null) {
 780  1
             clone.dataset.addChangeListener(clone);
 781  
         }
 782  1
         return clone;
 783  
 
 784  
     }
 785  
 
 786  
     /**
 787  
      * Provides serialization support.
 788  
      *
 789  
      * @param stream  the output stream.
 790  
      *
 791  
      * @throws IOException  if there is an I/O error.
 792  
      */
 793  
     private void writeObject(ObjectOutputStream stream) throws IOException {
 794  1
         stream.defaultWriteObject();
 795  1
     }
 796  
 
 797  
     /**
 798  
      * Provides serialization support.
 799  
      *
 800  
      * @param stream  the input stream.
 801  
      *
 802  
      * @throws IOException  if there is an I/O error.
 803  
      * @throws ClassNotFoundException  if there is a classpath problem.
 804  
      */
 805  
     private void readObject(ObjectInputStream stream) 
 806  
         throws IOException, ClassNotFoundException {
 807  1
         stream.defaultReadObject();
 808  1
     }
 809  
 
 810  
 }
 811