001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by David Gilbert 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.Objects; 057import java.util.Set; 058 059import org.jfree.chart.entity.CategoryLabelEntity; 060import org.jfree.chart.entity.EntityCollection; 061import org.jfree.chart.event.AxisChangeEvent; 062import org.jfree.chart.plot.CategoryPlot; 063import org.jfree.chart.plot.Plot; 064import org.jfree.chart.plot.PlotRenderingInfo; 065import org.jfree.chart.text.G2TextMeasurer; 066import org.jfree.chart.text.TextBlock; 067import org.jfree.chart.text.TextUtils; 068import org.jfree.chart.ui.RectangleEdge; 069import org.jfree.chart.ui.RectangleInsets; 070import org.jfree.chart.ui.Size2D; 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 public void addCategoryLabelURL(Comparable category, String url) { 490 Args.nullNotPermitted(category, "category"); 491 this.categoryLabelURLs.put(category, url); 492 fireChangeEvent(); 493 } 494 495 /** 496 * Returns the URL for the label belonging to the specified category. 497 * 498 * @param category the category ({@code null} not permitted). 499 * 500 * @return The URL text (possibly {@code null}). 501 * 502 * @see #addCategoryLabelURL(Comparable, String) 503 * @see #removeCategoryLabelURL(Comparable) 504 */ 505 public String getCategoryLabelURL(Comparable category) { 506 Args.nullNotPermitted(category, "category"); 507 return (String) this.categoryLabelURLs.get(category); 508 } 509 510 /** 511 * Removes the URL for the specified category and, if there was a URL 512 * associated with that category, sends an {@link AxisChangeEvent} to all 513 * registered listeners. 514 * 515 * @param category the category ({@code null} not permitted). 516 * 517 * @see #addCategoryLabelURL(Comparable, String) 518 * @see #clearCategoryLabelURLs() 519 */ 520 public void removeCategoryLabelURL(Comparable category) { 521 Args.nullNotPermitted(category, "category"); 522 if (this.categoryLabelURLs.remove(category) != null) { 523 fireChangeEvent(); 524 } 525 } 526 527 /** 528 * Clears the category label URLs and sends an {@link AxisChangeEvent} 529 * to all registered listeners. 530 * 531 * @see #addCategoryLabelURL(Comparable, String) 532 * @see #removeCategoryLabelURL(Comparable) 533 */ 534 public void clearCategoryLabelURLs() { 535 this.categoryLabelURLs.clear(); 536 fireChangeEvent(); 537 } 538 539 /** 540 * Returns the Java 2D coordinate for a category. 541 * 542 * @param anchor the anchor point. 543 * @param category the category index. 544 * @param categoryCount the category count. 545 * @param area the data area. 546 * @param edge the location of the axis. 547 * 548 * @return The coordinate. 549 */ 550 public double getCategoryJava2DCoordinate(CategoryAnchor anchor, 551 int category, int categoryCount, Rectangle2D area, 552 RectangleEdge edge) { 553 554 double result = 0.0; 555 if (anchor == CategoryAnchor.START) { 556 result = getCategoryStart(category, categoryCount, area, edge); 557 } 558 else if (anchor == CategoryAnchor.MIDDLE) { 559 result = getCategoryMiddle(category, categoryCount, area, edge); 560 } 561 else if (anchor == CategoryAnchor.END) { 562 result = getCategoryEnd(category, categoryCount, area, edge); 563 } 564 return result; 565 566 } 567 568 /** 569 * Returns the starting coordinate for the specified category. 570 * 571 * @param category the category. 572 * @param categoryCount the number of categories. 573 * @param area the data area. 574 * @param edge the axis location. 575 * 576 * @return The coordinate. 577 * 578 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge) 579 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge) 580 */ 581 public double getCategoryStart(int category, int categoryCount, 582 Rectangle2D area, RectangleEdge edge) { 583 584 double result = 0.0; 585 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 586 result = area.getX() + area.getWidth() * getLowerMargin(); 587 } 588 else if ((edge == RectangleEdge.LEFT) 589 || (edge == RectangleEdge.RIGHT)) { 590 result = area.getMinY() + area.getHeight() * getLowerMargin(); 591 } 592 593 double categorySize = calculateCategorySize(categoryCount, area, edge); 594 double categoryGapWidth = calculateCategoryGapSize(categoryCount, area, 595 edge); 596 597 result = result + category * (categorySize + categoryGapWidth); 598 return result; 599 } 600 601 /** 602 * Returns the middle coordinate for the specified category. 603 * 604 * @param category the category. 605 * @param categoryCount the number of categories. 606 * @param area the data area. 607 * @param edge the axis location. 608 * 609 * @return The coordinate. 610 * 611 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge) 612 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge) 613 */ 614 public double getCategoryMiddle(int category, int categoryCount, 615 Rectangle2D area, RectangleEdge edge) { 616 617 if (category < 0 || category >= categoryCount) { 618 throw new IllegalArgumentException("Invalid category index: " 619 + category); 620 } 621 return getCategoryStart(category, categoryCount, area, edge) 622 + calculateCategorySize(categoryCount, area, edge) / 2; 623 624 } 625 626 /** 627 * Returns the end coordinate for the specified category. 628 * 629 * @param category the category. 630 * @param categoryCount the number of categories. 631 * @param area the data area. 632 * @param edge the axis location. 633 * 634 * @return The coordinate. 635 * 636 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge) 637 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge) 638 */ 639 public double getCategoryEnd(int category, int categoryCount, 640 Rectangle2D area, RectangleEdge edge) { 641 return getCategoryStart(category, categoryCount, area, edge) 642 + calculateCategorySize(categoryCount, area, edge); 643 } 644 645 /** 646 * A convenience method that returns the axis coordinate for the centre of 647 * a category. 648 * 649 * @param category the category key ({@code null} not permitted). 650 * @param categories the categories ({@code null} not permitted). 651 * @param area the data area ({@code null} not permitted). 652 * @param edge the edge along which the axis lies ({@code null} not 653 * permitted). 654 * 655 * @return The centre coordinate. 656 * 657 * @see #getCategorySeriesMiddle(Comparable, Comparable, CategoryDataset, 658 * double, Rectangle2D, RectangleEdge) 659 */ 660 public double getCategoryMiddle(Comparable category, 661 List categories, Rectangle2D area, RectangleEdge edge) { 662 Args.nullNotPermitted(categories, "categories"); 663 int categoryIndex = categories.indexOf(category); 664 int categoryCount = categories.size(); 665 return getCategoryMiddle(categoryIndex, categoryCount, area, edge); 666 } 667 668 /** 669 * Returns the middle coordinate (in Java2D space) for a series within a 670 * category. 671 * 672 * @param category the category ({@code null} not permitted). 673 * @param seriesKey the series key ({@code null} not permitted). 674 * @param dataset the dataset ({@code null} not permitted). 675 * @param itemMargin the item margin (0.0 <= itemMargin < 1.0); 676 * @param area the area ({@code null} not permitted). 677 * @param edge the edge ({@code null} not permitted). 678 * 679 * @return The coordinate in Java2D space. 680 */ 681 public double getCategorySeriesMiddle(Comparable category, 682 Comparable seriesKey, CategoryDataset dataset, double itemMargin, 683 Rectangle2D area, RectangleEdge edge) { 684 685 int categoryIndex = dataset.getColumnIndex(category); 686 int categoryCount = dataset.getColumnCount(); 687 int seriesIndex = dataset.getRowIndex(seriesKey); 688 int seriesCount = dataset.getRowCount(); 689 double start = getCategoryStart(categoryIndex, categoryCount, area, 690 edge); 691 double end = getCategoryEnd(categoryIndex, categoryCount, area, edge); 692 double width = end - start; 693 if (seriesCount == 1) { 694 return start + width / 2.0; 695 } 696 else { 697 double gap = (width * itemMargin) / (seriesCount - 1); 698 double ww = (width * (1 - itemMargin)) / seriesCount; 699 return start + (seriesIndex * (ww + gap)) + ww / 2.0; 700 } 701 } 702 703 /** 704 * Returns the middle coordinate (in Java2D space) for a series within a 705 * category. 706 * 707 * @param categoryIndex the category index. 708 * @param categoryCount the category count. 709 * @param seriesIndex the series index. 710 * @param seriesCount the series count. 711 * @param itemMargin the item margin (0.0 <= itemMargin < 1.0); 712 * @param area the area ({@code null} not permitted). 713 * @param edge the edge ({@code null} not permitted). 714 * 715 * @return The coordinate in Java2D space. 716 */ 717 public double getCategorySeriesMiddle(int categoryIndex, int categoryCount, 718 int seriesIndex, int seriesCount, double itemMargin, 719 Rectangle2D area, RectangleEdge edge) { 720 721 double start = getCategoryStart(categoryIndex, categoryCount, area, 722 edge); 723 double end = getCategoryEnd(categoryIndex, categoryCount, area, edge); 724 double width = end - start; 725 if (seriesCount == 1) { 726 return start + width / 2.0; 727 } 728 else { 729 double gap = (width * itemMargin) / (seriesCount - 1); 730 double ww = (width * (1 - itemMargin)) / seriesCount; 731 return start + (seriesIndex * (ww + gap)) + ww / 2.0; 732 } 733 } 734 735 /** 736 * Calculates the size (width or height, depending on the location of the 737 * axis) of a category. 738 * 739 * @param categoryCount the number of categories. 740 * @param area the area within which the categories will be drawn. 741 * @param edge the axis location. 742 * 743 * @return The category size. 744 */ 745 protected double calculateCategorySize(int categoryCount, Rectangle2D area, 746 RectangleEdge edge) { 747 double result; 748 double available = 0.0; 749 750 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 751 available = area.getWidth(); 752 } 753 else if ((edge == RectangleEdge.LEFT) 754 || (edge == RectangleEdge.RIGHT)) { 755 available = area.getHeight(); 756 } 757 if (categoryCount > 1) { 758 result = available * (1 - getLowerMargin() - getUpperMargin() 759 - getCategoryMargin()); 760 result = result / categoryCount; 761 } 762 else { 763 result = available * (1 - getLowerMargin() - getUpperMargin()); 764 } 765 return result; 766 } 767 768 /** 769 * Calculates the size (width or height, depending on the location of the 770 * axis) of a category gap. 771 * 772 * @param categoryCount the number of categories. 773 * @param area the area within which the categories will be drawn. 774 * @param edge the axis location. 775 * 776 * @return The category gap width. 777 */ 778 protected double calculateCategoryGapSize(int categoryCount, 779 Rectangle2D area, RectangleEdge edge) { 780 781 double result = 0.0; 782 double available = 0.0; 783 784 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 785 available = area.getWidth(); 786 } 787 else if ((edge == RectangleEdge.LEFT) 788 || (edge == RectangleEdge.RIGHT)) { 789 available = area.getHeight(); 790 } 791 792 if (categoryCount > 1) { 793 result = available * getCategoryMargin() / (categoryCount - 1); 794 } 795 return result; 796 } 797 798 /** 799 * Estimates the space required for the axis, given a specific drawing area. 800 * 801 * @param g2 the graphics device (used to obtain font information). 802 * @param plot the plot that the axis belongs to. 803 * @param plotArea the area within which the axis should be drawn. 804 * @param edge the axis location (top or bottom). 805 * @param space the space already reserved. 806 * 807 * @return The space required to draw the axis. 808 */ 809 @Override 810 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 811 Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) { 812 813 // create a new space object if one wasn't supplied... 814 if (space == null) { 815 space = new AxisSpace(); 816 } 817 818 // if the axis is not visible, no additional space is required... 819 if (!isVisible()) { 820 return space; 821 } 822 823 // calculate the max size of the tick labels (if visible)... 824 double tickLabelHeight = 0.0; 825 double tickLabelWidth = 0.0; 826 if (isTickLabelsVisible()) { 827 g2.setFont(getTickLabelFont()); 828 AxisState state = new AxisState(); 829 // we call refresh ticks just to get the maximum width or height 830 refreshTicks(g2, state, plotArea, edge); 831 if (edge == RectangleEdge.TOP) { 832 tickLabelHeight = state.getMax(); 833 } 834 else if (edge == RectangleEdge.BOTTOM) { 835 tickLabelHeight = state.getMax(); 836 } 837 else if (edge == RectangleEdge.LEFT) { 838 tickLabelWidth = state.getMax(); 839 } 840 else if (edge == RectangleEdge.RIGHT) { 841 tickLabelWidth = state.getMax(); 842 } 843 } 844 845 // get the axis label size and update the space object... 846 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge); 847 double labelHeight, labelWidth; 848 if (RectangleEdge.isTopOrBottom(edge)) { 849 labelHeight = labelEnclosure.getHeight(); 850 space.add(labelHeight + tickLabelHeight 851 + this.categoryLabelPositionOffset, edge); 852 } 853 else if (RectangleEdge.isLeftOrRight(edge)) { 854 labelWidth = labelEnclosure.getWidth(); 855 space.add(labelWidth + tickLabelWidth 856 + this.categoryLabelPositionOffset, edge); 857 } 858 return space; 859 } 860 861 /** 862 * Configures the axis against the current plot. 863 */ 864 @Override 865 public void configure() { 866 // nothing required 867 } 868 869 /** 870 * Draws the axis on a Java 2D graphics device (such as the screen or a 871 * printer). 872 * 873 * @param g2 the graphics device ({@code null} not permitted). 874 * @param cursor the cursor location. 875 * @param plotArea the area within which the axis should be drawn 876 * ({@code null} not permitted). 877 * @param dataArea the area within which the plot is being drawn 878 * ({@code null} not permitted). 879 * @param edge the location of the axis ({@code null} not permitted). 880 * @param plotState collects information about the plot 881 * ({@code null} permitted). 882 * 883 * @return The axis state (never {@code null}). 884 */ 885 @Override 886 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 887 Rectangle2D dataArea, RectangleEdge edge, 888 PlotRenderingInfo plotState) { 889 890 // if the axis is not visible, don't draw it... 891 if (!isVisible()) { 892 return new AxisState(cursor); 893 } 894 895 if (isAxisLineVisible()) { 896 drawAxisLine(g2, cursor, dataArea, edge); 897 } 898 AxisState state = new AxisState(cursor); 899 if (isTickMarksVisible()) { 900 drawTickMarks(g2, cursor, dataArea, edge, state); 901 } 902 903 createAndAddEntity(cursor, state, dataArea, edge, plotState); 904 905 // draw the category labels and axis label 906 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, 907 plotState); 908 if (getAttributedLabel() != null) { 909 state = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 910 dataArea, edge, state); 911 912 } else { 913 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 914 } 915 return state; 916 917 } 918 919 /** 920 * Draws the category labels and returns the updated axis state. 921 * 922 * @param g2 the graphics device ({@code null} not permitted). 923 * @param plotArea the plot area ({@code null} not permitted). 924 * @param dataArea the area inside the axes ({@code null} not 925 * permitted). 926 * @param edge the axis location ({@code null} not permitted). 927 * @param state the axis state ({@code null} not permitted). 928 * @param plotState collects information about the plot ({@code null} 929 * permitted). 930 * 931 * @return The updated axis state (never {@code null}). 932 */ 933 protected AxisState drawCategoryLabels(Graphics2D g2, Rectangle2D plotArea, 934 Rectangle2D dataArea, RectangleEdge edge, AxisState state, 935 PlotRenderingInfo plotState) { 936 937 Args.nullNotPermitted(state, "state"); 938 if (!isTickLabelsVisible()) { 939 return state; 940 } 941 942 List ticks = refreshTicks(g2, state, plotArea, edge); 943 state.setTicks(ticks); 944 int categoryIndex = 0; 945 Iterator iterator = ticks.iterator(); 946 while (iterator.hasNext()) { 947 CategoryTick tick = (CategoryTick) iterator.next(); 948 g2.setFont(getTickLabelFont(tick.getCategory())); 949 g2.setPaint(getTickLabelPaint(tick.getCategory())); 950 951 CategoryLabelPosition position 952 = this.categoryLabelPositions.getLabelPosition(edge); 953 double x0 = 0.0; 954 double x1 = 0.0; 955 double y0 = 0.0; 956 double y1 = 0.0; 957 if (edge == RectangleEdge.TOP) { 958 x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 959 edge); 960 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 961 edge); 962 y1 = state.getCursor() - this.categoryLabelPositionOffset; 963 y0 = y1 - state.getMax(); 964 } 965 else if (edge == RectangleEdge.BOTTOM) { 966 x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 967 edge); 968 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 969 edge); 970 y0 = state.getCursor() + this.categoryLabelPositionOffset; 971 y1 = y0 + state.getMax(); 972 } 973 else if (edge == RectangleEdge.LEFT) { 974 y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 975 edge); 976 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 977 edge); 978 x1 = state.getCursor() - this.categoryLabelPositionOffset; 979 x0 = x1 - state.getMax(); 980 } 981 else if (edge == RectangleEdge.RIGHT) { 982 y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 983 edge); 984 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 985 edge); 986 x0 = state.getCursor() + this.categoryLabelPositionOffset; 987 x1 = x0 - state.getMax(); 988 } 989 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 990 (y1 - y0)); 991 Point2D anchorPoint = position.getCategoryAnchor().getAnchorPoint(area); 992 TextBlock block = tick.getLabel(); 993 block.draw(g2, (float) anchorPoint.getX(), 994 (float) anchorPoint.getY(), position.getLabelAnchor(), 995 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 996 position.getAngle()); 997 Shape bounds = block.calculateBounds(g2, 998 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 999 position.getLabelAnchor(), (float) anchorPoint.getX(), 1000 (float) anchorPoint.getY(), position.getAngle()); 1001 if (plotState != null && plotState.getOwner() != null) { 1002 EntityCollection entities = plotState.getOwner() 1003 .getEntityCollection(); 1004 if (entities != null) { 1005 String tooltip = getCategoryLabelToolTip( 1006 tick.getCategory()); 1007 String url = getCategoryLabelURL(tick.getCategory()); 1008 entities.add(new CategoryLabelEntity(tick.getCategory(), 1009 bounds, tooltip, url)); 1010 } 1011 } 1012 categoryIndex++; 1013 } 1014 1015 if (edge.equals(RectangleEdge.TOP)) { 1016 double h = state.getMax() + this.categoryLabelPositionOffset; 1017 state.cursorUp(h); 1018 } 1019 else if (edge.equals(RectangleEdge.BOTTOM)) { 1020 double h = state.getMax() + this.categoryLabelPositionOffset; 1021 state.cursorDown(h); 1022 } 1023 else if (edge == RectangleEdge.LEFT) { 1024 double w = state.getMax() + this.categoryLabelPositionOffset; 1025 state.cursorLeft(w); 1026 } 1027 else if (edge == RectangleEdge.RIGHT) { 1028 double w = state.getMax() + this.categoryLabelPositionOffset; 1029 state.cursorRight(w); 1030 } 1031 return state; 1032 } 1033 1034 /** 1035 * Creates a temporary list of ticks that can be used when drawing the axis. 1036 * 1037 * @param g2 the graphics device (used to get font measurements). 1038 * @param state the axis state. 1039 * @param dataArea the area inside the axes. 1040 * @param edge the location of the axis. 1041 * 1042 * @return A list of ticks. 1043 */ 1044 @Override 1045 public List refreshTicks(Graphics2D g2, AxisState state, 1046 Rectangle2D dataArea, RectangleEdge edge) { 1047 1048 List ticks = new java.util.ArrayList(); 1049 1050 // sanity check for data area... 1051 if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) { 1052 return ticks; 1053 } 1054 1055 CategoryPlot plot = (CategoryPlot) getPlot(); 1056 List categories = plot.getCategoriesForAxis(this); 1057 double max = 0.0; 1058 1059 if (categories != null) { 1060 CategoryLabelPosition position 1061 = this.categoryLabelPositions.getLabelPosition(edge); 1062 float r = this.maximumCategoryLabelWidthRatio; 1063 if (r <= 0.0) { 1064 r = position.getWidthRatio(); 1065 } 1066 1067 float l; 1068 if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) { 1069 l = (float) calculateCategorySize(categories.size(), dataArea, 1070 edge); 1071 } 1072 else { 1073 if (RectangleEdge.isLeftOrRight(edge)) { 1074 l = (float) dataArea.getWidth(); 1075 } 1076 else { 1077 l = (float) dataArea.getHeight(); 1078 } 1079 } 1080 int categoryIndex = 0; 1081 Iterator iterator = categories.iterator(); 1082 while (iterator.hasNext()) { 1083 Comparable category = (Comparable) iterator.next(); 1084 g2.setFont(getTickLabelFont(category)); 1085 TextBlock label = createLabel(category, l * r, edge, g2); 1086 if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) { 1087 max = Math.max(max, calculateTextBlockHeight(label, 1088 position, g2)); 1089 } 1090 else if (edge == RectangleEdge.LEFT 1091 || edge == RectangleEdge.RIGHT) { 1092 max = Math.max(max, calculateTextBlockWidth(label, 1093 position, g2)); 1094 } 1095 Tick tick = new CategoryTick(category, label, 1096 position.getLabelAnchor(), 1097 position.getRotationAnchor(), position.getAngle()); 1098 ticks.add(tick); 1099 categoryIndex = categoryIndex + 1; 1100 } 1101 } 1102 state.setMax(max); 1103 return ticks; 1104 1105 } 1106 1107 /** 1108 * Draws the tick marks. 1109 * 1110 * @param g2 the graphics target. 1111 * @param cursor the cursor position (an offset when drawing multiple axes) 1112 * @param dataArea the area for plotting the data. 1113 * @param edge the location of the axis. 1114 * @param state the axis state. 1115 */ 1116 public void drawTickMarks(Graphics2D g2, double cursor, 1117 Rectangle2D dataArea, RectangleEdge edge, AxisState state) { 1118 1119 Plot p = getPlot(); 1120 if (p == null) { 1121 return; 1122 } 1123 CategoryPlot plot = (CategoryPlot) p; 1124 double il = getTickMarkInsideLength(); 1125 double ol = getTickMarkOutsideLength(); 1126 Line2D line = new Line2D.Double(); 1127 List categories = plot.getCategoriesForAxis(this); 1128 g2.setPaint(getTickMarkPaint()); 1129 g2.setStroke(getTickMarkStroke()); 1130 Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 1131 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 1132 RenderingHints.VALUE_STROKE_NORMALIZE); 1133 if (edge.equals(RectangleEdge.TOP)) { 1134 Iterator iterator = categories.iterator(); 1135 while (iterator.hasNext()) { 1136 Comparable key = (Comparable) iterator.next(); 1137 double x = getCategoryMiddle(key, categories, dataArea, edge); 1138 line.setLine(x, cursor, x, cursor + il); 1139 g2.draw(line); 1140 line.setLine(x, cursor, x, cursor - ol); 1141 g2.draw(line); 1142 } 1143 state.cursorUp(ol); 1144 } else if (edge.equals(RectangleEdge.BOTTOM)) { 1145 Iterator iterator = categories.iterator(); 1146 while (iterator.hasNext()) { 1147 Comparable key = (Comparable) iterator.next(); 1148 double x = getCategoryMiddle(key, categories, dataArea, edge); 1149 line.setLine(x, cursor, x, cursor - il); 1150 g2.draw(line); 1151 line.setLine(x, cursor, x, cursor + ol); 1152 g2.draw(line); 1153 } 1154 state.cursorDown(ol); 1155 } else if (edge.equals(RectangleEdge.LEFT)) { 1156 Iterator iterator = categories.iterator(); 1157 while (iterator.hasNext()) { 1158 Comparable key = (Comparable) iterator.next(); 1159 double y = getCategoryMiddle(key, categories, dataArea, edge); 1160 line.setLine(cursor, y, cursor + il, y); 1161 g2.draw(line); 1162 line.setLine(cursor, y, cursor - ol, y); 1163 g2.draw(line); 1164 } 1165 state.cursorLeft(ol); 1166 } else if (edge.equals(RectangleEdge.RIGHT)) { 1167 Iterator iterator = categories.iterator(); 1168 while (iterator.hasNext()) { 1169 Comparable key = (Comparable) iterator.next(); 1170 double y = getCategoryMiddle(key, categories, dataArea, edge); 1171 line.setLine(cursor, y, cursor - il, y); 1172 g2.draw(line); 1173 line.setLine(cursor, y, cursor + ol, y); 1174 g2.draw(line); 1175 } 1176 state.cursorRight(ol); 1177 } 1178 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved); 1179 } 1180 1181 /** 1182 * Creates a label. 1183 * 1184 * @param category the category. 1185 * @param width the available width. 1186 * @param edge the edge on which the axis appears. 1187 * @param g2 the graphics device. 1188 * 1189 * @return A label. 1190 */ 1191 protected TextBlock createLabel(Comparable category, float width, 1192 RectangleEdge edge, Graphics2D g2) { 1193 TextBlock label = TextUtils.createTextBlock(category.toString(), 1194 getTickLabelFont(category), getTickLabelPaint(category), width, 1195 this.maximumCategoryLabelLines, new G2TextMeasurer(g2)); 1196 return label; 1197 } 1198 1199 /** 1200 * A utility method for determining the width of a text block. 1201 * 1202 * @param block the text block. 1203 * @param position the position. 1204 * @param g2 the graphics device. 1205 * 1206 * @return The width. 1207 */ 1208 protected double calculateTextBlockWidth(TextBlock block, 1209 CategoryLabelPosition position, Graphics2D g2) { 1210 RectangleInsets insets = getTickLabelInsets(); 1211 Size2D size = block.calculateDimensions(g2); 1212 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(), 1213 size.getHeight()); 1214 Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(), 1215 0.0f, 0.0f); 1216 double w = rotatedBox.getBounds2D().getWidth() + insets.getLeft() 1217 + insets.getRight(); 1218 return w; 1219 } 1220 1221 /** 1222 * A utility method for determining the height of a text block. 1223 * 1224 * @param block the text block. 1225 * @param position the label position. 1226 * @param g2 the graphics device. 1227 * 1228 * @return The height. 1229 */ 1230 protected double calculateTextBlockHeight(TextBlock block, 1231 CategoryLabelPosition position, Graphics2D g2) { 1232 RectangleInsets insets = getTickLabelInsets(); 1233 Size2D size = block.calculateDimensions(g2); 1234 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(), 1235 size.getHeight()); 1236 Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(), 1237 0.0f, 0.0f); 1238 double h = rotatedBox.getBounds2D().getHeight() 1239 + insets.getTop() + insets.getBottom(); 1240 return h; 1241 } 1242 1243 /** 1244 * Creates a clone of the axis. 1245 * 1246 * @return A clone. 1247 * 1248 * @throws CloneNotSupportedException if some component of the axis does 1249 * not support cloning. 1250 */ 1251 @Override 1252 public Object clone() throws CloneNotSupportedException { 1253 CategoryAxis clone = (CategoryAxis) super.clone(); 1254 clone.tickLabelFontMap = new HashMap(this.tickLabelFontMap); 1255 clone.tickLabelPaintMap = new HashMap(this.tickLabelPaintMap); 1256 clone.categoryLabelToolTips = new HashMap(this.categoryLabelToolTips); 1257 clone.categoryLabelURLs = new HashMap(this.categoryLabelToolTips); 1258 return clone; 1259 } 1260 1261 /** 1262 * Tests this axis for equality with an arbitrary object. 1263 * 1264 * @param obj the object ({@code null} permitted). 1265 * 1266 * @return A boolean. 1267 */ 1268 @Override 1269 public boolean equals(Object obj) { 1270 if (obj == this) { 1271 return true; 1272 } 1273 if (!(obj instanceof CategoryAxis)) { 1274 return false; 1275 } 1276 if (!super.equals(obj)) { 1277 return false; 1278 } 1279 CategoryAxis that = (CategoryAxis) obj; 1280 if (that.lowerMargin != this.lowerMargin) { 1281 return false; 1282 } 1283 if (that.upperMargin != this.upperMargin) { 1284 return false; 1285 } 1286 if (that.categoryMargin != this.categoryMargin) { 1287 return false; 1288 } 1289 if (that.maximumCategoryLabelWidthRatio 1290 != this.maximumCategoryLabelWidthRatio) { 1291 return false; 1292 } 1293 if (that.categoryLabelPositionOffset 1294 != this.categoryLabelPositionOffset) { 1295 return false; 1296 } 1297 if (!Objects.equals(that.categoryLabelPositions, 1298 this.categoryLabelPositions)) { 1299 return false; 1300 } 1301 if (!Objects.equals(that.categoryLabelToolTips, 1302 this.categoryLabelToolTips)) { 1303 return false; 1304 } 1305 if (!Objects.equals(this.categoryLabelURLs, 1306 that.categoryLabelURLs)) { 1307 return false; 1308 } 1309 if (!Objects.equals(this.tickLabelFontMap, 1310 that.tickLabelFontMap)) { 1311 return false; 1312 } 1313 if (!equalPaintMaps(this.tickLabelPaintMap, that.tickLabelPaintMap)) { 1314 return false; 1315 } 1316 return true; 1317 } 1318 1319 /** 1320 * Returns a hash code for this object. 1321 * 1322 * @return A hash code. 1323 */ 1324 @Override 1325 public int hashCode() { 1326 return super.hashCode(); 1327 } 1328 1329 /** 1330 * Provides serialization support. 1331 * 1332 * @param stream the output stream. 1333 * 1334 * @throws IOException if there is an I/O error. 1335 */ 1336 private void writeObject(ObjectOutputStream stream) throws IOException { 1337 stream.defaultWriteObject(); 1338 writePaintMap(this.tickLabelPaintMap, stream); 1339 } 1340 1341 /** 1342 * Provides serialization support. 1343 * 1344 * @param stream the input stream. 1345 * 1346 * @throws IOException if there is an I/O error. 1347 * @throws ClassNotFoundException if there is a classpath problem. 1348 */ 1349 private void readObject(ObjectInputStream stream) 1350 throws IOException, ClassNotFoundException { 1351 stream.defaultReadObject(); 1352 this.tickLabelPaintMap = readPaintMap(stream); 1353 } 1354 1355 /** 1356 * Reads a {@code Map} of ({@code Comparable}, {@code Paint}) 1357 * elements from a stream. 1358 * 1359 * @param in the input stream. 1360 * 1361 * @return The map. 1362 * 1363 * @throws IOException 1364 * @throws ClassNotFoundException 1365 * 1366 * @see #writePaintMap(Map, ObjectOutputStream) 1367 */ 1368 private Map readPaintMap(ObjectInputStream in) 1369 throws IOException, ClassNotFoundException { 1370 boolean isNull = in.readBoolean(); 1371 if (isNull) { 1372 return null; 1373 } 1374 Map result = new HashMap(); 1375 int count = in.readInt(); 1376 for (int i = 0; i < count; i++) { 1377 Comparable category = (Comparable) in.readObject(); 1378 Paint paint = SerialUtils.readPaint(in); 1379 result.put(category, paint); 1380 } 1381 return result; 1382 } 1383 1384 /** 1385 * Writes a map of ({@code Comparable}, {@code Paint}) 1386 * elements to a stream. 1387 * 1388 * @param map the map ({@code null} permitted). 1389 * 1390 * @param out 1391 * @throws IOException 1392 * 1393 * @see #readPaintMap(ObjectInputStream) 1394 */ 1395 private void writePaintMap(Map map, ObjectOutputStream out) 1396 throws IOException { 1397 if (map == null) { 1398 out.writeBoolean(true); 1399 } 1400 else { 1401 out.writeBoolean(false); 1402 Set keys = map.keySet(); 1403 int count = keys.size(); 1404 out.writeInt(count); 1405 Iterator iterator = keys.iterator(); 1406 while (iterator.hasNext()) { 1407 Comparable key = (Comparable) iterator.next(); 1408 out.writeObject(key); 1409 SerialUtils.writePaint((Paint) map.get(key), out); 1410 } 1411 } 1412 } 1413 1414 /** 1415 * Tests two maps containing ({@code Comparable}, {@code Paint}) 1416 * elements for equality. 1417 * 1418 * @param map1 the first map ({@code null} not permitted). 1419 * @param map2 the second map ({@code null} not permitted). 1420 * 1421 * @return A boolean. 1422 */ 1423 private boolean equalPaintMaps(Map map1, Map map2) { 1424 if (map1.size() != map2.size()) { 1425 return false; 1426 } 1427 Set entries = map1.entrySet(); 1428 Iterator iterator = entries.iterator(); 1429 while (iterator.hasNext()) { 1430 Map.Entry entry = (Map.Entry) iterator.next(); 1431 Paint p1 = (Paint) entry.getValue(); 1432 Paint p2 = (Paint) map2.get(entry.getKey()); 1433 if (!PaintUtils.equal(p1, p2)) { 1434 return false; 1435 } 1436 } 1437 return true; 1438 } 1439 1440}