Source for org.jfree.chart.axis.CategoryAxis

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it
  10:  * under the terms of the GNU Lesser General Public License as published by
  11:  * the Free Software Foundation; either version 2.1 of the License, or
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
  22:  * USA.
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
  25:  * in the United States and other countries.]
  26:  *
  27:  * -----------------
  28:  * CategoryAxis.java
  29:  * -----------------
  30:  * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert;
  33:  * Contributor(s):   Pady Srinivasan (patch 1217634);
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG);
  38:  * 18-Sep-2001 : Updated header (DG);
  39:  * 04-Dec-2001 : Changed constructors to protected, and tidied up default
  40:  *               values (DG);
  41:  * 19-Apr-2002 : Updated import statements (DG);
  42:  * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
  43:  * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG);
  44:  * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
  45:  * 22-Jan-2002 : Removed monolithic constructor (DG);
  46:  * 26-Mar-2003 : Implemented Serializable (DG);
  47:  * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into
  48:  *               this class (DG);
  49:  * 13-Aug-2003 : Implemented Cloneable (DG);
  50:  * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
  51:  * 05-Nov-2003 : Fixed serialization bug (DG);
  52:  * 26-Nov-2003 : Added category label offset (DG);
  53:  * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised
  54:  *               category label position attributes (DG);
  55:  * 07-Jan-2004 : Added new implementation for linewrapping of category
  56:  *               labels (DG);
  57:  * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG);
  58:  * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG);
  59:  * 16-Mar-2004 : Added support for tooltips on category labels (DG);
  60:  * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D
  61:  *               because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
  62:  * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG);
  63:  * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
  64:  * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
  65:  *               release (DG);
  66:  * 21-Jan-2005 : Modified return type for RectangleAnchor.coordinates()
  67:  *               method (DG);
  68:  * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
  69:  * 26-Apr-2005 : Removed LOGGER (DG);
  70:  * 08-Jun-2005 : Fixed bug in axis layout (DG);
  71:  * 22-Nov-2005 : Added a method to access the tool tip text for a category
  72:  *               label (DG);
  73:  * 23-Nov-2005 : Added per-category font and paint options - see patch
  74:  *               1217634 (DG);
  75:  * ------------- JFreeChart 1.0.x ---------------------------------------------
  76:  * 11-Jan-2006 : Fixed null pointer exception in drawCategoryLabels - see bug
  77:  *               1403043 (DG);
  78:  * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
  79:  *               Joubert (1277726) (DG);
  80:  * 02-Oct-2006 : Updated category label entity (DG);
  81:  * 30-Oct-2006 : Updated refreshTicks() method to account for possibility of
  82:  *               multiple domain axes (DG);
  83:  * 07-Mar-2007 : Fixed bug in axis label positioning (DG);
  84:  * 27-Sep-2007 : Added getCategorySeriesMiddle() method (DG);
  85:  * 21-Nov-2007 : Fixed performance bug noted by FindBugs in the
  86:  *               equalPaintMaps() method (DG);
  87:  * 23-Apr-2008 : Fixed bug 1942059, bad use of insets in
  88:  *               calculateTextBlockWidth() (DG);
  89:  *
  90:  */
  91: 
  92: package org.jfree.chart.axis;
  93: 
  94: import java.awt.Font;
  95: import java.awt.Graphics2D;
  96: import java.awt.Paint;
  97: import java.awt.Shape;
  98: import java.awt.geom.Point2D;
  99: import java.awt.geom.Rectangle2D;
 100: import java.io.IOException;
 101: import java.io.ObjectInputStream;
 102: import java.io.ObjectOutputStream;
 103: import java.io.Serializable;
 104: import java.util.HashMap;
 105: import java.util.Iterator;
 106: import java.util.List;
 107: import java.util.Map;
 108: import java.util.Set;
 109: 
 110: import org.jfree.chart.entity.CategoryLabelEntity;
 111: import org.jfree.chart.entity.EntityCollection;
 112: import org.jfree.chart.event.AxisChangeEvent;
 113: import org.jfree.chart.plot.CategoryPlot;
 114: import org.jfree.chart.plot.Plot;
 115: import org.jfree.chart.plot.PlotRenderingInfo;
 116: import org.jfree.data.category.CategoryDataset;
 117: import org.jfree.io.SerialUtilities;
 118: import org.jfree.text.G2TextMeasurer;
 119: import org.jfree.text.TextBlock;
 120: import org.jfree.text.TextUtilities;
 121: import org.jfree.ui.RectangleAnchor;
 122: import org.jfree.ui.RectangleEdge;
 123: import org.jfree.ui.RectangleInsets;
 124: import org.jfree.ui.Size2D;
 125: import org.jfree.util.ObjectUtilities;
 126: import org.jfree.util.PaintUtilities;
 127: import org.jfree.util.ShapeUtilities;
 128: 
 129: /**
 130:  * An axis that displays categories.
 131:  */
 132: public class CategoryAxis extends Axis implements Cloneable, Serializable {
 133: 
 134:     /** For serialization. */
 135:     private static final long serialVersionUID = 5886554608114265863L;
 136: 
 137:     /**
 138:      * The default margin for the axis (used for both lower and upper margins).
 139:      */
 140:     public static final double DEFAULT_AXIS_MARGIN = 0.05;
 141: 
 142:     /**
 143:      * The default margin between categories (a percentage of the overall axis
 144:      * length).
 145:      */
 146:     public static final double DEFAULT_CATEGORY_MARGIN = 0.20;
 147: 
 148:     /** The amount of space reserved at the start of the axis. */
 149:     private double lowerMargin;
 150: 
 151:     /** The amount of space reserved at the end of the axis. */
 152:     private double upperMargin;
 153: 
 154:     /** The amount of space reserved between categories. */
 155:     private double categoryMargin;
 156: 
 157:     /** The maximum number of lines for category labels. */
 158:     private int maximumCategoryLabelLines;
 159: 
 160:     /**
 161:      * A ratio that is multiplied by the width of one category to determine the
 162:      * maximum label width.
 163:      */
 164:     private float maximumCategoryLabelWidthRatio;
 165: 
 166:     /** The category label offset. */
 167:     private int categoryLabelPositionOffset;
 168: 
 169:     /**
 170:      * A structure defining the category label positions for each axis
 171:      * location.
 172:      */
 173:     private CategoryLabelPositions categoryLabelPositions;
 174: 
 175:     /** Storage for tick label font overrides (if any). */
 176:     private Map tickLabelFontMap;
 177: 
 178:     /** Storage for tick label paint overrides (if any). */
 179:     private transient Map tickLabelPaintMap;
 180: 
 181:     /** Storage for the category label tooltips (if any). */
 182:     private Map categoryLabelToolTips;
 183: 
 184:     /**
 185:      * Creates a new category axis with no label.
 186:      */
 187:     public CategoryAxis() {
 188:         this(null);
 189:     }
 190: 
 191:     /**
 192:      * Constructs a category axis, using default values where necessary.
 193:      *
 194:      * @param label  the axis label (<code>null</code> permitted).
 195:      */
 196:     public CategoryAxis(String label) {
 197: 
 198:         super(label);
 199: 
 200:         this.lowerMargin = DEFAULT_AXIS_MARGIN;
 201:         this.upperMargin = DEFAULT_AXIS_MARGIN;
 202:         this.categoryMargin = DEFAULT_CATEGORY_MARGIN;
 203:         this.maximumCategoryLabelLines = 1;
 204:         this.maximumCategoryLabelWidthRatio = 0.0f;
 205: 
 206:         setTickMarksVisible(false);  // not supported by this axis type yet
 207: 
 208:         this.categoryLabelPositionOffset = 4;
 209:         this.categoryLabelPositions = CategoryLabelPositions.STANDARD;
 210:         this.tickLabelFontMap = new HashMap();
 211:         this.tickLabelPaintMap = new HashMap();
 212:         this.categoryLabelToolTips = new HashMap();
 213: 
 214:     }
 215: 
 216:     /**
 217:      * Returns the lower margin for the axis.
 218:      *
 219:      * @return The margin.
 220:      *
 221:      * @see #getUpperMargin()
 222:      * @see #setLowerMargin(double)
 223:      */
 224:     public double getLowerMargin() {
 225:         return this.lowerMargin;
 226:     }
 227: 
 228:     /**
 229:      * Sets the lower margin for the axis and sends an {@link AxisChangeEvent}
 230:      * to all registered listeners.
 231:      *
 232:      * @param margin  the margin as a percentage of the axis length (for
 233:      *                example, 0.05 is five percent).
 234:      *
 235:      * @see #getLowerMargin()
 236:      */
 237:     public void setLowerMargin(double margin) {
 238:         this.lowerMargin = margin;
 239:         notifyListeners(new AxisChangeEvent(this));
 240:     }
 241: 
 242:     /**
 243:      * Returns the upper margin for the axis.
 244:      *
 245:      * @return The margin.
 246:      *
 247:      * @see #getLowerMargin()
 248:      * @see #setUpperMargin(double)
 249:      */
 250:     public double getUpperMargin() {
 251:         return this.upperMargin;
 252:     }
 253: 
 254:     /**
 255:      * Sets the upper margin for the axis and sends an {@link AxisChangeEvent}
 256:      * to all registered listeners.
 257:      *
 258:      * @param margin  the margin as a percentage of the axis length (for
 259:      *                example, 0.05 is five percent).
 260:      *
 261:      * @see #getUpperMargin()
 262:      */
 263:     public void setUpperMargin(double margin) {
 264:         this.upperMargin = margin;
 265:         notifyListeners(new AxisChangeEvent(this));
 266:     }
 267: 
 268:     /**
 269:      * Returns the category margin.
 270:      *
 271:      * @return The margin.
 272:      *
 273:      * @see #setCategoryMargin(double)
 274:      */
 275:     public double getCategoryMargin() {
 276:         return this.categoryMargin;
 277:     }
 278: 
 279:     /**
 280:      * Sets the category margin and sends an {@link AxisChangeEvent} to all
 281:      * registered listeners.  The overall category margin is distributed over
 282:      * N-1 gaps, where N is the number of categories on the axis.
 283:      *
 284:      * @param margin  the margin as a percentage of the axis length (for
 285:      *                example, 0.05 is five percent).
 286:      *
 287:      * @see #getCategoryMargin()
 288:      */
 289:     public void setCategoryMargin(double margin) {
 290:         this.categoryMargin = margin;
 291:         notifyListeners(new AxisChangeEvent(this));
 292:     }
 293: 
 294:     /**
 295:      * Returns the maximum number of lines to use for each category label.
 296:      *
 297:      * @return The maximum number of lines.
 298:      *
 299:      * @see #setMaximumCategoryLabelLines(int)
 300:      */
 301:     public int getMaximumCategoryLabelLines() {
 302:         return this.maximumCategoryLabelLines;
 303:     }
 304: 
 305:     /**
 306:      * Sets the maximum number of lines to use for each category label and
 307:      * sends an {@link AxisChangeEvent} to all registered listeners.
 308:      *
 309:      * @param lines  the maximum number of lines.
 310:      *
 311:      * @see #getMaximumCategoryLabelLines()
 312:      */
 313:     public void setMaximumCategoryLabelLines(int lines) {
 314:         this.maximumCategoryLabelLines = lines;
 315:         notifyListeners(new AxisChangeEvent(this));
 316:     }
 317: 
 318:     /**
 319:      * Returns the category label width ratio.
 320:      *
 321:      * @return The ratio.
 322:      *
 323:      * @see #setMaximumCategoryLabelWidthRatio(float)
 324:      */
 325:     public float getMaximumCategoryLabelWidthRatio() {
 326:         return this.maximumCategoryLabelWidthRatio;
 327:     }
 328: 
 329:     /**
 330:      * Sets the maximum category label width ratio and sends an
 331:      * {@link AxisChangeEvent} to all registered listeners.
 332:      *
 333:      * @param ratio  the ratio.
 334:      *
 335:      * @see #getMaximumCategoryLabelWidthRatio()
 336:      */
 337:     public void setMaximumCategoryLabelWidthRatio(float ratio) {
 338:         this.maximumCategoryLabelWidthRatio = ratio;
 339:         notifyListeners(new AxisChangeEvent(this));
 340:     }
 341: 
 342:     /**
 343:      * Returns the offset between the axis and the category labels (before
 344:      * label positioning is taken into account).
 345:      *
 346:      * @return The offset (in Java2D units).
 347:      *
 348:      * @see #setCategoryLabelPositionOffset(int)
 349:      */
 350:     public int getCategoryLabelPositionOffset() {
 351:         return this.categoryLabelPositionOffset;
 352:     }
 353: 
 354:     /**
 355:      * Sets the offset between the axis and the category labels (before label
 356:      * positioning is taken into account).
 357:      *
 358:      * @param offset  the offset (in Java2D units).
 359:      *
 360:      * @see #getCategoryLabelPositionOffset()
 361:      */
 362:     public void setCategoryLabelPositionOffset(int offset) {
 363:         this.categoryLabelPositionOffset = offset;
 364:         notifyListeners(new AxisChangeEvent(this));
 365:     }
 366: 
 367:     /**
 368:      * Returns the category label position specification (this contains label
 369:      * positioning info for all four possible axis locations).
 370:      *
 371:      * @return The positions (never <code>null</code>).
 372:      *
 373:      * @see #setCategoryLabelPositions(CategoryLabelPositions)
 374:      */
 375:     public CategoryLabelPositions getCategoryLabelPositions() {
 376:         return this.categoryLabelPositions;
 377:     }
 378: 
 379:     /**
 380:      * Sets the category label position specification for the axis and sends an
 381:      * {@link AxisChangeEvent} to all registered listeners.
 382:      *
 383:      * @param positions  the positions (<code>null</code> not permitted).
 384:      *
 385:      * @see #getCategoryLabelPositions()
 386:      */
 387:     public void setCategoryLabelPositions(CategoryLabelPositions positions) {
 388:         if (positions == null) {
 389:             throw new IllegalArgumentException("Null 'positions' argument.");
 390:         }
 391:         this.categoryLabelPositions = positions;
 392:         notifyListeners(new AxisChangeEvent(this));
 393:     }
 394: 
 395:     /**
 396:      * Returns the font for the tick label for the given category.
 397:      *
 398:      * @param category  the category (<code>null</code> not permitted).
 399:      *
 400:      * @return The font (never <code>null</code>).
 401:      *
 402:      * @see #setTickLabelFont(Comparable, Font)
 403:      */
 404:     public Font getTickLabelFont(Comparable category) {
 405:         if (category == null) {
 406:             throw new IllegalArgumentException("Null 'category' argument.");
 407:         }
 408:         Font result = (Font) this.tickLabelFontMap.get(category);
 409:         // if there is no specific font, use the general one...
 410:         if (result == null) {
 411:             result = getTickLabelFont();
 412:         }
 413:         return result;
 414:     }
 415: 
 416:     /**
 417:      * Sets the font for the tick label for the specified category and sends
 418:      * an {@link AxisChangeEvent} to all registered listeners.
 419:      *
 420:      * @param category  the category (<code>null</code> not permitted).
 421:      * @param font  the font (<code>null</code> permitted).
 422:      *
 423:      * @see #getTickLabelFont(Comparable)
 424:      */
 425:     public void setTickLabelFont(Comparable category, Font font) {
 426:         if (category == null) {
 427:             throw new IllegalArgumentException("Null 'category' argument.");
 428:         }
 429:         if (font == null) {
 430:             this.tickLabelFontMap.remove(category);
 431:         }
 432:         else {
 433:             this.tickLabelFontMap.put(category, font);
 434:         }
 435:         notifyListeners(new AxisChangeEvent(this));
 436:     }
 437: 
 438:     /**
 439:      * Returns the paint for the tick label for the given category.
 440:      *
 441:      * @param category  the category (<code>null</code> not permitted).
 442:      *
 443:      * @return The paint (never <code>null</code>).
 444:      *
 445:      * @see #setTickLabelPaint(Paint)
 446:      */
 447:     public Paint getTickLabelPaint(Comparable category) {
 448:         if (category == null) {
 449:             throw new IllegalArgumentException("Null 'category' argument.");
 450:         }
 451:         Paint result = (Paint) this.tickLabelPaintMap.get(category);
 452:         // if there is no specific paint, use the general one...
 453:         if (result == null) {
 454:             result = getTickLabelPaint();
 455:         }
 456:         return result;
 457:     }
 458: 
 459:     /**
 460:      * Sets the paint for the tick label for the specified category and sends
 461:      * an {@link AxisChangeEvent} to all registered listeners.
 462:      *
 463:      * @param category  the category (<code>null</code> not permitted).
 464:      * @param paint  the paint (<code>null</code> permitted).
 465:      *
 466:      * @see #getTickLabelPaint(Comparable)
 467:      */
 468:     public void setTickLabelPaint(Comparable category, Paint paint) {
 469:         if (category == null) {
 470:             throw new IllegalArgumentException("Null 'category' argument.");
 471:         }
 472:         if (paint == null) {
 473:             this.tickLabelPaintMap.remove(category);
 474:         }
 475:         else {
 476:             this.tickLabelPaintMap.put(category, paint);
 477:         }
 478:         notifyListeners(new AxisChangeEvent(this));
 479:     }
 480: 
 481:     /**
 482:      * Adds a tooltip to the specified category and sends an
 483:      * {@link AxisChangeEvent} to all registered listeners.
 484:      *
 485:      * @param category  the category (<code>null<code> not permitted).
 486:      * @param tooltip  the tooltip text (<code>null</code> permitted).
 487:      *
 488:      * @see #removeCategoryLabelToolTip(Comparable)
 489:      */
 490:     public void addCategoryLabelToolTip(Comparable category, String tooltip) {
 491:         if (category == null) {
 492:             throw new IllegalArgumentException("Null 'category' argument.");
 493:         }
 494:         this.categoryLabelToolTips.put(category, tooltip);
 495:         notifyListeners(new AxisChangeEvent(this));
 496:     }
 497: 
 498:     /**
 499:      * Returns the tool tip text for the label belonging to the specified
 500:      * category.
 501:      *
 502:      * @param category  the category (<code>null</code> not permitted).
 503:      *
 504:      * @return The tool tip text (possibly <code>null</code>).
 505:      *
 506:      * @see #addCategoryLabelToolTip(Comparable, String)
 507:      * @see #removeCategoryLabelToolTip(Comparable)
 508:      */
 509:     public String getCategoryLabelToolTip(Comparable category) {
 510:         if (category == null) {
 511:             throw new IllegalArgumentException("Null 'category' argument.");
 512:         }
 513:         return (String) this.categoryLabelToolTips.get(category);
 514:     }
 515: 
 516:     /**
 517:      * Removes the tooltip for the specified category and sends an
 518:      * {@link AxisChangeEvent} to all registered listeners.
 519:      *
 520:      * @param category  the category (<code>null<code> not permitted).
 521:      *
 522:      * @see #addCategoryLabelToolTip(Comparable, String)
 523:      * @see #clearCategoryLabelToolTips()
 524:      */
 525:     public void removeCategoryLabelToolTip(Comparable category) {
 526:         if (category == null) {
 527:             throw new IllegalArgumentException("Null 'category' argument.");
 528:         }
 529:         this.categoryLabelToolTips.remove(category);
 530:         notifyListeners(new AxisChangeEvent(this));
 531:     }
 532: 
 533:     /**
 534:      * Clears the category label tooltips and sends an {@link AxisChangeEvent}
 535:      * to all registered listeners.
 536:      *
 537:      * @see #addCategoryLabelToolTip(Comparable, String)
 538:      * @see #removeCategoryLabelToolTip(Comparable)
 539:      */
 540:     public void clearCategoryLabelToolTips() {
 541:         this.categoryLabelToolTips.clear();
 542:         notifyListeners(new AxisChangeEvent(this));
 543:     }
 544: 
 545:     /**
 546:      * Returns the Java 2D coordinate for a category.
 547:      *
 548:      * @param anchor  the anchor point.
 549:      * @param category  the category index.
 550:      * @param categoryCount  the category count.
 551:      * @param area  the data area.
 552:      * @param edge  the location of the axis.
 553:      *
 554:      * @return The coordinate.
 555:      */
 556:     public double getCategoryJava2DCoordinate(CategoryAnchor anchor,
 557:                                               int category,
 558:                                               int categoryCount,
 559:                                               Rectangle2D area,
 560:                                               RectangleEdge edge) {
 561: 
 562:         double result = 0.0;
 563:         if (anchor == CategoryAnchor.START) {
 564:             result = getCategoryStart(category, categoryCount, area, edge);
 565:         }
 566:         else if (anchor == CategoryAnchor.MIDDLE) {
 567:             result = getCategoryMiddle(category, categoryCount, area, edge);
 568:         }
 569:         else if (anchor == CategoryAnchor.END) {
 570:             result = getCategoryEnd(category, categoryCount, area, edge);
 571:         }
 572:         return result;
 573: 
 574:     }
 575: 
 576:     /**
 577:      * Returns the starting coordinate for the specified category.
 578:      *
 579:      * @param category  the category.
 580:      * @param categoryCount  the number of categories.
 581:      * @param area  the data area.
 582:      * @param edge  the axis location.
 583:      *
 584:      * @return The coordinate.
 585:      *
 586:      * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
 587:      * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
 588:      */
 589:     public double getCategoryStart(int category, int categoryCount,
 590:                                    Rectangle2D area,
 591:                                    RectangleEdge edge) {
 592: 
 593:         double result = 0.0;
 594:         if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
 595:             result = area.getX() + area.getWidth() * getLowerMargin();
 596:         }
 597:         else if ((edge == RectangleEdge.LEFT)
 598:                 || (edge == RectangleEdge.RIGHT)) {
 599:             result = area.getMinY() + area.getHeight() * getLowerMargin();
 600:         }
 601: 
 602:         double categorySize = calculateCategorySize(categoryCount, area, edge);
 603:         double categoryGapWidth = calculateCategoryGapSize(categoryCount, area,
 604:                 edge);
 605: 
 606:         result = result + category * (categorySize + categoryGapWidth);
 607:         return result;
 608: 
 609:     }
 610: 
 611:     /**
 612:      * Returns the middle coordinate for the specified category.
 613:      *
 614:      * @param category  the category.
 615:      * @param categoryCount  the number of categories.
 616:      * @param area  the data area.
 617:      * @param edge  the axis location.
 618:      *
 619:      * @return The coordinate.
 620:      *
 621:      * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
 622:      * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
 623:      */
 624:     public double getCategoryMiddle(int category, int categoryCount,
 625:                                     Rectangle2D area, RectangleEdge edge) {
 626: 
 627:         return getCategoryStart(category, categoryCount, area, edge)
 628:                + calculateCategorySize(categoryCount, area, edge) / 2;
 629: 
 630:     }
 631: 
 632:     /**
 633:      * Returns the end coordinate for the specified category.
 634:      *
 635:      * @param category  the category.
 636:      * @param categoryCount  the number of categories.
 637:      * @param area  the data area.
 638:      * @param edge  the axis location.
 639:      *
 640:      * @return The coordinate.
 641:      *
 642:      * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
 643:      * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
 644:      */
 645:     public double getCategoryEnd(int category, int categoryCount,
 646:                                  Rectangle2D area, RectangleEdge edge) {
 647: 
 648:         return getCategoryStart(category, categoryCount, area, edge)
 649:                + calculateCategorySize(categoryCount, area, edge);
 650: 
 651:     }
 652: 
 653:     /**
 654:      * Returns the middle coordinate (in Java2D space) for a series within a
 655:      * category.
 656:      *
 657:      * @param category  the category (<code>null</code> not permitted).
 658:      * @param seriesKey  the series key (<code>null</code> not permitted).
 659:      * @param dataset  the dataset (<code>null</code> not permitted).
 660:      * @param itemMargin  the item margin (0.0 <= itemMargin < 1.0);
 661:      * @param area  the area (<code>null</code> not permitted).
 662:      * @param edge  the edge (<code>null</code> not permitted).
 663:      *
 664:      * @return The coordinate in Java2D space.
 665:      *
 666:      * @since 1.0.7
 667:      */
 668:     public double getCategorySeriesMiddle(Comparable category,
 669:             Comparable seriesKey, CategoryDataset dataset, double itemMargin,
 670:             Rectangle2D area, RectangleEdge edge) {
 671: 
 672:         int categoryIndex = dataset.getColumnIndex(category);
 673:         int categoryCount = dataset.getColumnCount();
 674:         int seriesIndex = dataset.getRowIndex(seriesKey);
 675:         int seriesCount = dataset.getRowCount();
 676:         double start = getCategoryStart(categoryIndex, categoryCount, area,
 677:                 edge);
 678:         double end = getCategoryEnd(categoryIndex, categoryCount, area, edge);
 679:         double width = end - start;
 680:         if (seriesCount == 1) {
 681:             return start + width / 2.0;
 682:         }
 683:         else {
 684:             double gap = (width * itemMargin) / (seriesCount - 1);
 685:             double ww = (width * (1 - itemMargin)) / seriesCount;
 686:             return start + (seriesIndex * (ww + gap)) + ww / 2.0;
 687:         }
 688:     }
 689: 
 690:     /**
 691:      * Calculates the size (width or height, depending on the location of the
 692:      * axis) of a category.
 693:      *
 694:      * @param categoryCount  the number of categories.
 695:      * @param area  the area within which the categories will be drawn.
 696:      * @param edge  the axis location.
 697:      *
 698:      * @return The category size.
 699:      */
 700:     protected double calculateCategorySize(int categoryCount, Rectangle2D area,
 701:                                            RectangleEdge edge) {
 702: 
 703:         double result = 0.0;
 704:         double available = 0.0;
 705: 
 706:         if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
 707:             available = area.getWidth();
 708:         }
 709:         else if ((edge == RectangleEdge.LEFT)
 710:                 || (edge == RectangleEdge.RIGHT)) {
 711:             available = area.getHeight();
 712:         }
 713:         if (categoryCount > 1) {
 714:             result = available * (1 - getLowerMargin() - getUpperMargin()
 715:                      - getCategoryMargin());
 716:             result = result / categoryCount;
 717:         }
 718:         else {
 719:             result = available * (1 - getLowerMargin() - getUpperMargin());
 720:         }
 721:         return result;
 722: 
 723:     }
 724: 
 725:     /**
 726:      * Calculates the size (width or height, depending on the location of the
 727:      * axis) of a category gap.
 728:      *
 729:      * @param categoryCount  the number of categories.
 730:      * @param area  the area within which the categories will be drawn.
 731:      * @param edge  the axis location.
 732:      *
 733:      * @return The category gap width.
 734:      */
 735:     protected double calculateCategoryGapSize(int categoryCount,
 736:                                               Rectangle2D area,
 737:                                               RectangleEdge edge) {
 738: 
 739:         double result = 0.0;
 740:         double available = 0.0;
 741: 
 742:         if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
 743:             available = area.getWidth();
 744:         }
 745:         else if ((edge == RectangleEdge.LEFT)
 746:                 || (edge == RectangleEdge.RIGHT)) {
 747:             available = area.getHeight();
 748:         }
 749: 
 750:         if (categoryCount > 1) {
 751:             result = available * getCategoryMargin() / (categoryCount - 1);
 752:         }
 753: 
 754:         return result;
 755: 
 756:     }
 757: 
 758:     /**
 759:      * Estimates the space required for the axis, given a specific drawing area.
 760:      *
 761:      * @param g2  the graphics device (used to obtain font information).
 762:      * @param plot  the plot that the axis belongs to.
 763:      * @param plotArea  the area within which the axis should be drawn.
 764:      * @param edge  the axis location (top or bottom).
 765:      * @param space  the space already reserved.
 766:      *
 767:      * @return The space required to draw the axis.
 768:      */
 769:     public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
 770:                                   Rectangle2D plotArea,
 771:                                   RectangleEdge edge, AxisSpace space) {
 772: 
 773:         // create a new space object if one wasn't supplied...
 774:         if (space == null) {
 775:             space = new AxisSpace();
 776:         }
 777: 
 778:         // if the axis is not visible, no additional space is required...
 779:         if (!isVisible()) {
 780:             return space;
 781:         }
 782: 
 783:         // calculate the max size of the tick labels (if visible)...
 784:         double tickLabelHeight = 0.0;
 785:         double tickLabelWidth = 0.0;
 786:         if (isTickLabelsVisible()) {
 787:             g2.setFont(getTickLabelFont());
 788:             AxisState state = new AxisState();
 789:             // we call refresh ticks just to get the maximum width or height
 790:             refreshTicks(g2, state, plotArea, edge);
 791:             if (edge == RectangleEdge.TOP) {
 792:                 tickLabelHeight = state.getMax();
 793:             }
 794:             else if (edge == RectangleEdge.BOTTOM) {
 795:                 tickLabelHeight = state.getMax();
 796:             }
 797:             else if (edge == RectangleEdge.LEFT) {
 798:                 tickLabelWidth = state.getMax();
 799:             }
 800:             else if (edge == RectangleEdge.RIGHT) {
 801:                 tickLabelWidth = state.getMax();
 802:             }
 803:         }
 804: 
 805:         // get the axis label size and update the space object...
 806:         Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
 807:         double labelHeight = 0.0;
 808:         double labelWidth = 0.0;
 809:         if (RectangleEdge.isTopOrBottom(edge)) {
 810:             labelHeight = labelEnclosure.getHeight();
 811:             space.add(labelHeight + tickLabelHeight
 812:                     + this.categoryLabelPositionOffset, edge);
 813:         }
 814:         else if (RectangleEdge.isLeftOrRight(edge)) {
 815:             labelWidth = labelEnclosure.getWidth();
 816:             space.add(labelWidth + tickLabelWidth
 817:                     + this.categoryLabelPositionOffset, edge);
 818:         }
 819:         return space;
 820: 
 821:     }
 822: 
 823:     /**
 824:      * Configures the axis against the current plot.
 825:      */
 826:     public void configure() {
 827:         // nothing required
 828:     }
 829: 
 830:     /**
 831:      * Draws the axis on a Java 2D graphics device (such as the screen or a
 832:      * printer).
 833:      *
 834:      * @param g2  the graphics device (<code>null</code> not permitted).
 835:      * @param cursor  the cursor location.
 836:      * @param plotArea  the area within which the axis should be drawn
 837:      *                  (<code>null</code> not permitted).
 838:      * @param dataArea  the area within which the plot is being drawn
 839:      *                  (<code>null</code> not permitted).
 840:      * @param edge  the location of the axis (<code>null</code> not permitted).
 841:      * @param plotState  collects information about the plot
 842:      *                   (<code>null</code> permitted).
 843:      *
 844:      * @return The axis state (never <code>null</code>).
 845:      */
 846:     public AxisState draw(Graphics2D g2,
 847:                           double cursor,
 848:                           Rectangle2D plotArea,
 849:                           Rectangle2D dataArea,
 850:                           RectangleEdge edge,
 851:                           PlotRenderingInfo plotState) {
 852: 
 853:         // if the axis is not visible, don't draw it...
 854:         if (!isVisible()) {
 855:             return new AxisState(cursor);
 856:         }
 857: 
 858:         if (isAxisLineVisible()) {
 859:             drawAxisLine(g2, cursor, dataArea, edge);
 860:         }
 861: 
 862:         // draw the category labels and axis label
 863:         AxisState state = new AxisState(cursor);
 864:         state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
 865:                 plotState);
 866:         state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
 867: 
 868:         return state;
 869: 
 870:     }
 871: 
 872:     /**
 873:      * Draws the category labels and returns the updated axis state.
 874:      *
 875:      * @param g2  the graphics device (<code>null</code> not permitted).
 876:      * @param dataArea  the area inside the axes (<code>null</code> not
 877:      *                  permitted).
 878:      * @param edge  the axis location (<code>null</code> not permitted).
 879:      * @param state  the axis state (<code>null</code> not permitted).
 880:      * @param plotState  collects information about the plot (<code>null</code>
 881:      *                   permitted).
 882:      *
 883:      * @return The updated axis state (never <code>null</code>).
 884:      *
 885:      * @deprecated Use {@link #drawCategoryLabels(Graphics2D, Rectangle2D,
 886:      *     Rectangle2D, RectangleEdge, AxisState, PlotRenderingInfo)}.
 887:      */
 888:     protected AxisState drawCategoryLabels(Graphics2D g2,
 889:                                            Rectangle2D dataArea,
 890:                                            RectangleEdge edge,
 891:                                            AxisState state,
 892:                                            PlotRenderingInfo plotState) {
 893: 
 894:         // this method is deprecated because we really need the plotArea
 895:         // when drawing the labels - see bug 1277726
 896:         return drawCategoryLabels(g2, dataArea, dataArea, edge, state,
 897:                 plotState);
 898:     }
 899: 
 900:     /**
 901:      * Draws the category labels and returns the updated axis state.
 902:      *
 903:      * @param g2  the graphics device (<code>null</code> not permitted).
 904:      * @param plotArea  the plot area (<code>null</code> not permitted).
 905:      * @param dataArea  the area inside the axes (<code>null</code> not
 906:      *                  permitted).
 907:      * @param edge  the axis location (<code>null</code> not permitted).
 908:      * @param state  the axis state (<code>null</code> not permitted).
 909:      * @param plotState  collects information about the plot (<code>null</code>
 910:      *                   permitted).
 911:      *
 912:      * @return The updated axis state (never <code>null</code>).
 913:      */
 914:     protected AxisState drawCategoryLabels(Graphics2D g2,
 915:                                            Rectangle2D plotArea,
 916:                                            Rectangle2D dataArea,
 917:                                            RectangleEdge edge,
 918:                                            AxisState state,
 919:                                            PlotRenderingInfo plotState) {
 920: 
 921:         if (state == null) {
 922:             throw new IllegalArgumentException("Null 'state' argument.");
 923:         }
 924: 
 925:         if (isTickLabelsVisible()) {
 926:             List ticks = refreshTicks(g2, state, plotArea, edge);
 927:             state.setTicks(ticks);
 928: 
 929:             int categoryIndex = 0;
 930:             Iterator iterator = ticks.iterator();
 931:             while (iterator.hasNext()) {
 932: 
 933:                 CategoryTick tick = (CategoryTick) iterator.next();
 934:                 g2.setFont(getTickLabelFont(tick.getCategory()));
 935:                 g2.setPaint(getTickLabelPaint(tick.getCategory()));
 936: 
 937:                 CategoryLabelPosition position
 938:                         = this.categoryLabelPositions.getLabelPosition(edge);
 939:                 double x0 = 0.0;
 940:                 double x1 = 0.0;
 941:                 double y0 = 0.0;
 942:                 double y1 = 0.0;
 943:                 if (edge == RectangleEdge.TOP) {
 944:                     x0 = getCategoryStart(categoryIndex, ticks.size(),
 945:                             dataArea, edge);
 946:                     x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
 947:                             edge);
 948:                     y1 = state.getCursor() - this.categoryLabelPositionOffset;
 949:                     y0 = y1 - state.getMax();
 950:                 }
 951:                 else if (edge == RectangleEdge.BOTTOM) {
 952:                     x0 = getCategoryStart(categoryIndex, ticks.size(),
 953:                             dataArea, edge);
 954:                     x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
 955:                             edge);
 956:                     y0 = state.getCursor() + this.categoryLabelPositionOffset;
 957:                     y1 = y0 + state.getMax();
 958:                 }
 959:                 else if (edge == RectangleEdge.LEFT) {
 960:                     y0 = getCategoryStart(categoryIndex, ticks.size(),
 961:                             dataArea, edge);
 962:                     y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
 963:                             edge);
 964:                     x1 = state.getCursor() - this.categoryLabelPositionOffset;
 965:                     x0 = x1 - state.getMax();
 966:                 }
 967:                 else if (edge == RectangleEdge.RIGHT) {
 968:                     y0 = getCategoryStart(categoryIndex, ticks.size(),
 969:                             dataArea, edge);
 970:                     y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
 971:                             edge);
 972:                     x0 = state.getCursor() + this.categoryLabelPositionOffset;
 973:                     x1 = x0 - state.getMax();
 974:                 }
 975:                 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
 976:                         (y1 - y0));
 977:                 Point2D anchorPoint = RectangleAnchor.coordinates(area,
 978:                         position.getCategoryAnchor());
 979:                 TextBlock block = tick.getLabel();
 980:                 block.draw(g2, (float) anchorPoint.getX(),
 981:                         (float) anchorPoint.getY(), position.getLabelAnchor(),
 982:                         (float) anchorPoint.getX(), (float) anchorPoint.getY(),
 983:                         position.getAngle());
 984:                 Shape bounds = block.calculateBounds(g2,
 985:                         (float) anchorPoint.getX(), (float) anchorPoint.getY(),
 986:                         position.getLabelAnchor(), (float) anchorPoint.getX(),
 987:                         (float) anchorPoint.getY(), position.getAngle());
 988:                 if (plotState != null && plotState.getOwner() != null) {
 989:                     EntityCollection entities
 990:                             = plotState.getOwner().getEntityCollection();
 991:                     if (entities != null) {
 992:                         String tooltip = getCategoryLabelToolTip(
 993:                                 tick.getCategory());
 994:                         entities.add(new CategoryLabelEntity(tick.getCategory(),
 995:                                 bounds, tooltip, null));
 996:                     }
 997:                 }
 998:                 categoryIndex++;
 999:             }
1000: 
1001:             if (edge.equals(RectangleEdge.TOP)) {
1002:                 double h = state.getMax() + this.categoryLabelPositionOffset;
1003:                 state.cursorUp(h);
1004:             }
1005:             else if (edge.equals(RectangleEdge.BOTTOM)) {
1006:                 double h = state.getMax() + this.categoryLabelPositionOffset;
1007:                 state.cursorDown(h);
1008:             }
1009:             else if (edge == RectangleEdge.LEFT) {
1010:                 double w = state.getMax() + this.categoryLabelPositionOffset;
1011:                 state.cursorLeft(w);
1012:             }
1013:             else if (edge == RectangleEdge.RIGHT) {
1014:                 double w = state.getMax() + this.categoryLabelPositionOffset;
1015:                 state.cursorRight(w);
1016:             }
1017:         }
1018:         return state;
1019:     }
1020: 
1021:     /**
1022:      * Creates a temporary list of ticks that can be used when drawing the axis.
1023:      *
1024:      * @param g2  the graphics device (used to get font measurements).
1025:      * @param state  the axis state.
1026:      * @param dataArea  the area inside the axes.
1027:      * @param edge  the location of the axis.
1028:      *
1029:      * @return A list of ticks.
1030:      */
1031:     public List refreshTicks(Graphics2D g2,
1032:                              AxisState state,
1033:                              Rectangle2D dataArea,
1034:                              RectangleEdge edge) {
1035: 
1036:         List ticks = new java.util.ArrayList();
1037: 
1038:         // sanity check for data area...
1039:         if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
1040:             return ticks;
1041:         }
1042: 
1043:         CategoryPlot plot = (CategoryPlot) getPlot();
1044:         List categories = plot.getCategoriesForAxis(this);
1045:         double max = 0.0;
1046: 
1047:         if (categories != null) {
1048:             CategoryLabelPosition position
1049:                     = this.categoryLabelPositions.getLabelPosition(edge);
1050:             float r = this.maximumCategoryLabelWidthRatio;
1051:             if (r <= 0.0) {
1052:                 r = position.getWidthRatio();
1053:             }
1054: 
1055:             float l = 0.0f;
1056:             if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
1057:                 l = (float) calculateCategorySize(categories.size(), dataArea,
1058:                         edge);
1059:             }
1060:             else {
1061:                 if (RectangleEdge.isLeftOrRight(edge)) {
1062:                     l = (float) dataArea.getWidth();
1063:                 }
1064:                 else {
1065:                     l = (float) dataArea.getHeight();
1066:                 }
1067:             }
1068:             int categoryIndex = 0;
1069:             Iterator iterator = categories.iterator();
1070:             while (iterator.hasNext()) {
1071:                 Comparable category = (Comparable) iterator.next();
1072:                 TextBlock label = createLabel(category, l * r, edge, g2);
1073:                 if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
1074:                     max = Math.max(max, calculateTextBlockHeight(label,
1075:                             position, g2));
1076:                 }
1077:                 else if (edge == RectangleEdge.LEFT
1078:                         || edge == RectangleEdge.RIGHT) {
1079:                     max = Math.max(max, calculateTextBlockWidth(label,
1080:                             position, g2));
1081:                 }
1082:                 Tick tick = new CategoryTick(category, label,
1083:                         position.getLabelAnchor(),
1084:                         position.getRotationAnchor(), position.getAngle());
1085:                 ticks.add(tick);
1086:                 categoryIndex = categoryIndex + 1;
1087:             }
1088:         }
1089:         state.setMax(max);
1090:         return ticks;
1091: 
1092:     }
1093: 
1094:     /**
1095:      * Creates a label.
1096:      *
1097:      * @param category  the category.
1098:      * @param width  the available width.
1099:      * @param edge  the edge on which the axis appears.
1100:      * @param g2  the graphics device.
1101:      *
1102:      * @return A label.
1103:      */
1104:     protected TextBlock createLabel(Comparable category, float width,
1105:                                     RectangleEdge edge, Graphics2D g2) {
1106:         TextBlock label = TextUtilities.createTextBlock(category.toString(),
1107:                 getTickLabelFont(category), getTickLabelPaint(category), width,
1108:                 this.maximumCategoryLabelLines, new G2TextMeasurer(g2));
1109:         return label;
1110:     }
1111: 
1112:     /**
1113:      * A utility method for determining the width of a text block.
1114:      *
1115:      * @param block  the text block.
1116:      * @param position  the position.
1117:      * @param g2  the graphics device.
1118:      *
1119:      * @return The width.
1120:      */
1121:     protected double calculateTextBlockWidth(TextBlock block,
1122:             CategoryLabelPosition position, Graphics2D g2) {
1123: 
1124:         RectangleInsets insets = getTickLabelInsets();
1125:         Size2D size = block.calculateDimensions(g2);
1126:         Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1127:                 size.getHeight());
1128:         Shape rotatedBox = ShapeUtilities.rotateShape(box, position.getAngle(),
1129:                 0.0f, 0.0f);
1130:         double w = rotatedBox.getBounds2D().getWidth() + insets.getLeft()
1131:                 + insets.getRight();
1132:         return w;
1133: 
1134:     }
1135: 
1136:     /**
1137:      * A utility method for determining the height of a text block.
1138:      *
1139:      * @param block  the text block.
1140:      * @param position  the label position.
1141:      * @param g2  the graphics device.
1142:      *
1143:      * @return The height.
1144:      */
1145:     protected double calculateTextBlockHeight(TextBlock block,
1146:                                               CategoryLabelPosition position,
1147:                                               Graphics2D g2) {
1148: 
1149:         RectangleInsets insets = getTickLabelInsets();
1150:         Size2D size = block.calculateDimensions(g2);
1151:         Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1152:                 size.getHeight());
1153:         Shape rotatedBox = ShapeUtilities.rotateShape(box, position.getAngle(),
1154:                 0.0f, 0.0f);
1155:         double h = rotatedBox.getBounds2D().getHeight()
1156:                    + insets.getTop() + insets.getBottom();
1157:         return h;
1158: 
1159:     }
1160: 
1161:     /**
1162:      * Creates a clone of the axis.
1163:      *
1164:      * @return A clone.
1165:      *
1166:      * @throws CloneNotSupportedException if some component of the axis does
1167:      *         not support cloning.
1168:      */
1169:     public Object clone() throws CloneNotSupportedException {
1170:         CategoryAxis clone = (CategoryAxis) super.clone();
1171:         clone.tickLabelFontMap = new HashMap(this.tickLabelFontMap);
1172:         clone.tickLabelPaintMap = new HashMap(this.tickLabelPaintMap);
1173:         clone.categoryLabelToolTips = new HashMap(this.categoryLabelToolTips);
1174:         return clone;
1175:     }
1176: 
1177:     /**
1178:      * Tests this axis for equality with an arbitrary object.
1179:      *
1180:      * @param obj  the object (<code>null</code> permitted).
1181:      *
1182:      * @return A boolean.
1183:      */
1184:     public boolean equals(Object obj) {
1185:         if (obj == this) {
1186:             return true;
1187:         }
1188:         if (!(obj instanceof CategoryAxis)) {
1189:             return false;
1190:         }
1191:         if (!super.equals(obj)) {
1192:             return false;
1193:         }
1194:         CategoryAxis that = (CategoryAxis) obj;
1195:         if (that.lowerMargin != this.lowerMargin) {
1196:             return false;
1197:         }
1198:         if (that.upperMargin != this.upperMargin) {
1199:             return false;
1200:         }
1201:         if (that.categoryMargin != this.categoryMargin) {
1202:             return false;
1203:         }
1204:         if (that.maximumCategoryLabelWidthRatio
1205:                 != this.maximumCategoryLabelWidthRatio) {
1206:             return false;
1207:         }
1208:         if (that.categoryLabelPositionOffset
1209:                 != this.categoryLabelPositionOffset) {
1210:             return false;
1211:         }
1212:         if (!ObjectUtilities.equal(that.categoryLabelPositions,
1213:                 this.categoryLabelPositions)) {
1214:             return false;
1215:         }
1216:         if (!ObjectUtilities.equal(that.categoryLabelToolTips,
1217:                 this.categoryLabelToolTips)) {
1218:             return false;
1219:         }
1220:         if (!ObjectUtilities.equal(this.tickLabelFontMap,
1221:                 that.tickLabelFontMap)) {
1222:             return false;
1223:         }
1224:         if (!equalPaintMaps(this.tickLabelPaintMap, that.tickLabelPaintMap)) {
1225:             return false;
1226:         }
1227:         return true;
1228:     }
1229: 
1230:     /**
1231:      * Returns a hash code for this object.
1232:      *
1233:      * @return A hash code.
1234:      */
1235:     public int hashCode() {
1236:         if (getLabel() != null) {
1237:             return getLabel().hashCode();
1238:         }
1239:         else {
1240:             return 0;
1241:         }
1242:     }
1243: 
1244:     /**
1245:      * Provides serialization support.
1246:      *
1247:      * @param stream  the output stream.
1248:      *
1249:      * @throws IOException  if there is an I/O error.
1250:      */
1251:     private void writeObject(ObjectOutputStream stream) throws IOException {
1252:         stream.defaultWriteObject();
1253:         writePaintMap(this.tickLabelPaintMap, stream);
1254:     }
1255: 
1256:     /**
1257:      * Provides serialization support.
1258:      *
1259:      * @param stream  the input stream.
1260:      *
1261:      * @throws IOException  if there is an I/O error.
1262:      * @throws ClassNotFoundException  if there is a classpath problem.
1263:      */
1264:     private void readObject(ObjectInputStream stream)
1265:         throws IOException, ClassNotFoundException {
1266:         stream.defaultReadObject();
1267:         this.tickLabelPaintMap = readPaintMap(stream);
1268:     }
1269: 
1270:     /**
1271:      * Reads a <code>Map</code> of (<code>Comparable</code>, <code>Paint</code>)
1272:      * elements from a stream.
1273:      *
1274:      * @param in  the input stream.
1275:      *
1276:      * @return The map.
1277:      *
1278:      * @throws IOException
1279:      * @throws ClassNotFoundException
1280:      *
1281:      * @see #writePaintMap(Map, ObjectOutputStream)
1282:      */
1283:     private Map readPaintMap(ObjectInputStream in)
1284:             throws IOException, ClassNotFoundException {
1285:         boolean isNull = in.readBoolean();
1286:         if (isNull) {
1287:             return null;
1288:         }
1289:         Map result = new HashMap();
1290:         int count = in.readInt();
1291:         for (int i = 0; i < count; i++) {
1292:             Comparable category = (Comparable) in.readObject();
1293:             Paint paint = SerialUtilities.readPaint(in);
1294:             result.put(category, paint);
1295:         }
1296:         return result;
1297:     }
1298: 
1299:     /**
1300:      * Writes a map of (<code>Comparable</code>, <code>Paint</code>)
1301:      * elements to a stream.
1302:      *
1303:      * @param map  the map (<code>null</code> permitted).
1304:      *
1305:      * @param out
1306:      * @throws IOException
1307:      *
1308:      * @see #readPaintMap(ObjectInputStream)
1309:      */
1310:     private void writePaintMap(Map map, ObjectOutputStream out)
1311:             throws IOException {
1312:         if (map == null) {
1313:             out.writeBoolean(true);
1314:         }
1315:         else {
1316:             out.writeBoolean(false);
1317:             Set keys = map.keySet();
1318:             int count = keys.size();
1319:             out.writeInt(count);
1320:             Iterator iterator = keys.iterator();
1321:             while (iterator.hasNext()) {
1322:                 Comparable key = (Comparable) iterator.next();
1323:                 out.writeObject(key);
1324:                 SerialUtilities.writePaint((Paint) map.get(key), out);
1325:             }
1326:         }
1327:     }
1328: 
1329:     /**
1330:      * Tests two maps containing (<code>Comparable</code>, <code>Paint</code>)
1331:      * elements for equality.
1332:      *
1333:      * @param map1  the first map (<code>null</code> not permitted).
1334:      * @param map2  the second map (<code>null</code> not permitted).
1335:      *
1336:      * @return A boolean.
1337:      */
1338:     private boolean equalPaintMaps(Map map1, Map map2) {
1339:         if (map1.size() != map2.size()) {
1340:             return false;
1341:         }
1342:         Set entries = map1.entrySet();
1343:         Iterator iterator = entries.iterator();
1344:         while (iterator.hasNext()) {
1345:             Map.Entry entry = (Map.Entry) iterator.next();
1346:             Paint p1 = (Paint) entry.getValue();
1347:             Paint p2 = (Paint) map2.get(entry.getKey());
1348:             if (!PaintUtilities.equal(p1, p2)) {
1349:                 return false;
1350:             }
1351:         }
1352:         return true;
1353:     }
1354: 
1355: }