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