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 &lt;= itemMargin &lt; 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 &lt;= itemMargin &lt; 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}