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