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