001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------
028 * Axis.java
029 * ---------
030 * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Bill Kelemen;
034 *                   Nicolas Brodu;
035 *                   Peter Kolb (patches 1934255 and 2603321);
036 *                   Andrew Mickish (patch 1870189);
037 *
038 * Changes
039 * -------
040 * 21-Aug-2001 : Added standard header, fixed DOS encoding problem (DG);
041 * 18-Sep-2001 : Updated header (DG);
042 * 07-Nov-2001 : Allow null axis labels (DG);
043 *             : Added default font values (DG);
044 * 13-Nov-2001 : Modified the setPlot() method to check compatibility between
045 *               the axis and the plot (DG);
046 * 30-Nov-2001 : Changed default font from "Arial" --> "SansSerif" (DG);
047 * 06-Dec-2001 : Allow null in setPlot() method (BK);
048 * 06-Mar-2002 : Added AxisConstants interface (DG);
049 * 23-Apr-2002 : Added a visible property.  Moved drawVerticalString to
050 *               RefineryUtilities.  Added fixedDimension property for use in
051 *               combined plots (DG);
052 * 25-Jun-2002 : Removed unnecessary imports (DG);
053 * 05-Sep-2002 : Added attribute for tick mark paint (DG);
054 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
055 * 07-Nov-2002 : Added attributes to control the inside and outside length of
056 *               the tick marks (DG);
057 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
058 * 18-Nov-2002 : Added axis location to refreshTicks() parameters (DG);
059 * 15-Jan-2003 : Removed monolithic constructor (DG);
060 * 17-Jan-2003 : Moved plot classes to separate package (DG);
061 * 26-Mar-2003 : Implemented Serializable (DG);
062 * 03-Jul-2003 : Modified reserveSpace method (DG);
063 * 13-Aug-2003 : Implemented Cloneable (DG);
064 * 11-Sep-2003 : Took care of listeners while cloning (NB);
065 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
066 * 06-Nov-2003 : Modified refreshTicks() signature (DG);
067 * 06-Jan-2004 : Added axis line attributes (DG);
068 * 16-Mar-2004 : Added plot state to draw() method (DG);
069 * 07-Apr-2004 : Modified text bounds calculation (DG);
070 * 18-May-2004 : Eliminated AxisConstants.java (DG);
071 * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities -->
072 *               TextUtilities (DG);
073 * 04-Oct-2004 : Modified getLabelEnclosure() method to treat an empty String
074 *               the same way as a null string - see bug 1026521 (DG);
075 * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
076 * 26-Apr-2005 : Removed LOGGER (DG);
077 * 01-Jun-2005 : Added hasListener() method for unit testing (DG);
078 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
079 * ------------- JFREECHART 1.0.x ---------------------------------------------
080 * 22-Aug-2006 : API doc updates (DG);
081 * 06-Jun-2008 : Added setTickLabelInsets(RectangleInsets, boolean) (DG);
082 * 25-Sep-2008 : Added minor tick support, see patch 1934255 by Peter Kolb (DG);
083 * 26-Sep-2008 : Added fireChangeEvent() method (DG);
084 * 19-Mar-2009 : Added entity support - see patch 2603321 by Peter Kolb (DG);
085 * 02-Jul-2013 : Use ParamChecks (DG);
086 * 01-Aug-2013 : Added attributedLabel override to support superscripts,
087 *               subscripts and more (DG);
088 */
089
090package org.jfree.chart.axis;
091
092import java.awt.BasicStroke;
093import java.awt.Color;
094import java.awt.Font;
095import java.awt.FontMetrics;
096import java.awt.Graphics2D;
097import java.awt.Paint;
098import java.awt.Shape;
099import java.awt.Stroke;
100import java.awt.font.TextLayout;
101import java.awt.geom.AffineTransform;
102import java.awt.geom.Line2D;
103import java.awt.geom.Rectangle2D;
104import java.io.IOException;
105import java.io.ObjectInputStream;
106import java.io.ObjectOutputStream;
107import java.io.Serializable;
108import java.text.AttributedString;
109import java.util.Arrays;
110import java.util.EventListener;
111import java.util.List;
112
113import javax.swing.event.EventListenerList;
114
115import org.jfree.chart.entity.AxisEntity;
116import org.jfree.chart.entity.EntityCollection;
117import org.jfree.chart.event.AxisChangeEvent;
118import org.jfree.chart.event.AxisChangeListener;
119import org.jfree.chart.plot.Plot;
120import org.jfree.chart.plot.PlotRenderingInfo;
121import org.jfree.chart.util.AttrStringUtils;
122import org.jfree.chart.util.ParamChecks;
123import org.jfree.io.SerialUtilities;
124import org.jfree.text.TextUtilities;
125import org.jfree.ui.RectangleEdge;
126import org.jfree.ui.RectangleInsets;
127import org.jfree.ui.TextAnchor;
128import org.jfree.util.AttributedStringUtilities;
129import org.jfree.util.ObjectUtilities;
130import org.jfree.util.PaintUtilities;
131
132/**
133 * The base class for all axes in JFreeChart.  Subclasses are divided into
134 * those that display values ({@link ValueAxis}) and those that display
135 * categories ({@link CategoryAxis}).
136 */
137public abstract class Axis implements Cloneable, Serializable {
138
139    /** For serialization. */
140    private static final long serialVersionUID = 7719289504573298271L;
141
142    /** The default axis visibility. */
143    public static final boolean DEFAULT_AXIS_VISIBLE = true;
144
145    /** The default axis label font. */
146    public static final Font DEFAULT_AXIS_LABEL_FONT = new Font(
147            "SansSerif", Font.PLAIN, 12);
148
149    /** The default axis label paint. */
150    public static final Paint DEFAULT_AXIS_LABEL_PAINT = Color.black;
151
152    /** The default axis label insets. */
153    public static final RectangleInsets DEFAULT_AXIS_LABEL_INSETS
154            = new RectangleInsets(3.0, 3.0, 3.0, 3.0);
155
156    /** The default axis line paint. */
157    public static final Paint DEFAULT_AXIS_LINE_PAINT = Color.gray;
158
159    /** The default axis line stroke. */
160    public static final Stroke DEFAULT_AXIS_LINE_STROKE = new BasicStroke(1.0f);
161
162    /** The default tick labels visibility. */
163    public static final boolean DEFAULT_TICK_LABELS_VISIBLE = true;
164
165    /** The default tick label font. */
166    public static final Font DEFAULT_TICK_LABEL_FONT = new Font("SansSerif",
167            Font.PLAIN, 10);
168
169    /** The default tick label paint. */
170    public static final Paint DEFAULT_TICK_LABEL_PAINT = Color.black;
171
172    /** The default tick label insets. */
173    public static final RectangleInsets DEFAULT_TICK_LABEL_INSETS
174            = new RectangleInsets(2.0, 4.0, 2.0, 4.0);
175
176    /** The default tick marks visible. */
177    public static final boolean DEFAULT_TICK_MARKS_VISIBLE = true;
178
179    /** The default tick stroke. */
180    public static final Stroke DEFAULT_TICK_MARK_STROKE = new BasicStroke(1);
181
182    /** The default tick paint. */
183    public static final Paint DEFAULT_TICK_MARK_PAINT = Color.gray;
184
185    /** The default tick mark inside length. */
186    public static final float DEFAULT_TICK_MARK_INSIDE_LENGTH = 0.0f;
187
188    /** The default tick mark outside length. */
189    public static final float DEFAULT_TICK_MARK_OUTSIDE_LENGTH = 2.0f;
190
191    /** A flag indicating whether or not the axis is visible. */
192    private boolean visible;
193
194    /** The label for the axis. */
195    private String label;
196    
197    /** 
198     * An attributed label for the axis (overrides label if non-null).
199     * We have to use this override method to preserve the API compatibility.
200     */
201    private transient AttributedString attributedLabel;
202
203    /** The font for displaying the axis label. */
204    private Font labelFont;
205
206    /** The paint for drawing the axis label. */
207    private transient Paint labelPaint;
208
209    /** The insets for the axis label. */
210    private RectangleInsets labelInsets;
211
212    /** The label angle. */
213    private double labelAngle;
214    
215    /** The axis label location (new in 1.0.16). */
216    private AxisLabelLocation labelLocation;
217
218    /** A flag that controls whether or not the axis line is visible. */
219    private boolean axisLineVisible;
220
221    /** The stroke used for the axis line. */
222    private transient Stroke axisLineStroke;
223
224    /** The paint used for the axis line. */
225    private transient Paint axisLinePaint;
226
227    /**
228     * A flag that indicates whether or not tick labels are visible for the
229     * axis.
230     */
231    private boolean tickLabelsVisible;
232
233    /** The font used to display the tick labels. */
234    private Font tickLabelFont;
235
236    /** The color used to display the tick labels. */
237    private transient Paint tickLabelPaint;
238
239    /** The blank space around each tick label. */
240    private RectangleInsets tickLabelInsets;
241
242    /**
243     * A flag that indicates whether or not major tick marks are visible for
244     * the axis.
245     */
246    private boolean tickMarksVisible;
247
248    /**
249     * The length of the major tick mark inside the data area (zero
250     * permitted).
251     */
252    private float tickMarkInsideLength;
253
254    /**
255     * The length of the major tick mark outside the data area (zero
256     * permitted).
257     */
258    private float tickMarkOutsideLength;
259
260    /**
261     * A flag that indicates whether or not minor tick marks are visible for the
262     * axis.
263     *
264     * @since 1.0.12
265     */
266    private boolean minorTickMarksVisible;
267
268    /**
269     * The length of the minor tick mark inside the data area (zero permitted).
270     *
271     * @since 1.0.12
272     */
273    private float minorTickMarkInsideLength;
274
275    /**
276     * The length of the minor tick mark outside the data area (zero permitted).
277     *
278     * @since 1.0.12
279     */
280    private float minorTickMarkOutsideLength;
281
282    /** The stroke used to draw tick marks. */
283    private transient Stroke tickMarkStroke;
284
285    /** The paint used to draw tick marks. */
286    private transient Paint tickMarkPaint;
287
288    /** The fixed (horizontal or vertical) dimension for the axis. */
289    private double fixedDimension;
290
291    /**
292     * A reference back to the plot that the axis is assigned to (can be
293     * <code>null</code>).
294     */
295    private transient Plot plot;
296
297    /** Storage for registered listeners. */
298    private transient EventListenerList listenerList;
299
300    /**
301     * Constructs an axis, using default values where necessary.
302     *
303     * @param label  the axis label (<code>null</code> permitted).
304     */
305    protected Axis(String label) {
306
307        this.label = label;
308        this.visible = DEFAULT_AXIS_VISIBLE;
309        this.labelFont = DEFAULT_AXIS_LABEL_FONT;
310        this.labelPaint = DEFAULT_AXIS_LABEL_PAINT;
311        this.labelInsets = DEFAULT_AXIS_LABEL_INSETS;
312        this.labelAngle = 0.0;
313        this.labelLocation = AxisLabelLocation.MIDDLE;
314
315        this.axisLineVisible = true;
316        this.axisLinePaint = DEFAULT_AXIS_LINE_PAINT;
317        this.axisLineStroke = DEFAULT_AXIS_LINE_STROKE;
318
319        this.tickLabelsVisible = DEFAULT_TICK_LABELS_VISIBLE;
320        this.tickLabelFont = DEFAULT_TICK_LABEL_FONT;
321        this.tickLabelPaint = DEFAULT_TICK_LABEL_PAINT;
322        this.tickLabelInsets = DEFAULT_TICK_LABEL_INSETS;
323
324        this.tickMarksVisible = DEFAULT_TICK_MARKS_VISIBLE;
325        this.tickMarkStroke = DEFAULT_TICK_MARK_STROKE;
326        this.tickMarkPaint = DEFAULT_TICK_MARK_PAINT;
327        this.tickMarkInsideLength = DEFAULT_TICK_MARK_INSIDE_LENGTH;
328        this.tickMarkOutsideLength = DEFAULT_TICK_MARK_OUTSIDE_LENGTH;
329
330        this.minorTickMarksVisible = false;
331        this.minorTickMarkInsideLength = 0.0f;
332        this.minorTickMarkOutsideLength = 2.0f;
333
334        this.plot = null;
335
336        this.listenerList = new EventListenerList();
337    }
338
339    /**
340     * Returns <code>true</code> if the axis is visible, and
341     * <code>false</code> otherwise.
342     *
343     * @return A boolean.
344     *
345     * @see #setVisible(boolean)
346     */
347    public boolean isVisible() {
348        return this.visible;
349    }
350
351    /**
352     * Sets a flag that controls whether or not the axis is visible and sends
353     * an {@link AxisChangeEvent} to all registered listeners.
354     *
355     * @param flag  the flag.
356     *
357     * @see #isVisible()
358     */
359    public void setVisible(boolean flag) {
360        if (flag != this.visible) {
361            this.visible = flag;
362            fireChangeEvent();
363        }
364    }
365
366    /**
367     * Returns the label for the axis.
368     *
369     * @return The label for the axis (<code>null</code> possible).
370     *
371     * @see #getLabelFont()
372     * @see #getLabelPaint()
373     * @see #setLabel(String)
374     */
375    public String getLabel() {
376        return this.label;
377    }
378
379    /**
380     * Sets the label for the axis and sends an {@link AxisChangeEvent} to all
381     * registered listeners.
382     *
383     * @param label  the new label (<code>null</code> permitted).
384     *
385     * @see #getLabel()
386     * @see #setLabelFont(Font)
387     * @see #setLabelPaint(Paint)
388     */
389    public void setLabel(String label) {
390        this.label = label;
391        fireChangeEvent();
392    }
393
394    /**
395     * Returns the attributed label (the returned value is a copy, so 
396     * modifying it will not impact the state of the axis).  The default value 
397     * is <code>null</code>.
398     * 
399     * @return The attributed label (possibly <code>null</code>).
400     * 
401     * @since 1.0.16
402     */
403    public AttributedString getAttributedLabel() {
404        if (this.attributedLabel != null) {
405            return new AttributedString(this.attributedLabel.getIterator());
406        } else {
407            return null;
408        }
409    }
410    
411    /**
412     * Sets the attributed label for the axis and sends an 
413     * {@link AxisChangeEvent} to all registered listeners.  This is a 
414     * convenience method that converts the string into an 
415     * <code>AttributedString</code> using the current font attributes.
416     * 
417     * @param label  the label (<code>null</<code> permitted).
418     * 
419     * @since 1.0.16
420     */
421    public void setAttributedLabel(String label) {
422        setAttributedLabel(createAttributedLabel(label));    
423    }
424    
425    /**
426     * Sets the attributed label for the axis and sends an 
427     * {@link AxisChangeEvent} to all registered listeners.
428     * 
429     * @param label  the label (<code>null</code> permitted).
430     * 
431     * @since 1.0.16
432     */
433    public void setAttributedLabel(AttributedString label) {
434        if (label != null) {
435            this.attributedLabel = new AttributedString(label.getIterator());
436        } else {
437            this.attributedLabel = null;
438        }
439        fireChangeEvent();
440    }
441    
442    /**
443     * Creates and returns an <code>AttributedString</code> with the specified
444     * text and the labelFont and labelPaint applied as attributes.
445     * 
446     * @param label  the label (<code>null</code> permitted).
447     * 
448     * @return An attributed string or <code>null</code>.
449     * 
450     * @since 1.0.16
451     */
452    public AttributedString createAttributedLabel(String label) {
453        if (label == null) {
454            return null;
455        }
456        AttributedString s = new AttributedString(label);
457        s.addAttributes(this.labelFont.getAttributes(), 0, label.length());
458        return s;
459    }
460    
461    /**
462     * Returns the font for the axis label.
463     *
464     * @return The font (never <code>null</code>).
465     *
466     * @see #setLabelFont(Font)
467     */
468    public Font getLabelFont() {
469        return this.labelFont;
470    }
471
472    /**
473     * Sets the font for the axis label and sends an {@link AxisChangeEvent}
474     * to all registered listeners.
475     *
476     * @param font  the font (<code>null</code> not permitted).
477     *
478     * @see #getLabelFont()
479     */
480    public void setLabelFont(Font font) {
481        ParamChecks.nullNotPermitted(font, "font");
482        if (!this.labelFont.equals(font)) {
483            this.labelFont = font;
484            fireChangeEvent();
485        }
486    }
487
488    /**
489     * Returns the color/shade used to draw the axis label.
490     *
491     * @return The paint (never <code>null</code>).
492     *
493     * @see #setLabelPaint(Paint)
494     */
495    public Paint getLabelPaint() {
496        return this.labelPaint;
497    }
498
499    /**
500     * Sets the paint used to draw the axis label and sends an
501     * {@link AxisChangeEvent} to all registered listeners.
502     *
503     * @param paint  the paint (<code>null</code> not permitted).
504     *
505     * @see #getLabelPaint()
506     */
507    public void setLabelPaint(Paint paint) {
508        ParamChecks.nullNotPermitted(paint, "paint");
509        this.labelPaint = paint;
510        fireChangeEvent();
511    }
512
513    /**
514     * Returns the insets for the label (that is, the amount of blank space
515     * that should be left around the label).
516     *
517     * @return The label insets (never <code>null</code>).
518     *
519     * @see #setLabelInsets(RectangleInsets)
520     */
521    public RectangleInsets getLabelInsets() {
522        return this.labelInsets;
523    }
524
525    /**
526     * Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
527     * to all registered listeners.
528     *
529     * @param insets  the insets (<code>null</code> not permitted).
530     *
531     * @see #getLabelInsets()
532     */
533    public void setLabelInsets(RectangleInsets insets) {
534        setLabelInsets(insets, true);
535    }
536
537    /**
538     * Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
539     * to all registered listeners.
540     *
541     * @param insets  the insets (<code>null</code> not permitted).
542     * @param notify  notify listeners?
543     *
544     * @since 1.0.10
545     */
546    public void setLabelInsets(RectangleInsets insets, boolean notify) {
547        ParamChecks.nullNotPermitted(insets, "insets");
548        if (!insets.equals(this.labelInsets)) {
549            this.labelInsets = insets;
550            if (notify) {
551                fireChangeEvent();
552            }
553        }
554    }
555
556    /**
557     * Returns the angle of the axis label.
558     *
559     * @return The angle (in radians).
560     *
561     * @see #setLabelAngle(double)
562     */
563    public double getLabelAngle() {
564        return this.labelAngle;
565    }
566
567    /**
568     * Sets the angle for the label and sends an {@link AxisChangeEvent} to all
569     * registered listeners.
570     *
571     * @param angle  the angle (in radians).
572     *
573     * @see #getLabelAngle()
574     */
575    public void setLabelAngle(double angle) {
576        this.labelAngle = angle;
577        fireChangeEvent();
578    }
579    
580    /**
581     * Returns the location of the axis label.  The default is
582     * {@link AxisLabelLocation#MIDDLE}.
583     * 
584     * @return The location of the axis label (never <code>null</code>). 
585     * 
586     * @since 1.0.16
587     */
588    public AxisLabelLocation getLabelLocation() {
589        return this.labelLocation;
590    }
591    
592    /**
593     * Sets the axis label location and sends an {@link AxisChangeEvent} to
594     * all registered listeners.
595     * 
596     * @param location  the new location (<code>null</code> not permitted).
597     * 
598     * @since 1.0.16
599     */
600    public void setLabelLocation(AxisLabelLocation location) {
601        ParamChecks.nullNotPermitted(location, "location");
602        this.labelLocation = location;
603        fireChangeEvent();
604    }
605
606    /**
607     * A flag that controls whether or not the axis line is drawn.
608     *
609     * @return A boolean.
610     *
611     * @see #getAxisLinePaint()
612     * @see #getAxisLineStroke()
613     * @see #setAxisLineVisible(boolean)
614     */
615    public boolean isAxisLineVisible() {
616        return this.axisLineVisible;
617    }
618
619    /**
620     * Sets a flag that controls whether or not the axis line is visible and
621     * sends an {@link AxisChangeEvent} to all registered listeners.
622     *
623     * @param visible  the flag.
624     *
625     * @see #isAxisLineVisible()
626     * @see #setAxisLinePaint(Paint)
627     * @see #setAxisLineStroke(Stroke)
628     */
629    public void setAxisLineVisible(boolean visible) {
630        this.axisLineVisible = visible;
631        fireChangeEvent();
632    }
633
634    /**
635     * Returns the paint used to draw the axis line.
636     *
637     * @return The paint (never <code>null</code>).
638     *
639     * @see #setAxisLinePaint(Paint)
640     */
641    public Paint getAxisLinePaint() {
642        return this.axisLinePaint;
643    }
644
645    /**
646     * Sets the paint used to draw the axis line and sends an
647     * {@link AxisChangeEvent} to all registered listeners.
648     *
649     * @param paint  the paint (<code>null</code> not permitted).
650     *
651     * @see #getAxisLinePaint()
652     */
653    public void setAxisLinePaint(Paint paint) {
654        ParamChecks.nullNotPermitted(paint, "paint");
655        this.axisLinePaint = paint;
656        fireChangeEvent();
657    }
658
659    /**
660     * Returns the stroke used to draw the axis line.
661     *
662     * @return The stroke (never <code>null</code>).
663     *
664     * @see #setAxisLineStroke(Stroke)
665     */
666    public Stroke getAxisLineStroke() {
667        return this.axisLineStroke;
668    }
669
670    /**
671     * Sets the stroke used to draw the axis line and sends an
672     * {@link AxisChangeEvent} to all registered listeners.
673     *
674     * @param stroke  the stroke (<code>null</code> not permitted).
675     *
676     * @see #getAxisLineStroke()
677     */
678    public void setAxisLineStroke(Stroke stroke) {
679        ParamChecks.nullNotPermitted(stroke, "stroke");
680        this.axisLineStroke = stroke;
681        fireChangeEvent();
682    }
683
684    /**
685     * Returns a flag indicating whether or not the tick labels are visible.
686     *
687     * @return The flag.
688     *
689     * @see #getTickLabelFont()
690     * @see #getTickLabelPaint()
691     * @see #setTickLabelsVisible(boolean)
692     */
693    public boolean isTickLabelsVisible() {
694        return this.tickLabelsVisible;
695    }
696
697    /**
698     * Sets the flag that determines whether or not the tick labels are
699     * visible and sends an {@link AxisChangeEvent} to all registered
700     * listeners.
701     *
702     * @param flag  the flag.
703     *
704     * @see #isTickLabelsVisible()
705     * @see #setTickLabelFont(Font)
706     * @see #setTickLabelPaint(Paint)
707     */
708    public void setTickLabelsVisible(boolean flag) {
709
710        if (flag != this.tickLabelsVisible) {
711            this.tickLabelsVisible = flag;
712            fireChangeEvent();
713        }
714
715    }
716
717    /**
718     * Returns the flag that indicates whether or not the minor tick marks are
719     * showing.
720     *
721     * @return The flag that indicates whether or not the minor tick marks are
722     *         showing.
723     *
724     * @see #setMinorTickMarksVisible(boolean)
725     *
726     * @since 1.0.12
727     */
728    public boolean isMinorTickMarksVisible() {
729        return this.minorTickMarksVisible;
730    }
731
732    /**
733     * Sets the flag that indicates whether or not the minor tick marks are 
734     * showing and sends an {@link AxisChangeEvent} to all registered
735     * listeners.
736     *
737     * @param flag  the flag.
738     *
739     * @see #isMinorTickMarksVisible()
740     *
741     * @since 1.0.12
742     */
743    public void setMinorTickMarksVisible(boolean flag) {
744        if (flag != this.minorTickMarksVisible) {
745            this.minorTickMarksVisible = flag;
746            fireChangeEvent();
747        }
748    }
749
750    /**
751     * Returns the font used for the tick labels (if showing).
752     *
753     * @return The font (never <code>null</code>).
754     *
755     * @see #setTickLabelFont(Font)
756     */
757    public Font getTickLabelFont() {
758        return this.tickLabelFont;
759    }
760
761    /**
762     * Sets the font for the tick labels and sends an {@link AxisChangeEvent}
763     * to all registered listeners.
764     *
765     * @param font  the font (<code>null</code> not allowed).
766     *
767     * @see #getTickLabelFont()
768     */
769    public void setTickLabelFont(Font font) {
770        ParamChecks.nullNotPermitted(font, "font");
771        if (!this.tickLabelFont.equals(font)) {
772            this.tickLabelFont = font;
773            fireChangeEvent();
774        }
775    }
776
777    /**
778     * Returns the color/shade used for the tick labels.
779     *
780     * @return The paint used for the tick labels.
781     *
782     * @see #setTickLabelPaint(Paint)
783     */
784    public Paint getTickLabelPaint() {
785        return this.tickLabelPaint;
786    }
787
788    /**
789     * Sets the paint used to draw tick labels (if they are showing) and
790     * sends an {@link AxisChangeEvent} to all registered listeners.
791     *
792     * @param paint  the paint (<code>null</code> not permitted).
793     *
794     * @see #getTickLabelPaint()
795     */
796    public void setTickLabelPaint(Paint paint) {
797        ParamChecks.nullNotPermitted(paint, "paint");
798        this.tickLabelPaint = paint;
799        fireChangeEvent();
800    }
801
802    /**
803     * Returns the insets for the tick labels.
804     *
805     * @return The insets (never <code>null</code>).
806     *
807     * @see #setTickLabelInsets(RectangleInsets)
808     */
809    public RectangleInsets getTickLabelInsets() {
810        return this.tickLabelInsets;
811    }
812
813    /**
814     * Sets the insets for the tick labels and sends an {@link AxisChangeEvent}
815     * to all registered listeners.
816     *
817     * @param insets  the insets (<code>null</code> not permitted).
818     *
819     * @see #getTickLabelInsets()
820     */
821    public void setTickLabelInsets(RectangleInsets insets) {
822        ParamChecks.nullNotPermitted(insets, "insets");
823        if (!this.tickLabelInsets.equals(insets)) {
824            this.tickLabelInsets = insets;
825            fireChangeEvent();
826        }
827    }
828
829    /**
830     * Returns the flag that indicates whether or not the tick marks are
831     * showing.
832     *
833     * @return The flag that indicates whether or not the tick marks are
834     *         showing.
835     *
836     * @see #setTickMarksVisible(boolean)
837     */
838    public boolean isTickMarksVisible() {
839        return this.tickMarksVisible;
840    }
841
842    /**
843     * Sets the flag that indicates whether or not the tick marks are showing
844     * and sends an {@link AxisChangeEvent} to all registered listeners.
845     *
846     * @param flag  the flag.
847     *
848     * @see #isTickMarksVisible()
849     */
850    public void setTickMarksVisible(boolean flag) {
851        if (flag != this.tickMarksVisible) {
852            this.tickMarksVisible = flag;
853            fireChangeEvent();
854        }
855    }
856
857    /**
858     * Returns the inside length of the tick marks.
859     *
860     * @return The length.
861     *
862     * @see #getTickMarkOutsideLength()
863     * @see #setTickMarkInsideLength(float)
864     */
865    public float getTickMarkInsideLength() {
866        return this.tickMarkInsideLength;
867    }
868
869    /**
870     * Sets the inside length of the tick marks and sends
871     * an {@link AxisChangeEvent} to all registered listeners.
872     *
873     * @param length  the new length.
874     *
875     * @see #getTickMarkInsideLength()
876     */
877    public void setTickMarkInsideLength(float length) {
878        this.tickMarkInsideLength = length;
879        fireChangeEvent();
880    }
881
882    /**
883     * Returns the outside length of the tick marks.
884     *
885     * @return The length.
886     *
887     * @see #getTickMarkInsideLength()
888     * @see #setTickMarkOutsideLength(float)
889     */
890    public float getTickMarkOutsideLength() {
891        return this.tickMarkOutsideLength;
892    }
893
894    /**
895     * Sets the outside length of the tick marks and sends
896     * an {@link AxisChangeEvent} to all registered listeners.
897     *
898     * @param length  the new length.
899     *
900     * @see #getTickMarkInsideLength()
901     */
902    public void setTickMarkOutsideLength(float length) {
903        this.tickMarkOutsideLength = length;
904        fireChangeEvent();
905    }
906
907    /**
908     * Returns the stroke used to draw tick marks.
909     *
910     * @return The stroke (never <code>null</code>).
911     *
912     * @see #setTickMarkStroke(Stroke)
913     */
914    public Stroke getTickMarkStroke() {
915        return this.tickMarkStroke;
916    }
917
918    /**
919     * Sets the stroke used to draw tick marks and sends
920     * an {@link AxisChangeEvent} to all registered listeners.
921     *
922     * @param stroke  the stroke (<code>null</code> not permitted).
923     *
924     * @see #getTickMarkStroke()
925     */
926    public void setTickMarkStroke(Stroke stroke) {
927        ParamChecks.nullNotPermitted(stroke, "stroke");
928        if (!this.tickMarkStroke.equals(stroke)) {
929            this.tickMarkStroke = stroke;
930            fireChangeEvent();
931        }
932    }
933
934    /**
935     * Returns the paint used to draw tick marks (if they are showing).
936     *
937     * @return The paint (never <code>null</code>).
938     *
939     * @see #setTickMarkPaint(Paint)
940     */
941    public Paint getTickMarkPaint() {
942        return this.tickMarkPaint;
943    }
944
945    /**
946     * Sets the paint used to draw tick marks and sends an
947     * {@link AxisChangeEvent} to all registered listeners.
948     *
949     * @param paint  the paint (<code>null</code> not permitted).
950     *
951     * @see #getTickMarkPaint()
952     */
953    public void setTickMarkPaint(Paint paint) {
954        ParamChecks.nullNotPermitted(paint, "paint");
955        this.tickMarkPaint = paint;
956        fireChangeEvent();
957    }
958
959    /**
960     * Returns the inside length of the minor tick marks.
961     *
962     * @return The length.
963     *
964     * @see #getMinorTickMarkOutsideLength()
965     * @see #setMinorTickMarkInsideLength(float)
966     *
967     * @since 1.0.12
968     */
969    public float getMinorTickMarkInsideLength() {
970        return this.minorTickMarkInsideLength;
971    }
972
973    /**
974     * Sets the inside length of the minor tick marks and sends
975     * an {@link AxisChangeEvent} to all registered listeners.
976     *
977     * @param length  the new length.
978     *
979     * @see #getMinorTickMarkInsideLength()
980     *
981     * @since 1.0.12
982     */
983    public void setMinorTickMarkInsideLength(float length) {
984        this.minorTickMarkInsideLength = length;
985        fireChangeEvent();
986    }
987
988    /**
989     * Returns the outside length of the minor tick marks.
990     *
991     * @return The length.
992     *
993     * @see #getMinorTickMarkInsideLength()
994     * @see #setMinorTickMarkOutsideLength(float)
995     *
996     * @since 1.0.12
997     */
998    public float getMinorTickMarkOutsideLength() {
999        return this.minorTickMarkOutsideLength;
1000    }
1001
1002    /**
1003     * Sets the outside length of the minor tick marks and sends
1004     * an {@link AxisChangeEvent} to all registered listeners.
1005     *
1006     * @param length  the new length.
1007     *
1008     * @see #getMinorTickMarkInsideLength()
1009     *
1010     * @since 1.0.12
1011     */
1012    public void setMinorTickMarkOutsideLength(float length) {
1013        this.minorTickMarkOutsideLength = length;
1014        fireChangeEvent();
1015    }
1016
1017    /**
1018     * Returns the plot that the axis is assigned to.  This method will return
1019     * <code>null</code> if the axis is not currently assigned to a plot.
1020     *
1021     * @return The plot that the axis is assigned to (possibly
1022     *         <code>null</code>).
1023     *
1024     * @see #setPlot(Plot)
1025     */
1026    public Plot getPlot() {
1027        return this.plot;
1028    }
1029
1030    /**
1031     * Sets a reference to the plot that the axis is assigned to.
1032     * <P>
1033     * This method is used internally, you shouldn't need to call it yourself.
1034     *
1035     * @param plot  the plot.
1036     *
1037     * @see #getPlot()
1038     */
1039    public void setPlot(Plot plot) {
1040        this.plot = plot;
1041        configure();
1042    }
1043
1044    /**
1045     * Returns the fixed dimension for the axis.
1046     *
1047     * @return The fixed dimension.
1048     *
1049     * @see #setFixedDimension(double)
1050     */
1051    public double getFixedDimension() {
1052        return this.fixedDimension;
1053    }
1054
1055    /**
1056     * Sets the fixed dimension for the axis.
1057     * <P>
1058     * This is used when combining more than one plot on a chart.  In this case,
1059     * there may be several axes that need to have the same height or width so
1060     * that they are aligned.  This method is used to fix a dimension for the
1061     * axis (the context determines whether the dimension is horizontal or
1062     * vertical).
1063     *
1064     * @param dimension  the fixed dimension.
1065     *
1066     * @see #getFixedDimension()
1067     */
1068    public void setFixedDimension(double dimension) {
1069        this.fixedDimension = dimension;
1070    }
1071
1072    /**
1073     * Configures the axis to work with the current plot.  Override this method
1074     * to perform any special processing (such as auto-rescaling).
1075     */
1076    public abstract void configure();
1077
1078    /**
1079     * Estimates the space (height or width) required to draw the axis.
1080     *
1081     * @param g2  the graphics device.
1082     * @param plot  the plot that the axis belongs to.
1083     * @param plotArea  the area within which the plot (including axes) should
1084     *                  be drawn.
1085     * @param edge  the axis location.
1086     * @param space  space already reserved.
1087     *
1088     * @return The space required to draw the axis (including pre-reserved
1089     *         space).
1090     */
1091    public abstract AxisSpace reserveSpace(Graphics2D g2, Plot plot,
1092                                           Rectangle2D plotArea,
1093                                           RectangleEdge edge,
1094                                           AxisSpace space);
1095
1096    /**
1097     * Draws the axis on a Java 2D graphics device (such as the screen or a
1098     * printer).
1099     *
1100     * @param g2  the graphics device (<code>null</code> not permitted).
1101     * @param cursor  the cursor location (determines where to draw the axis).
1102     * @param plotArea  the area within which the axes and plot should be drawn.
1103     * @param dataArea  the area within which the data should be drawn.
1104     * @param edge  the axis location (<code>null</code> not permitted).
1105     * @param plotState  collects information about the plot
1106     *                   (<code>null</code> permitted).
1107     *
1108     * @return The axis state (never <code>null</code>).
1109     */
1110    public abstract AxisState draw(Graphics2D g2,
1111                                   double cursor,
1112                                   Rectangle2D plotArea,
1113                                   Rectangle2D dataArea,
1114                                   RectangleEdge edge,
1115                                   PlotRenderingInfo plotState);
1116
1117    /**
1118     * Calculates the positions of the ticks for the axis, storing the results
1119     * in the tick list (ready for drawing).
1120     *
1121     * @param g2  the graphics device.
1122     * @param state  the axis state.
1123     * @param dataArea  the area inside the axes.
1124     * @param edge  the edge on which the axis is located.
1125     *
1126     * @return The list of ticks.
1127     */
1128    public abstract List refreshTicks(Graphics2D g2, AxisState state,
1129            Rectangle2D dataArea, RectangleEdge edge);
1130
1131    /**
1132     * Created an entity for the axis.
1133     *
1134     * @param cursor  the initial cursor value.
1135     * @param state  the axis state after completion of the drawing with a
1136     *     possibly updated cursor position.
1137     * @param dataArea  the data area.
1138     * @param edge  the edge.
1139     * @param plotState  the PlotRenderingInfo from which a reference to the
1140     *     entity collection can be obtained.
1141     *
1142     * @since 1.0.13
1143     */
1144    protected void createAndAddEntity(double cursor, AxisState state,
1145            Rectangle2D dataArea, RectangleEdge edge,
1146            PlotRenderingInfo plotState) {
1147
1148        if (plotState == null || plotState.getOwner() == null) {
1149            return;  // no need to create entity if we can't save it anyways...
1150        }
1151        Rectangle2D hotspot = null;
1152        if (edge.equals(RectangleEdge.TOP)) {
1153            hotspot = new Rectangle2D.Double(dataArea.getX(),
1154                    state.getCursor(), dataArea.getWidth(),
1155                    cursor - state.getCursor());
1156        }
1157        else if (edge.equals(RectangleEdge.BOTTOM)) {
1158            hotspot = new Rectangle2D.Double(dataArea.getX(), cursor,
1159                    dataArea.getWidth(), state.getCursor() - cursor);
1160        }
1161        else if (edge.equals(RectangleEdge.LEFT)) {
1162            hotspot = new Rectangle2D.Double(state.getCursor(),
1163                    dataArea.getY(), cursor - state.getCursor(),
1164                    dataArea.getHeight());
1165        }
1166        else if (edge.equals(RectangleEdge.RIGHT)) {
1167            hotspot = new Rectangle2D.Double(cursor, dataArea.getY(),
1168                    state.getCursor() - cursor, dataArea.getHeight());
1169        }
1170        EntityCollection e = plotState.getOwner().getEntityCollection();
1171        if (e != null) {
1172            e.add(new AxisEntity(hotspot, this));
1173        }
1174    }
1175
1176    /**
1177     * Registers an object for notification of changes to the axis.
1178     *
1179     * @param listener  the object that is being registered.
1180     *
1181     * @see #removeChangeListener(AxisChangeListener)
1182     */
1183    public void addChangeListener(AxisChangeListener listener) {
1184        this.listenerList.add(AxisChangeListener.class, listener);
1185    }
1186
1187    /**
1188     * Deregisters an object for notification of changes to the axis.
1189     *
1190     * @param listener  the object to deregister.
1191     *
1192     * @see #addChangeListener(AxisChangeListener)
1193     */
1194    public void removeChangeListener(AxisChangeListener listener) {
1195        this.listenerList.remove(AxisChangeListener.class, listener);
1196    }
1197
1198    /**
1199     * Returns <code>true</code> if the specified object is registered with
1200     * the dataset as a listener.  Most applications won't need to call this
1201     * method, it exists mainly for use by unit testing code.
1202     *
1203     * @param listener  the listener.
1204     *
1205     * @return A boolean.
1206     */
1207    public boolean hasListener(EventListener listener) {
1208        List list = Arrays.asList(this.listenerList.getListenerList());
1209        return list.contains(listener);
1210    }
1211
1212    /**
1213     * Notifies all registered listeners that the axis has changed.
1214     * The AxisChangeEvent provides information about the change.
1215     *
1216     * @param event  information about the change to the axis.
1217     */
1218    protected void notifyListeners(AxisChangeEvent event) {
1219        Object[] listeners = this.listenerList.getListenerList();
1220        for (int i = listeners.length - 2; i >= 0; i -= 2) {
1221            if (listeners[i] == AxisChangeListener.class) {
1222                ((AxisChangeListener) listeners[i + 1]).axisChanged(event);
1223            }
1224        }
1225    }
1226
1227    /**
1228     * Sends an {@link AxisChangeEvent} to all registered listeners.
1229     *
1230     * @since 1.0.12
1231     */
1232    protected void fireChangeEvent() {
1233        notifyListeners(new AxisChangeEvent(this));
1234    }
1235
1236    /**
1237     * Returns a rectangle that encloses the axis label.  This is typically
1238     * used for layout purposes (it gives the maximum dimensions of the label).
1239     *
1240     * @param g2  the graphics device.
1241     * @param edge  the edge of the plot area along which the axis is measuring.
1242     *
1243     * @return The enclosing rectangle.
1244     */
1245    protected Rectangle2D getLabelEnclosure(Graphics2D g2, RectangleEdge edge) {
1246        Rectangle2D result = new Rectangle2D.Double();
1247        Rectangle2D bounds = null;
1248        if (this.attributedLabel != null) {
1249            TextLayout layout = new TextLayout(
1250                    this.attributedLabel.getIterator(), 
1251                    g2.getFontRenderContext());
1252            bounds = layout.getBounds();
1253        } else {
1254            String axisLabel = getLabel();
1255            if (axisLabel != null && !axisLabel.equals("")) {
1256                FontMetrics fm = g2.getFontMetrics(getLabelFont());
1257                bounds = TextUtilities.getTextBounds(axisLabel, g2, fm);
1258            }
1259        }
1260        if (bounds != null) {
1261            RectangleInsets insets = getLabelInsets();
1262            bounds = insets.createOutsetRectangle(bounds);
1263            double angle = getLabelAngle();
1264            if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {
1265                angle = angle - Math.PI / 2.0;
1266            }
1267            double x = bounds.getCenterX();
1268            double y = bounds.getCenterY();
1269            AffineTransform transformer
1270                = AffineTransform.getRotateInstance(angle, x, y);
1271            Shape labelBounds = transformer.createTransformedShape(bounds);
1272            result = labelBounds.getBounds2D();
1273        }
1274        return result;
1275    }
1276
1277    protected double labelLocationX(AxisLabelLocation location, 
1278            Rectangle2D dataArea) {
1279        if (location.equals(AxisLabelLocation.HIGH_END)) {
1280            return dataArea.getMaxX();
1281        }
1282        if (location.equals(AxisLabelLocation.MIDDLE)) {
1283            return dataArea.getCenterX();
1284        }
1285        if (location.equals(AxisLabelLocation.LOW_END)) {
1286            return dataArea.getMinX();
1287        }
1288        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1289    }
1290    
1291    protected double labelLocationY(AxisLabelLocation location, 
1292            Rectangle2D dataArea) {
1293        if (location.equals(AxisLabelLocation.HIGH_END)) {
1294            return dataArea.getMinY();
1295        }
1296        if (location.equals(AxisLabelLocation.MIDDLE)) {
1297            return dataArea.getCenterY();
1298        }
1299        if (location.equals(AxisLabelLocation.LOW_END)) {
1300            return dataArea.getMaxY();
1301        }
1302        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1303    }
1304    
1305    protected TextAnchor labelAnchorH(AxisLabelLocation location) {
1306        if (location.equals(AxisLabelLocation.HIGH_END)) {
1307            return TextAnchor.CENTER_RIGHT;
1308        }
1309        if (location.equals(AxisLabelLocation.MIDDLE)) {
1310            return TextAnchor.CENTER;
1311        }
1312        if (location.equals(AxisLabelLocation.LOW_END)) {
1313            return TextAnchor.CENTER_LEFT;
1314        }
1315        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1316    }
1317    
1318    protected TextAnchor labelAnchorV(AxisLabelLocation location) {
1319        if (location.equals(AxisLabelLocation.HIGH_END)) {
1320            return TextAnchor.CENTER_RIGHT;
1321        }
1322        if (location.equals(AxisLabelLocation.MIDDLE)) {
1323            return TextAnchor.CENTER;
1324        }
1325        if (location.equals(AxisLabelLocation.LOW_END)) {
1326            return TextAnchor.CENTER_LEFT;
1327        }
1328        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1329    }
1330
1331    /**
1332     * Draws the axis label.
1333     *
1334     * @param label  the label text.
1335     * @param g2  the graphics device.
1336     * @param plotArea  the plot area.
1337     * @param dataArea  the area inside the axes.
1338     * @param edge  the location of the axis.
1339     * @param state  the axis state (<code>null</code> not permitted).
1340     *
1341     * @return Information about the axis.
1342     */
1343    protected AxisState drawLabel(String label, Graphics2D g2,
1344            Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
1345            AxisState state) {
1346
1347        // it is unlikely that 'state' will be null, but check anyway...
1348        ParamChecks.nullNotPermitted(state, "state");
1349
1350        if ((label == null) || (label.equals(""))) {
1351            return state;
1352        }
1353
1354        Font font = getLabelFont();
1355        RectangleInsets insets = getLabelInsets();
1356        g2.setFont(font);
1357        g2.setPaint(getLabelPaint());
1358        FontMetrics fm = g2.getFontMetrics();
1359        Rectangle2D labelBounds = TextUtilities.getTextBounds(label, g2, fm);
1360
1361        if (edge == RectangleEdge.TOP) {
1362            AffineTransform t = AffineTransform.getRotateInstance(
1363                    getLabelAngle(), labelBounds.getCenterX(),
1364                    labelBounds.getCenterY());
1365            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1366            labelBounds = rotatedLabelBounds.getBounds2D();
1367            double labelx = labelLocationX(this.labelLocation, dataArea);
1368            double labely = state.getCursor() - insets.getBottom()
1369                            - labelBounds.getHeight() / 2.0;
1370            TextAnchor anchor = labelAnchorH(this.labelLocation);
1371            TextUtilities.drawRotatedString(label, g2, (float) labelx,
1372                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1373            state.cursorUp(insets.getTop() + labelBounds.getHeight()
1374                    + insets.getBottom());
1375        }
1376        else if (edge == RectangleEdge.BOTTOM) {
1377            AffineTransform t = AffineTransform.getRotateInstance(
1378                    getLabelAngle(), labelBounds.getCenterX(),
1379                    labelBounds.getCenterY());
1380            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1381            labelBounds = rotatedLabelBounds.getBounds2D();
1382            double labelx = labelLocationX(this.labelLocation, dataArea);
1383            double labely = state.getCursor()
1384                            + insets.getTop() + labelBounds.getHeight() / 2.0;
1385            TextAnchor anchor = labelAnchorH(this.labelLocation);
1386            TextUtilities.drawRotatedString(label, g2, (float) labelx,
1387                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1388            state.cursorDown(insets.getTop() + labelBounds.getHeight()
1389                    + insets.getBottom());
1390        }
1391        else if (edge == RectangleEdge.LEFT) {
1392            AffineTransform t = AffineTransform.getRotateInstance(
1393                    getLabelAngle() - Math.PI / 2.0, labelBounds.getCenterX(),
1394                    labelBounds.getCenterY());
1395            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1396            labelBounds = rotatedLabelBounds.getBounds2D();
1397            double labelx = state.getCursor()
1398                            - insets.getRight() - labelBounds.getWidth() / 2.0;
1399            double labely = labelLocationY(this.labelLocation, dataArea);
1400            TextAnchor anchor = labelAnchorV(this.labelLocation);
1401            TextUtilities.drawRotatedString(label, g2, (float) labelx,
1402                    (float) labely, anchor, getLabelAngle() - Math.PI / 2.0, 
1403                    anchor);
1404            state.cursorLeft(insets.getLeft() + labelBounds.getWidth()
1405                    + insets.getRight());
1406        }
1407        else if (edge == RectangleEdge.RIGHT) {
1408            AffineTransform t = AffineTransform.getRotateInstance(
1409                    getLabelAngle() + Math.PI / 2.0,
1410                    labelBounds.getCenterX(), labelBounds.getCenterY());
1411            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1412            labelBounds = rotatedLabelBounds.getBounds2D();
1413            double labelx = state.getCursor()
1414                            + insets.getLeft() + labelBounds.getWidth() / 2.0;
1415            double labely = labelLocationY(this.labelLocation, dataArea);
1416            TextAnchor anchor = labelAnchorV(this.labelLocation);
1417            TextUtilities.drawRotatedString(label, g2, (float) labelx,
1418                    (float) labely, anchor, getLabelAngle() + Math.PI / 2.0, 
1419                    anchor);
1420            state.cursorRight(insets.getLeft() + labelBounds.getWidth()
1421                    + insets.getRight());
1422        }
1423
1424        return state;
1425
1426    }
1427
1428    /**
1429     * Draws the axis label.
1430     *
1431     * @param label  the label text.
1432     * @param g2  the graphics device.
1433     * @param plotArea  the plot area.
1434     * @param dataArea  the area inside the axes.
1435     * @param edge  the location of the axis.
1436     * @param state  the axis state (<code>null</code> not permitted).
1437     *
1438     * @return Information about the axis.
1439     * 
1440     * @since 1.0.16
1441     */
1442    protected AxisState drawAttributedLabel(AttributedString label, 
1443            Graphics2D g2, Rectangle2D plotArea, Rectangle2D dataArea, 
1444            RectangleEdge edge, AxisState state) {
1445
1446        // it is unlikely that 'state' will be null, but check anyway...
1447        ParamChecks.nullNotPermitted(state, "state");
1448
1449        if (label == null) {
1450            return state;
1451        }
1452
1453        RectangleInsets insets = getLabelInsets();
1454        g2.setFont(getLabelFont());
1455        g2.setPaint(getLabelPaint());
1456        TextLayout layout = new TextLayout(this.attributedLabel.getIterator(),
1457                g2.getFontRenderContext());
1458        Rectangle2D labelBounds = layout.getBounds();
1459
1460        if (edge == RectangleEdge.TOP) {
1461            AffineTransform t = AffineTransform.getRotateInstance(
1462                    getLabelAngle(), labelBounds.getCenterX(),
1463                    labelBounds.getCenterY());
1464            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1465            labelBounds = rotatedLabelBounds.getBounds2D();
1466            double labelx = labelLocationX(this.labelLocation, dataArea);
1467            double labely = state.getCursor() - insets.getBottom()
1468                            - labelBounds.getHeight() / 2.0;
1469            TextAnchor anchor = labelAnchorH(this.labelLocation);
1470            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1471                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1472            state.cursorUp(insets.getTop() + labelBounds.getHeight()
1473                    + insets.getBottom());
1474        }
1475        else if (edge == RectangleEdge.BOTTOM) {
1476            AffineTransform t = AffineTransform.getRotateInstance(
1477                    getLabelAngle(), labelBounds.getCenterX(),
1478                    labelBounds.getCenterY());
1479            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1480            labelBounds = rotatedLabelBounds.getBounds2D();
1481            double labelx = labelLocationX(this.labelLocation, dataArea);
1482            double labely = state.getCursor()
1483                            + insets.getTop() + labelBounds.getHeight() / 2.0;
1484            TextAnchor anchor = labelAnchorH(this.labelLocation);
1485            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1486                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1487            state.cursorDown(insets.getTop() + labelBounds.getHeight()
1488                    + insets.getBottom());
1489        }
1490        else if (edge == RectangleEdge.LEFT) {
1491            AffineTransform t = AffineTransform.getRotateInstance(
1492                    getLabelAngle() - Math.PI / 2.0, labelBounds.getCenterX(),
1493                    labelBounds.getCenterY());
1494            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1495            labelBounds = rotatedLabelBounds.getBounds2D();
1496            double labelx = state.getCursor()
1497                            - insets.getRight() - labelBounds.getWidth() / 2.0;
1498            double labely = labelLocationY(this.labelLocation, dataArea);
1499            TextAnchor anchor = labelAnchorV(this.labelLocation);
1500            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1501                    (float) labely, anchor, getLabelAngle() - Math.PI / 2.0, 
1502                    anchor);
1503            state.cursorLeft(insets.getLeft() + labelBounds.getWidth()
1504                    + insets.getRight());
1505        }
1506        else if (edge == RectangleEdge.RIGHT) {
1507            AffineTransform t = AffineTransform.getRotateInstance(
1508                    getLabelAngle() + Math.PI / 2.0,
1509                    labelBounds.getCenterX(), labelBounds.getCenterY());
1510            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1511            labelBounds = rotatedLabelBounds.getBounds2D();
1512            double labelx = state.getCursor()
1513                            + insets.getLeft() + labelBounds.getWidth() / 2.0;
1514            double labely = labelLocationY(this.labelLocation, dataArea);
1515            TextAnchor anchor = labelAnchorV(this.labelLocation);
1516            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1517                    (float) labely, anchor, getLabelAngle() + Math.PI / 2.0, 
1518                    anchor);
1519            state.cursorRight(insets.getLeft() + labelBounds.getWidth()
1520                    + insets.getRight());
1521        }
1522        return state;
1523    }
1524
1525    /**
1526     * Draws an axis line at the current cursor position and edge.
1527     *
1528     * @param g2  the graphics device.
1529     * @param cursor  the cursor position.
1530     * @param dataArea  the data area.
1531     * @param edge  the edge.
1532     */
1533    protected void drawAxisLine(Graphics2D g2, double cursor,
1534            Rectangle2D dataArea, RectangleEdge edge) {
1535
1536        Line2D axisLine = null;
1537        if (edge == RectangleEdge.TOP) {
1538            axisLine = new Line2D.Double(dataArea.getX(), cursor,
1539                    dataArea.getMaxX(), cursor);
1540        }
1541        else if (edge == RectangleEdge.BOTTOM) {
1542            axisLine = new Line2D.Double(dataArea.getX(), cursor,
1543                    dataArea.getMaxX(), cursor);
1544        }
1545        else if (edge == RectangleEdge.LEFT) {
1546            axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
1547                    dataArea.getMaxY());
1548        }
1549        else if (edge == RectangleEdge.RIGHT) {
1550            axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
1551                    dataArea.getMaxY());
1552        }
1553        g2.setPaint(this.axisLinePaint);
1554        g2.setStroke(this.axisLineStroke);
1555        g2.draw(axisLine);
1556
1557    }
1558
1559    /**
1560     * Returns a clone of the axis.
1561     *
1562     * @return A clone.
1563     *
1564     * @throws CloneNotSupportedException if some component of the axis does
1565     *         not support cloning.
1566     */
1567    @Override
1568    public Object clone() throws CloneNotSupportedException {
1569        Axis clone = (Axis) super.clone();
1570        // It's up to the plot which clones up to restore the correct references
1571        clone.plot = null;
1572        clone.listenerList = new EventListenerList();
1573        return clone;
1574    }
1575
1576    /**
1577     * Tests this axis for equality with another object.
1578     *
1579     * @param obj  the object (<code>null</code> permitted).
1580     *
1581     * @return <code>true</code> or <code>false</code>.
1582     */
1583    @Override
1584    public boolean equals(Object obj) {
1585        if (obj == this) {
1586            return true;
1587        }
1588        if (!(obj instanceof Axis)) {
1589            return false;
1590        }
1591        Axis that = (Axis) obj;
1592        if (this.visible != that.visible) {
1593            return false;
1594        }
1595        if (!ObjectUtilities.equal(this.label, that.label)) {
1596            return false;
1597        }
1598        if (!AttributedStringUtilities.equal(this.attributedLabel, 
1599                that.attributedLabel)) {
1600            return false;
1601        }
1602        if (!ObjectUtilities.equal(this.labelFont, that.labelFont)) {
1603            return false;
1604        }
1605        if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
1606            return false;
1607        }
1608        if (!ObjectUtilities.equal(this.labelInsets, that.labelInsets)) {
1609            return false;
1610        }
1611        if (this.labelAngle != that.labelAngle) {
1612            return false;
1613        }
1614        if (!this.labelLocation.equals(that.labelLocation)) {
1615            return false;
1616        }
1617        if (this.axisLineVisible != that.axisLineVisible) {
1618            return false;
1619        }
1620        if (!ObjectUtilities.equal(this.axisLineStroke, that.axisLineStroke)) {
1621            return false;
1622        }
1623        if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
1624            return false;
1625        }
1626        if (this.tickLabelsVisible != that.tickLabelsVisible) {
1627            return false;
1628        }
1629        if (!ObjectUtilities.equal(this.tickLabelFont, that.tickLabelFont)) {
1630            return false;
1631        }
1632        if (!PaintUtilities.equal(this.tickLabelPaint, that.tickLabelPaint)) {
1633            return false;
1634        }
1635        if (!ObjectUtilities.equal(
1636            this.tickLabelInsets, that.tickLabelInsets
1637        )) {
1638            return false;
1639        }
1640        if (this.tickMarksVisible != that.tickMarksVisible) {
1641            return false;
1642        }
1643        if (this.tickMarkInsideLength != that.tickMarkInsideLength) {
1644            return false;
1645        }
1646        if (this.tickMarkOutsideLength != that.tickMarkOutsideLength) {
1647            return false;
1648        }
1649        if (!PaintUtilities.equal(this.tickMarkPaint, that.tickMarkPaint)) {
1650            return false;
1651        }
1652        if (!ObjectUtilities.equal(this.tickMarkStroke, that.tickMarkStroke)) {
1653            return false;
1654        }
1655        if (this.minorTickMarksVisible != that.minorTickMarksVisible) {
1656            return false;
1657        }
1658        if (this.minorTickMarkInsideLength != that.minorTickMarkInsideLength) {
1659            return false;
1660        }
1661        if (this.minorTickMarkOutsideLength
1662                != that.minorTickMarkOutsideLength) {
1663            return false;
1664        }
1665        if (this.fixedDimension != that.fixedDimension) {
1666            return false;
1667        }
1668        return true;
1669    }
1670
1671    /**
1672     * Returns a hash code for this instance.
1673     * 
1674     * @return A hash code. 
1675     */
1676    @Override
1677    public int hashCode() {
1678        int hash = 3;
1679        if (this.label != null) {
1680            hash = 83 * hash + this.label.hashCode();
1681        }
1682        return hash;
1683    }
1684
1685    /**
1686     * Provides serialization support.
1687     *
1688     * @param stream  the output stream.
1689     *
1690     * @throws IOException  if there is an I/O error.
1691     */
1692    private void writeObject(ObjectOutputStream stream) throws IOException {
1693        stream.defaultWriteObject();
1694        SerialUtilities.writeAttributedString(this.attributedLabel, stream);
1695        SerialUtilities.writePaint(this.labelPaint, stream);
1696        SerialUtilities.writePaint(this.tickLabelPaint, stream);
1697        SerialUtilities.writeStroke(this.axisLineStroke, stream);
1698        SerialUtilities.writePaint(this.axisLinePaint, stream);
1699        SerialUtilities.writeStroke(this.tickMarkStroke, stream);
1700        SerialUtilities.writePaint(this.tickMarkPaint, stream);
1701    }
1702
1703    /**
1704     * Provides serialization support.
1705     *
1706     * @param stream  the input stream.
1707     *
1708     * @throws IOException  if there is an I/O error.
1709     * @throws ClassNotFoundException  if there is a classpath problem.
1710     */
1711    private void readObject(ObjectInputStream stream)
1712        throws IOException, ClassNotFoundException {
1713        stream.defaultReadObject();
1714        this.attributedLabel = SerialUtilities.readAttributedString(stream);
1715        this.labelPaint = SerialUtilities.readPaint(stream);
1716        this.tickLabelPaint = SerialUtilities.readPaint(stream);
1717        this.axisLineStroke = SerialUtilities.readStroke(stream);
1718        this.axisLinePaint = SerialUtilities.readPaint(stream);
1719        this.tickMarkStroke = SerialUtilities.readStroke(stream);
1720        this.tickMarkPaint = SerialUtilities.readPaint(stream);
1721        this.listenerList = new EventListenerList();
1722    }
1723
1724}