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