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