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