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