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