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