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