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