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