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