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