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