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