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