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