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