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