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