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