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