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