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     * Axis.java
029     * ---------
030     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Bill Kelemen; Nicolas Brodu
034     *
035     * Changes
036     * -------
037     * 21-Aug-2001 : Added standard header, fixed DOS encoding problem (DG);
038     * 18-Sep-2001 : Updated header (DG);
039     * 07-Nov-2001 : Allow null axis labels (DG);
040     *             : Added default font values (DG);
041     * 13-Nov-2001 : Modified the setPlot() method to check compatibility between
042     *               the axis and the plot (DG);
043     * 30-Nov-2001 : Changed default font from "Arial" --> "SansSerif" (DG);
044     * 06-Dec-2001 : Allow null in setPlot() method (BK);
045     * 06-Mar-2002 : Added AxisConstants interface (DG);
046     * 23-Apr-2002 : Added a visible property.  Moved drawVerticalString to
047     *               RefineryUtilities.  Added fixedDimension property for use in
048     *               combined plots (DG);
049     * 25-Jun-2002 : Removed unnecessary imports (DG);
050     * 05-Sep-2002 : Added attribute for tick mark paint (DG);
051     * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
052     * 07-Nov-2002 : Added attributes to control the inside and outside length of
053     *               the tick marks (DG);
054     * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
055     * 18-Nov-2002 : Added axis location to refreshTicks() parameters (DG);
056     * 15-Jan-2003 : Removed monolithic constructor (DG);
057     * 17-Jan-2003 : Moved plot classes to separate package (DG);
058     * 26-Mar-2003 : Implemented Serializable (DG);
059     * 03-Jul-2003 : Modified reserveSpace method (DG);
060     * 13-Aug-2003 : Implemented Cloneable (DG);
061     * 11-Sep-2003 : Took care of listeners while cloning (NB);
062     * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
063     * 06-Nov-2003 : Modified refreshTicks() signature (DG);
064     * 06-Jan-2004 : Added axis line attributes (DG);
065     * 16-Mar-2004 : Added plot state to draw() method (DG);
066     * 07-Apr-2004 : Modified text bounds calculation (DG);
067     * 18-May-2004 : Eliminated AxisConstants.java (DG);
068     * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities -->
069     *               TextUtilities (DG);
070     * 04-Oct-2004 : Modified getLabelEnclosure() method to treat an empty String
071     *               the same way as a null string - see bug 1026521 (DG);
072     * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
073     * 26-Apr-2005 : Removed LOGGER (DG);
074     * 01-Jun-2005 : Added hasListener() method for unit testing (DG);
075     * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
076     * ------------- JFREECHART 1.0.x ---------------------------------------------
077     * 22-Aug-2006 : API doc updates (DG);
078     * 06-Jun-2008 : Added setTickLabelInsets(RectangleInsets, boolean) (DG);
079     *
080     */
081    
082    package org.jfree.chart.axis;
083    
084    import java.awt.BasicStroke;
085    import java.awt.Color;
086    import java.awt.Font;
087    import java.awt.FontMetrics;
088    import java.awt.Graphics2D;
089    import java.awt.Paint;
090    import java.awt.Shape;
091    import java.awt.Stroke;
092    import java.awt.geom.AffineTransform;
093    import java.awt.geom.Line2D;
094    import java.awt.geom.Rectangle2D;
095    import java.io.IOException;
096    import java.io.ObjectInputStream;
097    import java.io.ObjectOutputStream;
098    import java.io.Serializable;
099    import java.util.Arrays;
100    import java.util.EventListener;
101    import java.util.List;
102    
103    import javax.swing.event.EventListenerList;
104    
105    import org.jfree.chart.event.AxisChangeEvent;
106    import org.jfree.chart.event.AxisChangeListener;
107    import org.jfree.chart.plot.Plot;
108    import org.jfree.chart.plot.PlotRenderingInfo;
109    import org.jfree.io.SerialUtilities;
110    import org.jfree.text.TextUtilities;
111    import org.jfree.ui.RectangleEdge;
112    import org.jfree.ui.RectangleInsets;
113    import org.jfree.ui.TextAnchor;
114    import org.jfree.util.ObjectUtilities;
115    import org.jfree.util.PaintUtilities;
116    
117    /**
118     * The base class for all axes in JFreeChart.  Subclasses are divided into
119     * those that display values ({@link ValueAxis}) and those that display
120     * categories ({@link CategoryAxis}).
121     */
122    public abstract class Axis implements Cloneable, Serializable {
123    
124        /** For serialization. */
125        private static final long serialVersionUID = 7719289504573298271L;
126    
127        /** The default axis visibility. */
128        public static final boolean DEFAULT_AXIS_VISIBLE = true;
129    
130        /** The default axis label font. */
131        public static final Font DEFAULT_AXIS_LABEL_FONT
132                = new Font("SansSerif", Font.PLAIN, 12);
133    
134        /** The default axis label paint. */
135        public static final Paint DEFAULT_AXIS_LABEL_PAINT = Color.black;
136    
137        /** The default axis label insets. */
138        public static final RectangleInsets DEFAULT_AXIS_LABEL_INSETS
139                = new RectangleInsets(3.0, 3.0, 3.0, 3.0);
140    
141        /** The default axis line paint. */
142        public static final Paint DEFAULT_AXIS_LINE_PAINT = Color.gray;
143    
144        /** The default axis line stroke. */
145        public static final Stroke DEFAULT_AXIS_LINE_STROKE = new BasicStroke(1.0f);
146    
147        /** The default tick labels visibility. */
148        public static final boolean DEFAULT_TICK_LABELS_VISIBLE = true;
149    
150        /** The default tick label font. */
151        public static final Font DEFAULT_TICK_LABEL_FONT
152                = new Font("SansSerif", Font.PLAIN, 10);
153    
154        /** The default tick label paint. */
155        public static final Paint DEFAULT_TICK_LABEL_PAINT = Color.black;
156    
157        /** The default tick label insets. */
158        public static final RectangleInsets DEFAULT_TICK_LABEL_INSETS
159                = new RectangleInsets(2.0, 4.0, 2.0, 4.0);
160    
161        /** The default tick marks visible. */
162        public static final boolean DEFAULT_TICK_MARKS_VISIBLE = true;
163    
164        /** The default tick stroke. */
165        public static final Stroke DEFAULT_TICK_MARK_STROKE = new BasicStroke(1);
166    
167        /** The default tick paint. */
168        public static final Paint DEFAULT_TICK_MARK_PAINT = Color.gray;
169    
170        /** The default tick mark inside length. */
171        public static final float DEFAULT_TICK_MARK_INSIDE_LENGTH = 0.0f;
172    
173        /** The default tick mark outside length. */
174        public static final float DEFAULT_TICK_MARK_OUTSIDE_LENGTH = 2.0f;
175    
176        /** A flag indicating whether or not the axis is visible. */
177        private boolean visible;
178    
179        /** The label for the axis. */
180        private String label;
181    
182        /** The font for displaying the axis label. */
183        private Font labelFont;
184    
185        /** The paint for drawing the axis label. */
186        private transient Paint labelPaint;
187    
188        /** The insets for the axis label. */
189        private RectangleInsets labelInsets;
190    
191        /** The label angle. */
192        private double labelAngle;
193    
194        /** A flag that controls whether or not the axis line is visible. */
195        private boolean axisLineVisible;
196    
197        /** The stroke used for the axis line. */
198        private transient Stroke axisLineStroke;
199    
200        /** The paint used for the axis line. */
201        private transient Paint axisLinePaint;
202    
203        /**
204         * A flag that indicates whether or not tick labels are visible for the
205         * axis.
206         */
207        private boolean tickLabelsVisible;
208    
209        /** The font used to display the tick labels. */
210        private Font tickLabelFont;
211    
212        /** The color used to display the tick labels. */
213        private transient Paint tickLabelPaint;
214    
215        /** The blank space around each tick label. */
216        private RectangleInsets tickLabelInsets;
217    
218        /**
219         * A flag that indicates whether or not tick marks are visible for the
220         * axis.
221         */
222        private boolean tickMarksVisible;
223    
224        /** The length of the tick mark inside the data area (zero permitted). */
225        private float tickMarkInsideLength;
226    
227        /** The length of the tick mark outside the data area (zero permitted). */
228        private float tickMarkOutsideLength;
229    
230        /** The stroke used to draw tick marks. */
231        private transient Stroke tickMarkStroke;
232    
233        /** The paint used to draw tick marks. */
234        private transient Paint tickMarkPaint;
235    
236        /** The fixed (horizontal or vertical) dimension for the axis. */
237        private double fixedDimension;
238    
239        /**
240         * A reference back to the plot that the axis is assigned to (can be
241         * <code>null</code>).
242         */
243        private transient Plot plot;
244    
245        /** Storage for registered listeners. */
246        private transient EventListenerList listenerList;
247    
248        /**
249         * Constructs an axis, using default values where necessary.
250         *
251         * @param label  the axis label (<code>null</code> permitted).
252         */
253        protected Axis(String label) {
254    
255            this.label = label;
256            this.visible = DEFAULT_AXIS_VISIBLE;
257            this.labelFont = DEFAULT_AXIS_LABEL_FONT;
258            this.labelPaint = DEFAULT_AXIS_LABEL_PAINT;
259            this.labelInsets = DEFAULT_AXIS_LABEL_INSETS;
260            this.labelAngle = 0.0;
261    
262            this.axisLineVisible = true;
263            this.axisLinePaint = DEFAULT_AXIS_LINE_PAINT;
264            this.axisLineStroke = DEFAULT_AXIS_LINE_STROKE;
265    
266            this.tickLabelsVisible = DEFAULT_TICK_LABELS_VISIBLE;
267            this.tickLabelFont = DEFAULT_TICK_LABEL_FONT;
268            this.tickLabelPaint = DEFAULT_TICK_LABEL_PAINT;
269            this.tickLabelInsets = DEFAULT_TICK_LABEL_INSETS;
270    
271            this.tickMarksVisible = DEFAULT_TICK_MARKS_VISIBLE;
272            this.tickMarkStroke = DEFAULT_TICK_MARK_STROKE;
273            this.tickMarkPaint = DEFAULT_TICK_MARK_PAINT;
274            this.tickMarkInsideLength = DEFAULT_TICK_MARK_INSIDE_LENGTH;
275            this.tickMarkOutsideLength = DEFAULT_TICK_MARK_OUTSIDE_LENGTH;
276    
277            this.plot = null;
278    
279            this.listenerList = new EventListenerList();
280    
281        }
282    
283        /**
284         * Returns <code>true</code> if the axis is visible, and
285         * <code>false</code> otherwise.
286         *
287         * @return A boolean.
288         *
289         * @see #setVisible(boolean)
290         */
291        public boolean isVisible() {
292            return this.visible;
293        }
294    
295        /**
296         * Sets a flag that controls whether or not the axis is visible and sends
297         * an {@link AxisChangeEvent} to all registered listeners.
298         *
299         * @param flag  the flag.
300         *
301         * @see #isVisible()
302         */
303        public void setVisible(boolean flag) {
304            if (flag != this.visible) {
305                this.visible = flag;
306                notifyListeners(new AxisChangeEvent(this));
307            }
308        }
309    
310        /**
311         * Returns the label for the axis.
312         *
313         * @return The label for the axis (<code>null</code> possible).
314         *
315         * @see #getLabelFont()
316         * @see #getLabelPaint()
317         * @see #setLabel(String)
318         */
319        public String getLabel() {
320            return this.label;
321        }
322    
323        /**
324         * Sets the label for the axis and sends an {@link AxisChangeEvent} to all
325         * registered listeners.
326         *
327         * @param label  the new label (<code>null</code> permitted).
328         *
329         * @see #getLabel()
330         * @see #setLabelFont(Font)
331         * @see #setLabelPaint(Paint)
332         */
333        public void setLabel(String label) {
334    
335            String existing = this.label;
336            if (existing != null) {
337                if (!existing.equals(label)) {
338                    this.label = label;
339                    notifyListeners(new AxisChangeEvent(this));
340                }
341            }
342            else {
343                if (label != null) {
344                    this.label = label;
345                    notifyListeners(new AxisChangeEvent(this));
346                }
347            }
348    
349        }
350    
351        /**
352         * Returns the font for the axis label.
353         *
354         * @return The font (never <code>null</code>).
355         *
356         * @see #setLabelFont(Font)
357         */
358        public Font getLabelFont() {
359            return this.labelFont;
360        }
361    
362        /**
363         * Sets the font for the axis label and sends an {@link AxisChangeEvent}
364         * to all registered listeners.
365         *
366         * @param font  the font (<code>null</code> not permitted).
367         *
368         * @see #getLabelFont()
369         */
370        public void setLabelFont(Font font) {
371            if (font == null) {
372                throw new IllegalArgumentException("Null 'font' argument.");
373            }
374            if (!this.labelFont.equals(font)) {
375                this.labelFont = font;
376                notifyListeners(new AxisChangeEvent(this));
377            }
378        }
379    
380        /**
381         * Returns the color/shade used to draw the axis label.
382         *
383         * @return The paint (never <code>null</code>).
384         *
385         * @see #setLabelPaint(Paint)
386         */
387        public Paint getLabelPaint() {
388            return this.labelPaint;
389        }
390    
391        /**
392         * Sets the paint used to draw the axis label and sends an
393         * {@link AxisChangeEvent} to all registered listeners.
394         *
395         * @param paint  the paint (<code>null</code> not permitted).
396         *
397         * @see #getLabelPaint()
398         */
399        public void setLabelPaint(Paint paint) {
400            if (paint == null) {
401                throw new IllegalArgumentException("Null 'paint' argument.");
402            }
403            this.labelPaint = paint;
404            notifyListeners(new AxisChangeEvent(this));
405        }
406    
407        /**
408         * Returns the insets for the label (that is, the amount of blank space
409         * that should be left around the label).
410         *
411         * @return The label insets (never <code>null</code>).
412         *
413         * @see #setLabelInsets(RectangleInsets)
414         */
415        public RectangleInsets getLabelInsets() {
416            return this.labelInsets;
417        }
418    
419        /**
420         * Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
421         * to all registered listeners.
422         *
423         * @param insets  the insets (<code>null</code> not permitted).
424         *
425         * @see #getLabelInsets()
426         */
427        public void setLabelInsets(RectangleInsets insets) {
428            setLabelInsets(insets, true);
429        }
430    
431        /**
432         * Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
433         * to all registered listeners.
434         *
435         * @param insets  the insets (<code>null</code> not permitted).
436         * @param notify  notify listeners?
437         *
438         * @since 1.0.10
439         */
440        public void setLabelInsets(RectangleInsets insets, boolean notify) {
441            if (insets == null) {
442                throw new IllegalArgumentException("Null 'insets' argument.");
443            }
444            if (!insets.equals(this.labelInsets)) {
445                this.labelInsets = insets;
446                if (notify) {
447                    notifyListeners(new AxisChangeEvent(this));
448                }
449            }
450        }
451    
452        /**
453         * Returns the angle of the axis label.
454         *
455         * @return The angle (in radians).
456         *
457         * @see #setLabelAngle(double)
458         */
459        public double getLabelAngle() {
460            return this.labelAngle;
461        }
462    
463        /**
464         * Sets the angle for the label and sends an {@link AxisChangeEvent} to all
465         * registered listeners.
466         *
467         * @param angle  the angle (in radians).
468         *
469         * @see #getLabelAngle()
470         */
471        public void setLabelAngle(double angle) {
472            this.labelAngle = angle;
473            notifyListeners(new AxisChangeEvent(this));
474        }
475    
476        /**
477         * A flag that controls whether or not the axis line is drawn.
478         *
479         * @return A boolean.
480         *
481         * @see #getAxisLinePaint()
482         * @see #getAxisLineStroke()
483         * @see #setAxisLineVisible(boolean)
484         */
485        public boolean isAxisLineVisible() {
486            return this.axisLineVisible;
487        }
488    
489        /**
490         * Sets a flag that controls whether or not the axis line is visible and
491         * sends an {@link AxisChangeEvent} to all registered listeners.
492         *
493         * @param visible  the flag.
494         *
495         * @see #isAxisLineVisible()
496         * @see #setAxisLinePaint(Paint)
497         * @see #setAxisLineStroke(Stroke)
498         */
499        public void setAxisLineVisible(boolean visible) {
500            this.axisLineVisible = visible;
501            notifyListeners(new AxisChangeEvent(this));
502        }
503    
504        /**
505         * Returns the paint used to draw the axis line.
506         *
507         * @return The paint (never <code>null</code>).
508         *
509         * @see #setAxisLinePaint(Paint)
510         */
511        public Paint getAxisLinePaint() {
512            return this.axisLinePaint;
513        }
514    
515        /**
516         * Sets the paint used to draw the axis line and sends an
517         * {@link AxisChangeEvent} to all registered listeners.
518         *
519         * @param paint  the paint (<code>null</code> not permitted).
520         *
521         * @see #getAxisLinePaint()
522         */
523        public void setAxisLinePaint(Paint paint) {
524            if (paint == null) {
525                throw new IllegalArgumentException("Null 'paint' argument.");
526            }
527            this.axisLinePaint = paint;
528            notifyListeners(new AxisChangeEvent(this));
529        }
530    
531        /**
532         * Returns the stroke used to draw the axis line.
533         *
534         * @return The stroke (never <code>null</code>).
535         *
536         * @see #setAxisLineStroke(Stroke)
537         */
538        public Stroke getAxisLineStroke() {
539            return this.axisLineStroke;
540        }
541    
542        /**
543         * Sets the stroke used to draw the axis line and sends an
544         * {@link AxisChangeEvent} to all registered listeners.
545         *
546         * @param stroke  the stroke (<code>null</code> not permitted).
547         *
548         * @see #getAxisLineStroke()
549         */
550        public void setAxisLineStroke(Stroke stroke) {
551            if (stroke == null) {
552                throw new IllegalArgumentException("Null 'stroke' argument.");
553            }
554            this.axisLineStroke = stroke;
555            notifyListeners(new AxisChangeEvent(this));
556        }
557    
558        /**
559         * Returns a flag indicating whether or not the tick labels are visible.
560         *
561         * @return The flag.
562         *
563         * @see #getTickLabelFont()
564         * @see #getTickLabelPaint()
565         * @see #setTickLabelsVisible(boolean)
566         */
567        public boolean isTickLabelsVisible() {
568            return this.tickLabelsVisible;
569        }
570    
571        /**
572         * Sets the flag that determines whether or not the tick labels are
573         * visible and sends an {@link AxisChangeEvent} to all registered
574         * listeners.
575         *
576         * @param flag  the flag.
577         *
578         * @see #isTickLabelsVisible()
579         * @see #setTickLabelFont(Font)
580         * @see #setTickLabelPaint(Paint)
581         */
582        public void setTickLabelsVisible(boolean flag) {
583    
584            if (flag != this.tickLabelsVisible) {
585                this.tickLabelsVisible = flag;
586                notifyListeners(new AxisChangeEvent(this));
587            }
588    
589        }
590    
591        /**
592         * Returns the font used for the tick labels (if showing).
593         *
594         * @return The font (never <code>null</code>).
595         *
596         * @see #setTickLabelFont(Font)
597         */
598        public Font getTickLabelFont() {
599            return this.tickLabelFont;
600        }
601    
602        /**
603         * Sets the font for the tick labels and sends an {@link AxisChangeEvent}
604         * to all registered listeners.
605         *
606         * @param font  the font (<code>null</code> not allowed).
607         *
608         * @see #getTickLabelFont()
609         */
610        public void setTickLabelFont(Font font) {
611    
612            if (font == null) {
613                throw new IllegalArgumentException("Null 'font' argument.");
614            }
615    
616            if (!this.tickLabelFont.equals(font)) {
617                this.tickLabelFont = font;
618                notifyListeners(new AxisChangeEvent(this));
619            }
620    
621        }
622    
623        /**
624         * Returns the color/shade used for the tick labels.
625         *
626         * @return The paint used for the tick labels.
627         *
628         * @see #setTickLabelPaint(Paint)
629         */
630        public Paint getTickLabelPaint() {
631            return this.tickLabelPaint;
632        }
633    
634        /**
635         * Sets the paint used to draw tick labels (if they are showing) and
636         * sends an {@link AxisChangeEvent} to all registered listeners.
637         *
638         * @param paint  the paint (<code>null</code> not permitted).
639         *
640         * @see #getTickLabelPaint()
641         */
642        public void setTickLabelPaint(Paint paint) {
643            if (paint == null) {
644                throw new IllegalArgumentException("Null 'paint' argument.");
645            }
646            this.tickLabelPaint = paint;
647            notifyListeners(new AxisChangeEvent(this));
648        }
649    
650        /**
651         * Returns the insets for the tick labels.
652         *
653         * @return The insets (never <code>null</code>).
654         *
655         * @see #setTickLabelInsets(RectangleInsets)
656         */
657        public RectangleInsets getTickLabelInsets() {
658            return this.tickLabelInsets;
659        }
660    
661        /**
662         * Sets the insets for the tick labels and sends an {@link AxisChangeEvent}
663         * to all registered listeners.
664         *
665         * @param insets  the insets (<code>null</code> not permitted).
666         *
667         * @see #getTickLabelInsets()
668         */
669        public void setTickLabelInsets(RectangleInsets insets) {
670            if (insets == null) {
671                throw new IllegalArgumentException("Null 'insets' argument.");
672            }
673            if (!this.tickLabelInsets.equals(insets)) {
674                this.tickLabelInsets = insets;
675                notifyListeners(new AxisChangeEvent(this));
676            }
677        }
678    
679        /**
680         * Returns the flag that indicates whether or not the tick marks are
681         * showing.
682         *
683         * @return The flag that indicates whether or not the tick marks are
684         *         showing.
685         *
686         * @see #setTickMarksVisible(boolean)
687         */
688        public boolean isTickMarksVisible() {
689            return this.tickMarksVisible;
690        }
691    
692        /**
693         * Sets the flag that indicates whether or not the tick marks are showing
694         * and sends an {@link AxisChangeEvent} to all registered listeners.
695         *
696         * @param flag  the flag.
697         *
698         * @see #isTickMarksVisible()
699         */
700        public void setTickMarksVisible(boolean flag) {
701            if (flag != this.tickMarksVisible) {
702                this.tickMarksVisible = flag;
703                notifyListeners(new AxisChangeEvent(this));
704            }
705        }
706    
707        /**
708         * Returns the inside length of the tick marks.
709         *
710         * @return The length.
711         *
712         * @see #getTickMarkOutsideLength()
713         * @see #setTickMarkInsideLength(float)
714         */
715        public float getTickMarkInsideLength() {
716            return this.tickMarkInsideLength;
717        }
718    
719        /**
720         * Sets the inside length of the tick marks and sends
721         * an {@link AxisChangeEvent} to all registered listeners.
722         *
723         * @param length  the new length.
724         *
725         * @see #getTickMarkInsideLength()
726         */
727        public void setTickMarkInsideLength(float length) {
728            this.tickMarkInsideLength = length;
729            notifyListeners(new AxisChangeEvent(this));
730        }
731    
732        /**
733         * Returns the outside length of the tick marks.
734         *
735         * @return The length.
736         *
737         * @see #getTickMarkInsideLength()
738         * @see #setTickMarkOutsideLength(float)
739         */
740        public float getTickMarkOutsideLength() {
741            return this.tickMarkOutsideLength;
742        }
743    
744        /**
745         * Sets the outside length of the tick marks and sends
746         * an {@link AxisChangeEvent} to all registered listeners.
747         *
748         * @param length  the new length.
749         *
750         * @see #getTickMarkInsideLength()
751         */
752        public void setTickMarkOutsideLength(float length) {
753            this.tickMarkOutsideLength = length;
754            notifyListeners(new AxisChangeEvent(this));
755        }
756    
757        /**
758         * Returns the stroke used to draw tick marks.
759         *
760         * @return The stroke (never <code>null</code>).
761         *
762         * @see #setTickMarkStroke(Stroke)
763         */
764        public Stroke getTickMarkStroke() {
765            return this.tickMarkStroke;
766        }
767    
768        /**
769         * Sets the stroke used to draw tick marks and sends
770         * an {@link AxisChangeEvent} to all registered listeners.
771         *
772         * @param stroke  the stroke (<code>null</code> not permitted).
773         *
774         * @see #getTickMarkStroke()
775         */
776        public void setTickMarkStroke(Stroke stroke) {
777            if (stroke == null) {
778                throw new IllegalArgumentException("Null 'stroke' argument.");
779            }
780            if (!this.tickMarkStroke.equals(stroke)) {
781                this.tickMarkStroke = stroke;
782                notifyListeners(new AxisChangeEvent(this));
783            }
784        }
785    
786        /**
787         * Returns the paint used to draw tick marks (if they are showing).
788         *
789         * @return The paint (never <code>null</code>).
790         *
791         * @see #setTickMarkPaint(Paint)
792         */
793        public Paint getTickMarkPaint() {
794            return this.tickMarkPaint;
795        }
796    
797        /**
798         * Sets the paint used to draw tick marks and sends an
799         * {@link AxisChangeEvent} to all registered listeners.
800         *
801         * @param paint  the paint (<code>null</code> not permitted).
802         *
803         * @see #getTickMarkPaint()
804         */
805        public void setTickMarkPaint(Paint paint) {
806            if (paint == null) {
807                throw new IllegalArgumentException("Null 'paint' argument.");
808            }
809            this.tickMarkPaint = paint;
810            notifyListeners(new AxisChangeEvent(this));
811        }
812    
813        /**
814         * Returns the plot that the axis is assigned to.  This method will return
815         * <code>null</code> if the axis is not currently assigned to a plot.
816         *
817         * @return The plot that the axis is assigned to (possibly
818         *         <code>null</code>).
819         *
820         * @see #setPlot(Plot)
821         */
822        public Plot getPlot() {
823            return this.plot;
824        }
825    
826        /**
827         * Sets a reference to the plot that the axis is assigned to.
828         * <P>
829         * This method is used internally, you shouldn't need to call it yourself.
830         *
831         * @param plot  the plot.
832         *
833         * @see #getPlot()
834         */
835        public void setPlot(Plot plot) {
836            this.plot = plot;
837            configure();
838        }
839    
840        /**
841         * Returns the fixed dimension for the axis.
842         *
843         * @return The fixed dimension.
844         *
845         * @see #setFixedDimension(double)
846         */
847        public double getFixedDimension() {
848            return this.fixedDimension;
849        }
850    
851        /**
852         * Sets the fixed dimension for the axis.
853         * <P>
854         * This is used when combining more than one plot on a chart.  In this case,
855         * there may be several axes that need to have the same height or width so
856         * that they are aligned.  This method is used to fix a dimension for the
857         * axis (the context determines whether the dimension is horizontal or
858         * vertical).
859         *
860         * @param dimension  the fixed dimension.
861         *
862         * @see #getFixedDimension()
863         */
864        public void setFixedDimension(double dimension) {
865            this.fixedDimension = dimension;
866        }
867    
868        /**
869         * Configures the axis to work with the current plot.  Override this method
870         * to perform any special processing (such as auto-rescaling).
871         */
872        public abstract void configure();
873    
874        /**
875         * Estimates the space (height or width) required to draw the axis.
876         *
877         * @param g2  the graphics device.
878         * @param plot  the plot that the axis belongs to.
879         * @param plotArea  the area within which the plot (including axes) should
880         *                  be drawn.
881         * @param edge  the axis location.
882         * @param space  space already reserved.
883         *
884         * @return The space required to draw the axis (including pre-reserved
885         *         space).
886         */
887        public abstract AxisSpace reserveSpace(Graphics2D g2, Plot plot,
888                                               Rectangle2D plotArea,
889                                               RectangleEdge edge,
890                                               AxisSpace space);
891    
892        /**
893         * Draws the axis on a Java 2D graphics device (such as the screen or a
894         * printer).
895         *
896         * @param g2  the graphics device (<code>null</code> not permitted).
897         * @param cursor  the cursor location (determines where to draw the axis).
898         * @param plotArea  the area within which the axes and plot should be drawn.
899         * @param dataArea  the area within which the data should be drawn.
900         * @param edge  the axis location (<code>null</code> not permitted).
901         * @param plotState  collects information about the plot
902         *                   (<code>null</code> permitted).
903         *
904         * @return The axis state (never <code>null</code>).
905         */
906        public abstract AxisState draw(Graphics2D g2,
907                                       double cursor,
908                                       Rectangle2D plotArea,
909                                       Rectangle2D dataArea,
910                                       RectangleEdge edge,
911                                       PlotRenderingInfo plotState);
912    
913        /**
914         * Calculates the positions of the ticks for the axis, storing the results
915         * in the tick list (ready for drawing).
916         *
917         * @param g2  the graphics device.
918         * @param state  the axis state.
919         * @param dataArea  the area inside the axes.
920         * @param edge  the edge on which the axis is located.
921         *
922         * @return The list of ticks.
923         */
924        public abstract List refreshTicks(Graphics2D g2,
925                                          AxisState state,
926                                          Rectangle2D dataArea,
927                                          RectangleEdge edge);
928    
929        /**
930         * Registers an object for notification of changes to the axis.
931         *
932         * @param listener  the object that is being registered.
933         *
934         * @see #removeChangeListener(AxisChangeListener)
935         */
936        public void addChangeListener(AxisChangeListener listener) {
937            this.listenerList.add(AxisChangeListener.class, listener);
938        }
939    
940        /**
941         * Deregisters an object for notification of changes to the axis.
942         *
943         * @param listener  the object to deregister.
944         *
945         * @see #addChangeListener(AxisChangeListener)
946         */
947        public void removeChangeListener(AxisChangeListener listener) {
948            this.listenerList.remove(AxisChangeListener.class, listener);
949        }
950    
951        /**
952         * Returns <code>true</code> if the specified object is registered with
953         * the dataset as a listener.  Most applications won't need to call this
954         * method, it exists mainly for use by unit testing code.
955         *
956         * @param listener  the listener.
957         *
958         * @return A boolean.
959         */
960        public boolean hasListener(EventListener listener) {
961            List list = Arrays.asList(this.listenerList.getListenerList());
962            return list.contains(listener);
963        }
964    
965        /**
966         * Notifies all registered listeners that the axis has changed.
967         * The AxisChangeEvent provides information about the change.
968         *
969         * @param event  information about the change to the axis.
970         */
971        protected void notifyListeners(AxisChangeEvent event) {
972    
973            Object[] listeners = this.listenerList.getListenerList();
974            for (int i = listeners.length - 2; i >= 0; i -= 2) {
975                if (listeners[i] == AxisChangeListener.class) {
976                    ((AxisChangeListener) listeners[i + 1]).axisChanged(event);
977                }
978            }
979    
980        }
981    
982        /**
983         * Returns a rectangle that encloses the axis label.  This is typically
984         * used for layout purposes (it gives the maximum dimensions of the label).
985         *
986         * @param g2  the graphics device.
987         * @param edge  the edge of the plot area along which the axis is measuring.
988         *
989         * @return The enclosing rectangle.
990         */
991        protected Rectangle2D getLabelEnclosure(Graphics2D g2, RectangleEdge edge) {
992    
993            Rectangle2D result = new Rectangle2D.Double();
994            String axisLabel = getLabel();
995            if (axisLabel != null && !axisLabel.equals("")) {
996                FontMetrics fm = g2.getFontMetrics(getLabelFont());
997                Rectangle2D bounds = TextUtilities.getTextBounds(axisLabel, g2, fm);
998                RectangleInsets insets = getLabelInsets();
999                bounds = insets.createOutsetRectangle(bounds);
1000                double angle = getLabelAngle();
1001                if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {
1002                    angle = angle - Math.PI / 2.0;
1003                }
1004                double x = bounds.getCenterX();
1005                double y = bounds.getCenterY();
1006                AffineTransform transformer
1007                    = AffineTransform.getRotateInstance(angle, x, y);
1008                Shape labelBounds = transformer.createTransformedShape(bounds);
1009                result = labelBounds.getBounds2D();
1010            }
1011    
1012            return result;
1013    
1014        }
1015    
1016        /**
1017         * Draws the axis label.
1018         *
1019         * @param label  the label text.
1020         * @param g2  the graphics device.
1021         * @param plotArea  the plot area.
1022         * @param dataArea  the area inside the axes.
1023         * @param edge  the location of the axis.
1024         * @param state  the axis state (<code>null</code> not permitted).
1025         *
1026         * @return Information about the axis.
1027         */
1028        protected AxisState drawLabel(String label,
1029                                      Graphics2D g2,
1030                                      Rectangle2D plotArea,
1031                                      Rectangle2D dataArea,
1032                                      RectangleEdge edge,
1033                                      AxisState state) {
1034    
1035            // it is unlikely that 'state' will be null, but check anyway...
1036            if (state == null) {
1037                throw new IllegalArgumentException("Null 'state' argument.");
1038            }
1039    
1040            if ((label == null) || (label.equals(""))) {
1041                return state;
1042            }
1043    
1044            Font font = getLabelFont();
1045            RectangleInsets insets = getLabelInsets();
1046            g2.setFont(font);
1047            g2.setPaint(getLabelPaint());
1048            FontMetrics fm = g2.getFontMetrics();
1049            Rectangle2D labelBounds = TextUtilities.getTextBounds(label, g2, fm);
1050    
1051            if (edge == RectangleEdge.TOP) {
1052    
1053                AffineTransform t = AffineTransform.getRotateInstance(
1054                        getLabelAngle(), labelBounds.getCenterX(),
1055                        labelBounds.getCenterY());
1056                Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1057                labelBounds = rotatedLabelBounds.getBounds2D();
1058                double labelx = dataArea.getCenterX();
1059                double labely = state.getCursor() - insets.getBottom()
1060                                - labelBounds.getHeight() / 2.0;
1061                TextUtilities.drawRotatedString(label, g2, (float) labelx,
1062                        (float) labely, TextAnchor.CENTER, getLabelAngle(),
1063                        TextAnchor.CENTER);
1064                state.cursorUp(insets.getTop() + labelBounds.getHeight()
1065                        + insets.getBottom());
1066    
1067            }
1068            else if (edge == RectangleEdge.BOTTOM) {
1069    
1070                AffineTransform t = AffineTransform.getRotateInstance(
1071                        getLabelAngle(), labelBounds.getCenterX(),
1072                        labelBounds.getCenterY());
1073                Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1074                labelBounds = rotatedLabelBounds.getBounds2D();
1075                double labelx = dataArea.getCenterX();
1076                double labely = state.getCursor()
1077                                + insets.getTop() + labelBounds.getHeight() / 2.0;
1078                TextUtilities.drawRotatedString(label, g2, (float) labelx,
1079                        (float) labely, TextAnchor.CENTER, getLabelAngle(),
1080                        TextAnchor.CENTER);
1081                state.cursorDown(insets.getTop() + labelBounds.getHeight()
1082                        + insets.getBottom());
1083    
1084            }
1085            else if (edge == RectangleEdge.LEFT) {
1086    
1087                AffineTransform t = AffineTransform.getRotateInstance(
1088                        getLabelAngle() - Math.PI / 2.0, labelBounds.getCenterX(),
1089                        labelBounds.getCenterY());
1090                Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1091                labelBounds = rotatedLabelBounds.getBounds2D();
1092                double labelx = state.getCursor()
1093                                - insets.getRight() - labelBounds.getWidth() / 2.0;
1094                double labely = dataArea.getCenterY();
1095                TextUtilities.drawRotatedString(label, g2, (float) labelx,
1096                        (float) labely, TextAnchor.CENTER,
1097                        getLabelAngle() - Math.PI / 2.0, TextAnchor.CENTER);
1098                state.cursorLeft(insets.getLeft() + labelBounds.getWidth()
1099                        + insets.getRight());
1100            }
1101            else if (edge == RectangleEdge.RIGHT) {
1102    
1103                AffineTransform t = AffineTransform.getRotateInstance(
1104                        getLabelAngle() + Math.PI / 2.0,
1105                        labelBounds.getCenterX(), labelBounds.getCenterY());
1106                Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1107                labelBounds = rotatedLabelBounds.getBounds2D();
1108                double labelx = state.getCursor()
1109                                + insets.getLeft() + labelBounds.getWidth() / 2.0;
1110                double labely = dataArea.getY() + dataArea.getHeight() / 2.0;
1111                TextUtilities.drawRotatedString(label, g2, (float) labelx,
1112                        (float) labely, TextAnchor.CENTER,
1113                        getLabelAngle() + Math.PI / 2.0, TextAnchor.CENTER);
1114                state.cursorRight(insets.getLeft() + labelBounds.getWidth()
1115                        + insets.getRight());
1116    
1117            }
1118    
1119            return state;
1120    
1121        }
1122    
1123        /**
1124         * Draws an axis line at the current cursor position and edge.
1125         *
1126         * @param g2  the graphics device.
1127         * @param cursor  the cursor position.
1128         * @param dataArea  the data area.
1129         * @param edge  the edge.
1130         */
1131        protected void drawAxisLine(Graphics2D g2, double cursor,
1132                Rectangle2D dataArea, RectangleEdge edge) {
1133    
1134            Line2D axisLine = null;
1135            if (edge == RectangleEdge.TOP) {
1136                axisLine = new Line2D.Double(dataArea.getX(), cursor,
1137                        dataArea.getMaxX(), cursor);
1138            }
1139            else if (edge == RectangleEdge.BOTTOM) {
1140                axisLine = new Line2D.Double(dataArea.getX(), cursor,
1141                        dataArea.getMaxX(), cursor);
1142            }
1143            else if (edge == RectangleEdge.LEFT) {
1144                axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
1145                        dataArea.getMaxY());
1146            }
1147            else if (edge == RectangleEdge.RIGHT) {
1148                axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
1149                        dataArea.getMaxY());
1150            }
1151            g2.setPaint(this.axisLinePaint);
1152            g2.setStroke(this.axisLineStroke);
1153            g2.draw(axisLine);
1154    
1155        }
1156    
1157        /**
1158         * Returns a clone of the axis.
1159         *
1160         * @return A clone.
1161         *
1162         * @throws CloneNotSupportedException if some component of the axis does
1163         *         not support cloning.
1164         */
1165        public Object clone() throws CloneNotSupportedException {
1166            Axis clone = (Axis) super.clone();
1167            // It's up to the plot which clones up to restore the correct references
1168            clone.plot = null;
1169            clone.listenerList = new EventListenerList();
1170            return clone;
1171        }
1172    
1173        /**
1174         * Tests this axis for equality with another object.
1175         *
1176         * @param obj  the object (<code>null</code> permitted).
1177         *
1178         * @return <code>true</code> or <code>false</code>.
1179         */
1180        public boolean equals(Object obj) {
1181            if (obj == this) {
1182                return true;
1183            }
1184            if (!(obj instanceof Axis)) {
1185                return false;
1186            }
1187            Axis that = (Axis) obj;
1188            if (this.visible != that.visible) {
1189                return false;
1190            }
1191            if (!ObjectUtilities.equal(this.label, that.label)) {
1192                return false;
1193            }
1194            if (!ObjectUtilities.equal(this.labelFont, that.labelFont)) {
1195                return false;
1196            }
1197            if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
1198                return false;
1199            }
1200            if (!ObjectUtilities.equal(this.labelInsets, that.labelInsets)) {
1201                return false;
1202            }
1203            if (this.labelAngle != that.labelAngle) {
1204                return false;
1205            }
1206            if (this.axisLineVisible != that.axisLineVisible) {
1207                return false;
1208            }
1209            if (!ObjectUtilities.equal(this.axisLineStroke, that.axisLineStroke)) {
1210                return false;
1211            }
1212            if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
1213                return false;
1214            }
1215            if (this.tickLabelsVisible != that.tickLabelsVisible) {
1216                return false;
1217            }
1218            if (!ObjectUtilities.equal(this.tickLabelFont, that.tickLabelFont)) {
1219                return false;
1220            }
1221            if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) {
1222                return false;
1223            }
1224            if (!ObjectUtilities.equal(
1225                this.tickLabelInsets, that.tickLabelInsets
1226            )) {
1227                return false;
1228            }
1229            if (this.tickMarksVisible != that.tickMarksVisible) {
1230                return false;
1231            }
1232            if (this.tickMarkInsideLength != that.tickMarkInsideLength) {
1233                return false;
1234            }
1235            if (this.tickMarkOutsideLength != that.tickMarkOutsideLength) {
1236                return false;
1237            }
1238            if (!PaintUtilities.equal(this.tickMarkPaint, that.tickMarkPaint)) {
1239                return false;
1240            }
1241            if (!ObjectUtilities.equal(this.tickMarkStroke, that.tickMarkStroke)) {
1242                return false;
1243            }
1244            if (this.fixedDimension != that.fixedDimension) {
1245                return false;
1246            }
1247            return true;
1248        }
1249    
1250        /**
1251         * Provides serialization support.
1252         *
1253         * @param stream  the output stream.
1254         *
1255         * @throws IOException  if there is an I/O error.
1256         */
1257        private void writeObject(ObjectOutputStream stream) throws IOException {
1258            stream.defaultWriteObject();
1259            SerialUtilities.writePaint(this.labelPaint, stream);
1260            SerialUtilities.writePaint(this.tickLabelPaint, stream);
1261            SerialUtilities.writeStroke(this.axisLineStroke, stream);
1262            SerialUtilities.writePaint(this.axisLinePaint, stream);
1263            SerialUtilities.writeStroke(this.tickMarkStroke, stream);
1264            SerialUtilities.writePaint(this.tickMarkPaint, stream);
1265        }
1266    
1267        /**
1268         * Provides serialization support.
1269         *
1270         * @param stream  the input stream.
1271         *
1272         * @throws IOException  if there is an I/O error.
1273         * @throws ClassNotFoundException  if there is a classpath problem.
1274         */
1275        private void readObject(ObjectInputStream stream)
1276            throws IOException, ClassNotFoundException {
1277            stream.defaultReadObject();
1278            this.labelPaint = SerialUtilities.readPaint(stream);
1279            this.tickLabelPaint = SerialUtilities.readPaint(stream);
1280            this.axisLineStroke = SerialUtilities.readStroke(stream);
1281            this.axisLinePaint = SerialUtilities.readPaint(stream);
1282            this.tickMarkStroke = SerialUtilities.readStroke(stream);
1283            this.tickMarkPaint = SerialUtilities.readPaint(stream);
1284            this.listenerList = new EventListenerList();
1285        }
1286    
1287    }