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