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