    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * PeriodAxis.java
    029 * ---------------
    030 * (C) Copyright 2004-2009, by Object Refinery Limited and Contributors.
    031 *
    032 * Original Author:  David Gilbert (for Object Refinery Limited);
    033 * Contributor(s):   -;
    034 *
    035 * Changes
    036 * -------
    037 * 01-Jun-2004 : Version 1 (DG);
    038 * 16-Sep-2004 : Fixed bug in equals() method, added clone() method and
    039 *               PublicCloneable interface (DG);
    040 * 25-Nov-2004 : Updates to support major and minor tick marks (DG);
    041 * 25-Feb-2005 : Fixed some tick mark bugs (DG);
    042 * 15-Apr-2005 : Fixed some more tick mark bugs (DG);
    043 * 26-Apr-2005 : Removed LOGGER (DG);
    044 * 16-Jun-2005 : Fixed zooming (DG);
    045 * 15-Sep-2005 : Changed configure() method to check autoRange flag,
    046 *               and added ticks to state (DG);
    047 * ------------- JFREECHART 1.0.x ---------------------------------------------
    048 * 06-Oct-2006 : Updated for deprecations in RegularTimePeriod and
    049 *               subclasses (DG);
    050 * 22-Mar-2007 : Use new defaultAutoRange attribute (DG);
    051 * 31-Jul-2007 : Fix for inverted axis labelling (see bug 1763413) (DG);
    052 * 08-Apr-2008 : Notify listeners in setRange(Range, boolean, boolean) - fixes
    053 *               bug 1932146 (DG);
    054 * 16-Jan-2009 : Fixed bug 2490803, a problem in the setRange() method (DG);
    055 * 02-Mar-2009 : Added locale - see patch 2569670 by Benjamin Bignell (DG);
    056 * 02-Mar-2009 : Fixed draw() method to check tickMarksVisible and
    057 *               tickLabelsVisible (DG);
    058 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG);
    059 *
    060 */
    061
    062package org.jfree.chart.axis;
    063
    064import java.awt.BasicStroke;
    065import java.awt.Color;
    066import java.awt.FontMetrics;
    067import java.awt.Graphics2D;
    068import java.awt.Paint;
    069import java.awt.Stroke;
    070import java.awt.geom.Line2D;
    071import java.awt.geom.Rectangle2D;
    072import java.io.IOException;
    073import java.io.ObjectInputStream;
    074import java.io.ObjectOutputStream;
    075import java.io.Serializable;
    076import java.lang.reflect.Constructor;
    077import java.text.DateFormat;
    078import java.text.SimpleDateFormat;
    079import java.util.ArrayList;
    080import java.util.Arrays;
    081import java.util.Calendar;
    082import java.util.Collections;
    083import java.util.Date;
    084import java.util.List;
    085import java.util.Locale;
    086import java.util.TimeZone;
    087
    088import org.jfree.chart.event.AxisChangeEvent;
    089import org.jfree.chart.plot.Plot;
    090import org.jfree.chart.plot.PlotRenderingInfo;
    091import org.jfree.chart.plot.ValueAxisPlot;
    092import org.jfree.data.Range;
    093import org.jfree.data.time.Day;
    094import org.jfree.data.time.Month;
    095import org.jfree.data.time.RegularTimePeriod;
    096import org.jfree.data.time.Year;
    097import org.jfree.io.SerialUtilities;
    098import org.jfree.text.TextUtilities;
    099import org.jfree.ui.RectangleEdge;
    100import org.jfree.ui.TextAnchor;
    101import org.jfree.util.PublicCloneable;
    102
    103/**
    104 * An axis that displays a date scale based on a
    105 * {@link org.jfree.data.time.RegularTimePeriod}.  This axis works when
    106 * displayed across the bottom or top of a plot, but is broken for display at
    107 * the left or right of charts.
    108 */
    109public class PeriodAxis extends ValueAxis
    110        implements Cloneable, PublicCloneable, Serializable {
    111
    112    /** For serialization. */
    113    private static final long serialVersionUID = 8353295532075872069L;
    114
    115    /** The first time period in the overall range. */
    116    private RegularTimePeriod first;
    117
    118    /** The last time period in the overall range. */
    119    private RegularTimePeriod last;
    120
    121    /**
    122     * The time zone used to convert 'first' and 'last' to absolute
    123     * milliseconds.
    124     */
    125    private TimeZone timeZone;
    126
    127    /**
    128     * The locale (never <code>null</code>).
    129     *
    130     * @since 1.0.13
    131     */
    132    private Locale locale;
    133
    134    /**
    135     * A calendar used for date manipulations in the current time zone and
    136     * locale.
    137     */
    138    private Calendar calendar;
    139
    140    /**
    141     * The {@link RegularTimePeriod} subclass used to automatically determine
    142     * the axis range.
    143     */
    144    private Class autoRangeTimePeriodClass;
    145
    146    /**
    147     * Indicates the {@link RegularTimePeriod} subclass that is used to
    148     * determine the spacing of the major tick marks.
    149     */
    150    private Class majorTickTimePeriodClass;
    151
    152    /**
    153     * A flag that indicates whether or not tick marks are visible for the
    154     * axis.
    155     */
    156    private boolean minorTickMarksVisible;
    157
    158    /**
    159     * Indicates the {@link RegularTimePeriod} subclass that is used to
    160     * determine the spacing of the minor tick marks.
    161     */
    162    private Class minorTickTimePeriodClass;
    163
    164    /** The length of the tick mark inside the data area (zero permitted). */
    165    private float minorTickMarkInsideLength = 0.0f;
    166
    167    /** The length of the tick mark outside the data area (zero permitted). */
    168    private float minorTickMarkOutsideLength = 2.0f;
    169
    170    /** The stroke used to draw tick marks. */
    171    private transient Stroke minorTickMarkStroke = new BasicStroke(0.5f);
    172
    173    /** The paint used to draw tick marks. */
    174    private transient Paint minorTickMarkPaint = Color.black;
    175
    176    /** Info for each labelling band. */
    177    private PeriodAxisLabelInfo[] labelInfo;
    178
    179    /**
    180     * Creates a new axis.
    181     *
    182     * @param label  the axis label.
    183     */
    184    public PeriodAxis(String label) {
    185        this(label, new Day(), new Day());
    186    }
    187
    188    /**
    189     * Creates a new axis.
    190     *
    191     * @param label  the axis label (<code>null</code> permitted).
    192     * @param first  the first time period in the axis range
    193     *               (<code>null</code> not permitted).
    194     * @param last  the last time period in the axis range
    195     *              (<code>null</code> not permitted).
    196     */
    197    public PeriodAxis(String label,
    198                      RegularTimePeriod first, RegularTimePeriod last) {
    199        this(label, first, last, TimeZone.getDefault(), Locale.getDefault());
    200    }
    201
    202    /**
    203     * Creates a new axis.
    204     *
    205     * @param label  the axis label (<code>null</code> permitted).
    206     * @param first  the first time period in the axis range
    207     *               (<code>null</code> not permitted).
    208     * @param last  the last time period in the axis range
    209     *              (<code>null</code> not permitted).
    210     * @param timeZone  the time zone (<code>null</code> not permitted).
    211     *
    212     * @deprecated As of version 1.0.13, you should use the constructor that
    213     *     specifies a Locale also.
    214     */
    215    public PeriodAxis(String label,
    216                      RegularTimePeriod first, RegularTimePeriod last,
    217                      TimeZone timeZone) {
    218        this(label, first, last, timeZone, Locale.getDefault());
    219    }
    220
    221    /**
    222     * Creates a new axis.
    223     *
    224     * @param label  the axis label (<code>null</code> permitted).
    225     * @param first  the first time period in the axis range
    226     *               (<code>null</code> not permitted).
    227     * @param last  the last time period in the axis range
    228     *              (<code>null</code> not permitted).
    229     * @param timeZone  the time zone (<code>null</code> not permitted).
    230     * @param locale  the locale (<code>null</code> not permitted).
    231     *
    232     * @since 1.0.13
    233     */
    234    public PeriodAxis(String label, RegularTimePeriod first,
    235            RegularTimePeriod last, TimeZone timeZone, Locale locale) {
    236        super(label, null);
    237        if (timeZone == null) {
    238            throw new IllegalArgumentException("Null 'timeZone' argument.");
    239        }
    240        if (locale == null) {
    241            throw new IllegalArgumentException("Null 'locale' argument.");
    242        }
    243        this.first = first;
    244        this.last = last;
    245        this.timeZone = timeZone;
    246        this.locale = locale;
    247        this.calendar = Calendar.getInstance(timeZone, locale);
    248        this.first.peg(this.calendar);
    249        this.last.peg(this.calendar);
    250        this.autoRangeTimePeriodClass = first.getClass();
    251        this.majorTickTimePeriodClass = first.getClass();
    252        this.minorTickMarksVisible = false;
    253        this.minorTickTimePeriodClass = RegularTimePeriod.downsize(
    254                this.majorTickTimePeriodClass);
    255        setAutoRange(true);
    256        this.labelInfo = new PeriodAxisLabelInfo[2];
    257        this.labelInfo[0] = new PeriodAxisLabelInfo(Month.class,
    258                new SimpleDateFormat("MMM", locale));
    259        this.labelInfo[1] = new PeriodAxisLabelInfo(Year.class,
    260                new SimpleDateFormat("yyyy", locale));
    261    }
    262
    263    /**
    264     * Returns the first time period in the axis range.
    265     *
    266     * @return The first time period (never <code>null</code>).
    267     */
    268    public RegularTimePeriod getFirst() {
    269        return this.first;
    270    }
    271
    272    /**
    273     * Sets the first time period in the axis range and sends an
    274     * {@link AxisChangeEvent} to all registered listeners.
    275     *
    276     * @param first  the time period (<code>null</code> not permitted).
    277     */
    278    public void setFirst(RegularTimePeriod first) {
    279        if (first == null) {
    280            throw new IllegalArgumentException("Null 'first' argument.");
    281        }
    282        this.first = first;
    283        this.first.peg(this.calendar);
    284        notifyListeners(new AxisChangeEvent(this));
    285    }
    286
    287    /**
    288     * Returns the last time period in the axis range.
    289     *
    290     * @return The last time period (never <code>null</code>).
    291     */
    292    public RegularTimePeriod getLast() {
    293        return this.last;
    294    }
    295
    296    /**
    297     * Sets the last time period in the axis range and sends an
    298     * {@link AxisChangeEvent} to all registered listeners.
    299     *
    300     * @param last  the time period (<code>null</code> not permitted).
    301     */
    302    public void setLast(RegularTimePeriod last) {
    303        if (last == null) {
    304            throw new IllegalArgumentException("Null 'last' argument.");
    305        }
    306        this.last = last;
    307        this.last.peg(this.calendar);
    308        notifyListeners(new AxisChangeEvent(this));
    309    }
    310
    311    /**
    312     * Returns the time zone used to convert the periods defining the axis
    313     * range into absolute milliseconds.
    314     *
    315     * @return The time zone (never <code>null</code>).
    316     */
    317    public TimeZone getTimeZone() {
    318        return this.timeZone;
    319    }
    320
    321    /**
    322     * Sets the time zone that is used to convert the time periods into
    323     * absolute milliseconds.
    324     *
    325     * @param zone  the time zone (<code>null</code> not permitted).
    326     */
    327    public void setTimeZone(TimeZone zone) {
    328        if (zone == null) {
    329            throw new IllegalArgumentException("Null 'zone' argument.");
    330        }
    331        this.timeZone = zone;
    332        this.calendar = Calendar.getInstance(zone, this.locale);
    333        this.first.peg(this.calendar);
    334        this.last.peg(this.calendar);
    335        notifyListeners(new AxisChangeEvent(this));
    336    }
    337
    338    /**
    339     * Returns the locale for this axis.
    340     *
    341     * @return The locale (never (<code>null</code>).
    342     *
    343     * @since 1.0.13
    344     */
    345    public Locale getLocale() {
    346        return this.locale;
    347    }
    348
    349    /**
    350     * Returns the class used to create the first and last time periods for
    351     * the axis range when the auto-range flag is set to <code>true</code>.
    352     *
    353     * @return The class (never <code>null</code>).
    354     */
    355    public Class getAutoRangeTimePeriodClass() {
    356        return this.autoRangeTimePeriodClass;
    357    }
    358
    359    /**
    360     * Sets the class used to create the first and last time periods for the
    361     * axis range when the auto-range flag is set to <code>true</code> and
    362     * sends an {@link AxisChangeEvent} to all registered listeners.
    363     *
    364     * @param c  the class (<code>null</code> not permitted).
    365     */
    366    public void setAutoRangeTimePeriodClass(Class c) {
    367        if (c == null) {
    368            throw new IllegalArgumentException("Null 'c' argument.");
    369        }
    370        this.autoRangeTimePeriodClass = c;
    371        notifyListeners(new AxisChangeEvent(this));
    372    }
    373
    374    /**
    375     * Returns the class that controls the spacing of the major tick marks.
    376     *
    377     * @return The class (never <code>null</code>).
    378     */
    379    public Class getMajorTickTimePeriodClass() {
    380        return this.majorTickTimePeriodClass;
    381    }
    382
    383    /**
    384     * Sets the class that controls the spacing of the major tick marks, and
    385     * sends an {@link AxisChangeEvent} to all registered listeners.
    386     *
    387     * @param c  the class (a subclass of {@link RegularTimePeriod} is
    388     *           expected).
    389     */
    390    public void setMajorTickTimePeriodClass(Class c) {
    391        if (c == null) {
    392            throw new IllegalArgumentException("Null 'c' argument.");
    393        }
    394        this.majorTickTimePeriodClass = c;
    395        notifyListeners(new AxisChangeEvent(this));
    396    }
    397
    398    /**
    399     * Returns the flag that controls whether or not minor tick marks
    400     * are displayed for the axis.
    401     *
    402     * @return A boolean.
    403     */
    404    public boolean isMinorTickMarksVisible() {
    405        return this.minorTickMarksVisible;
    406    }
    407
    408    /**
    409     * Sets the flag that controls whether or not minor tick marks
    410     * are displayed for the axis, and sends a {@link AxisChangeEvent}
    411     * to all registered listeners.
    412     *
    413     * @param visible  the flag.
    414     */
    415    public void setMinorTickMarksVisible(boolean visible) {
    416        this.minorTickMarksVisible = visible;
    417        notifyListeners(new AxisChangeEvent(this));
    418    }
    419
    420    /**
    421     * Returns the class that controls the spacing of the minor tick marks.
    422     *
    423     * @return The class (never <code>null</code>).
    424     */
    425    public Class getMinorTickTimePeriodClass() {
    426        return this.minorTickTimePeriodClass;
    427    }
    428
    429    /**
    430     * Sets the class that controls the spacing of the minor tick marks, and
    431     * sends an {@link AxisChangeEvent} to all registered listeners.
    432     *
    433     * @param c  the class (a subclass of {@link RegularTimePeriod} is
    434     *           expected).
    435     */
    436    public void setMinorTickTimePeriodClass(Class c) {
    437        if (c == null) {
    438            throw new IllegalArgumentException("Null 'c' argument.");
    439        }
    440        this.minorTickTimePeriodClass = c;
    441        notifyListeners(new AxisChangeEvent(this));
    442    }
    443
    444    /**
    445     * Returns the stroke used to display minor tick marks, if they are
    446     * visible.
    447     *
    448     * @return A stroke (never <code>null</code>).
    449     */
    450    public Stroke getMinorTickMarkStroke() {
    451        return this.minorTickMarkStroke;
    452    }
    453
    454    /**
    455     * Sets the stroke used to display minor tick marks, if they are
    456     * visible, and sends a {@link AxisChangeEvent} to all registered
    457     * listeners.
    458     *
    459     * @param stroke  the stroke (<code>null</code> not permitted).
    460     */
    461    public void setMinorTickMarkStroke(Stroke stroke) {
    462        if (stroke == null) {
    463            throw new IllegalArgumentException("Null 'stroke' argument.");
    464        }
    465        this.minorTickMarkStroke = stroke;
    466        notifyListeners(new AxisChangeEvent(this));
    467    }
    468
    469    /**
    470     * Returns the paint used to display minor tick marks, if they are
    471     * visible.
    472     *
    473     * @return A paint (never <code>null</code>).
    474     */
    475    public Paint getMinorTickMarkPaint() {
    476        return this.minorTickMarkPaint;
    477    }
    478
    479    /**
    480     * Sets the paint used to display minor tick marks, if they are
    481     * visible, and sends a {@link AxisChangeEvent} to all registered
    482     * listeners.
    483     *
    484     * @param paint  the paint (<code>null</code> not permitted).
    485     */
    486    public void setMinorTickMarkPaint(Paint paint) {
    487        if (paint == null) {
    488            throw new IllegalArgumentException("Null 'paint' argument.");
    489        }
    490        this.minorTickMarkPaint = paint;
    491        notifyListeners(new AxisChangeEvent(this));
    492    }
    493
    494    /**
    495     * Returns the inside length for the minor tick marks.
    496     *
    497     * @return The length.
    498     */
    499    public float getMinorTickMarkInsideLength() {
    500        return this.minorTickMarkInsideLength;
    501    }
    502
    503    /**
    504     * Sets the inside length of the minor tick marks and sends an
    505     * {@link AxisChangeEvent} to all registered listeners.
    506     *
    507     * @param length  the length.
    508     */
    509    public void setMinorTickMarkInsideLength(float length) {
    510        this.minorTickMarkInsideLength = length;
    511        notifyListeners(new AxisChangeEvent(this));
    512    }
    513
    514    /**
    515     * Returns the outside length for the minor tick marks.
    516     *
    517     * @return The length.
    518     */
    519    public float getMinorTickMarkOutsideLength() {
    520        return this.minorTickMarkOutsideLength;
    521    }
    522
    523    /**
    524     * Sets the outside length of the minor tick marks and sends an
    525     * {@link AxisChangeEvent} to all registered listeners.
    526     *
    527     * @param length  the length.
    528     */
    529    public void setMinorTickMarkOutsideLength(float length) {
    530        this.minorTickMarkOutsideLength = length;
    531        notifyListeners(new AxisChangeEvent(this));
    532    }
    533
    534    /**
    535     * Returns an array of label info records.
    536     *
    537     * @return An array.
    538     */
    539    public PeriodAxisLabelInfo[] getLabelInfo() {
    540        return this.labelInfo;
    541    }
    542
    543    /**
    544     * Sets the array of label info records and sends an
    545     * {@link AxisChangeEvent} to all registered listeners.
    546     *
    547     * @param info  the info.
    548     */
    549    public void setLabelInfo(PeriodAxisLabelInfo[] info) {
    550        this.labelInfo = info;
    551        notifyListeners(new AxisChangeEvent(this));
    552    }
    553
    554    /**
    555     * Sets the range for the axis, if requested, sends an
    556     * {@link AxisChangeEvent} to all registered listeners.  As a side-effect,
    557     * the auto-range flag is set to <code>false</code> (optional).
    558     *
    559     * @param range  the range (<code>null</code> not permitted).
    560     * @param turnOffAutoRange  a flag that controls whether or not the auto
    561     *                          range is turned off.
    562     * @param notify  a flag that controls whether or not listeners are
    563     *                notified.
    564     */
    565    public void setRange(Range range, boolean turnOffAutoRange,
    566                         boolean notify) {
    567        long upper = Math.round(range.getUpperBound());
    568        long lower = Math.round(range.getLowerBound());
    569        this.first = createInstance(this.autoRangeTimePeriodClass,
    570                new Date(lower), this.timeZone, this.locale);
    571        this.last = createInstance(this.autoRangeTimePeriodClass,
    572                new Date(upper), this.timeZone, this.locale);
    573        super.setRange(new Range(this.first.getFirstMillisecond(),
    574                this.last.getLastMillisecond() + 1.0), turnOffAutoRange,
    575                notify);
    576    }
    577
    578    /**
    579     * Configures the axis to work with the current plot.  Override this method
    580     * to perform any special processing (such as auto-rescaling).
    581     */
    582    public void configure() {
    583        if (this.isAutoRange()) {
    584            autoAdjustRange();
    585        }
    586    }
    587
    588    /**
    589     * Estimates the space (height or width) required to draw the axis.
    590     *
    591     * @param g2  the graphics device.
    592     * @param plot  the plot that the axis belongs to.
    593     * @param plotArea  the area within which the plot (including axes) should
    594     *                  be drawn.
    595     * @param edge  the axis location.
    596     * @param space  space already reserved.
    597     *
    598     * @return The space required to draw the axis (including pre-reserved
    599     *         space).
    600     */
    601    public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
    602                                  Rectangle2D plotArea, RectangleEdge edge,
    603                                  AxisSpace space) {
    604        // create a new space object if one wasn't supplied...
    605        if (space == null) {
    606            space = new AxisSpace();
    607        }
    608
    609        // if the axis is not visible, no additional space is required...
    610        if (!isVisible()) {
    611            return space;
    612        }
    613
    614        // if the axis has a fixed dimension, return it...
    615        double dimension = getFixedDimension();
    616        if (dimension > 0.0) {
    617            space.ensureAtLeast(dimension, edge);
    618        }
    619
    620        // get the axis label size and update the space object...
    621        Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
    622        double labelHeight = 0.0;
    623        double labelWidth = 0.0;
    624        double tickLabelBandsDimension = 0.0;
    625
    626        for (int i = 0; i < this.labelInfo.length; i++) {
    627            PeriodAxisLabelInfo info = this.labelInfo[i];
    628            FontMetrics fm = g2.getFontMetrics(info.getLabelFont());
    629            tickLabelBandsDimension
    630                += info.getPadding().extendHeight(fm.getHeight());
    631        }
    632
    633        if (RectangleEdge.isTopOrBottom(edge)) {
    634            labelHeight = labelEnclosure.getHeight();
    635            space.add(labelHeight + tickLabelBandsDimension, edge);
    636        }
    637        else if (RectangleEdge.isLeftOrRight(edge)) {
    638            labelWidth = labelEnclosure.getWidth();
    639            space.add(labelWidth + tickLabelBandsDimension, edge);
    640        }
    641
    642        // add space for the outer tick labels, if any...
    643        double tickMarkSpace = 0.0;
    644        if (isTickMarksVisible()) {
    645            tickMarkSpace = getTickMarkOutsideLength();
    646        }
    647        if (this.minorTickMarksVisible) {
    648            tickMarkSpace = Math.max(tickMarkSpace,
    649                    this.minorTickMarkOutsideLength);
    650        }
    651        space.add(tickMarkSpace, edge);
    652        return space;
    653    }
    654
    655    /**
    656     * Draws the axis on a Java 2D graphics device (such as the screen or a
    657     * printer).
    658     *
    659     * @param g2  the graphics device (<code>null</code> not permitted).
    660     * @param cursor  the cursor location (determines where to draw the axis).
    661     * @param plotArea  the area within which the axes and plot should be drawn.
    662     * @param dataArea  the area within which the data should be drawn.
    663     * @param edge  the axis location (<code>null</code> not permitted).
    664     * @param plotState  collects information about the plot
    665     *                   (<code>null</code> permitted).
    666     *
    667     * @return The axis state (never <code>null</code>).
    668     */
    669    public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
    670            Rectangle2D dataArea, RectangleEdge edge,
    671            PlotRenderingInfo plotState) {
    672
    673        AxisState axisState = new AxisState(cursor);
    674        if (isAxisLineVisible()) {
    675            drawAxisLine(g2, cursor, dataArea, edge);
    676        }
    677        if (isTickMarksVisible()) {
    678            drawTickMarks(g2, axisState, dataArea, edge);
    679        }
    680        if (isTickLabelsVisible()) {
    681            for (int band = 0; band < this.labelInfo.length; band++) {
    682                axisState = drawTickLabels(band, g2, axisState, dataArea, edge);
    683            }
    684        }
    685
    686        // draw the axis label (note that 'state' is passed in *and*
    687        // returned)...
    688        axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge,
    689                axisState);
    690        return axisState;
    691
    692    }
    693
    694    /**
    695     * Draws the tick marks for the axis.
    696     *
    697     * @param g2  the graphics device.
    698     * @param state  the axis state.
    699     * @param dataArea  the data area.
    700     * @param edge  the edge.
    701     */
    702    protected void drawTickMarks(Graphics2D g2, AxisState state,
    703                                 Rectangle2D dataArea,
    704                                 RectangleEdge edge) {
    705        if (RectangleEdge.isTopOrBottom(edge)) {
    706            drawTickMarksHorizontal(g2, state, dataArea, edge);
    707        }
    708        else if (RectangleEdge.isLeftOrRight(edge)) {
    709            drawTickMarksVertical(g2, state, dataArea, edge);
    710        }
    711    }
    712
    713    /**
    714     * Draws the major and minor tick marks for an axis that lies at the top or
    715     * bottom of the plot.
    716     *
    717     * @param g2  the graphics device.
    718     * @param state  the axis state.
    719     * @param dataArea  the data area.
    720     * @param edge  the edge.
    721     */
    722    protected void drawTickMarksHorizontal(Graphics2D g2, AxisState state,
    723                                           Rectangle2D dataArea,
    724                                           RectangleEdge edge) {
    725        List ticks = new ArrayList();
    726        double x0;
    727        double y0 = state.getCursor();
    728        double insideLength = getTickMarkInsideLength();
    729        double outsideLength = getTickMarkOutsideLength();
    730        RegularTimePeriod t = createInstance(this.majorTickTimePeriodClass,
    731                this.first.getStart(), getTimeZone(), this.locale);
    732        long t0 = t.getFirstMillisecond();
    733        Line2D inside = null;
    734        Line2D outside = null;
    735        long firstOnAxis = getFirst().getFirstMillisecond();
    736        long lastOnAxis = getLast().getLastMillisecond() + 1;
    737        while (t0 <= lastOnAxis) {
    738            ticks.add(new NumberTick(new Double(t0), "", TextAnchor.CENTER,
    739                    TextAnchor.CENTER, 0.0));
    740            x0 = valueToJava2D(t0, dataArea, edge);
    741            if (edge == RectangleEdge.TOP) {
    742                inside = new Line2D.Double(x0, y0, x0, y0 + insideLength);
    743                outside = new Line2D.Double(x0, y0, x0, y0 - outsideLength);
    744            }
    745            else if (edge == RectangleEdge.BOTTOM) {
    746                inside = new Line2D.Double(x0, y0, x0, y0 - insideLength);
    747                outside = new Line2D.Double(x0, y0, x0, y0 + outsideLength);
    748            }
    749            if (t0 >= firstOnAxis) {
    750                g2.setPaint(getTickMarkPaint());
    751                g2.setStroke(getTickMarkStroke());
    752                g2.draw(inside);
    753                g2.draw(outside);
    754            }
    755            // draw minor tick marks
    756            if (this.minorTickMarksVisible) {
    757                RegularTimePeriod tminor = createInstance(
    758                        this.minorTickTimePeriodClass, new Date(t0),
    759                        getTimeZone(), this.locale);
    760                long tt0 = tminor.getFirstMillisecond();
    761                while (tt0 < t.getLastMillisecond()
    762                        && tt0 < lastOnAxis) {
    763                    double xx0 = valueToJava2D(tt0, dataArea, edge);
    764                    if (edge == RectangleEdge.TOP) {
    765                        inside = new Line2D.Double(xx0, y0, xx0,
    766                                y0 + this.minorTickMarkInsideLength);
    767                        outside = new Line2D.Double(xx0, y0, xx0,
    768                                y0 - this.minorTickMarkOutsideLength);
    769                    }
    770                    else if (edge == RectangleEdge.BOTTOM) {
    771                        inside = new Line2D.Double(xx0, y0, xx0,
    772                                y0 - this.minorTickMarkInsideLength);
    773                        outside = new Line2D.Double(xx0, y0, xx0,
    774                                y0 + this.minorTickMarkOutsideLength);
    775                    }
    776                    if (tt0 >= firstOnAxis) {
    777                        g2.setPaint(this.minorTickMarkPaint);
    778                        g2.setStroke(this.minorTickMarkStroke);
    779                        g2.draw(inside);
    780                        g2.draw(outside);
    781                    }
    782                    tminor = tminor.next();
    783                    tminor.peg(this.calendar);
    784                    tt0 = tminor.getFirstMillisecond();
    785                }
    786            }
    787            t = t.next();
    788            t.peg(this.calendar);
    789            t0 = t.getFirstMillisecond();
    790        }
    791        if (edge == RectangleEdge.TOP) {
    792            state.cursorUp(Math.max(outsideLength,
    793                    this.minorTickMarkOutsideLength));
    794        }
    795        else if (edge == RectangleEdge.BOTTOM) {
    796            state.cursorDown(Math.max(outsideLength,
    797                    this.minorTickMarkOutsideLength));
    798        }
    799        state.setTicks(ticks);
    800    }
    801
    802    /**
    803     * Draws the tick marks for a vertical axis.
    804     *
    805     * @param g2  the graphics device.
    806     * @param state  the axis state.
    807     * @param dataArea  the data area.
    808     * @param edge  the edge.
    809     */
    810    protected void drawTickMarksVertical(Graphics2D g2, AxisState state,
    811                                         Rectangle2D dataArea,
    812                                         RectangleEdge edge) {
    813        // FIXME:  implement this...
    814    }
    815
    816    /**
    817     * Draws the tick labels for one "band" of time periods.
    818     *
    819     * @param band  the band index (zero-based).
    820     * @param g2  the graphics device.
    821     * @param state  the axis state.
    822     * @param dataArea  the data area.
    823     * @param edge  the edge where the axis is located.
    824     *
    825     * @return The updated axis state.
    826     */
    827    protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state,
    828                                       Rectangle2D dataArea,
    829                                       RectangleEdge edge) {
    830
    831        // work out the initial gap
    832        double delta1 = 0.0;
    833        FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont());
    834        if (edge == RectangleEdge.BOTTOM) {
    835            delta1 = this.labelInfo[band].getPadding().calculateTopOutset(
    836                    fm.getHeight());
    837        }
    838        else if (edge == RectangleEdge.TOP) {
    839            delta1 = this.labelInfo[band].getPadding().calculateBottomOutset(
    840                    fm.getHeight());
    841        }
    842        state.moveCursor(delta1, edge);
    843        long axisMin = this.first.getFirstMillisecond();
    844        long axisMax = this.last.getLastMillisecond();
    845        g2.setFont(this.labelInfo[band].getLabelFont());
    846        g2.setPaint(this.labelInfo[band].getLabelPaint());
    847
    848        // work out the number of periods to skip for labelling
    849        RegularTimePeriod p1 = this.labelInfo[band].createInstance(
    850                new Date(axisMin), this.timeZone, this.locale);
    851        RegularTimePeriod p2 = this.labelInfo[band].createInstance(
    852                new Date(axisMax), this.timeZone, this.locale);
    853        String label1 = this.labelInfo[band].getDateFormat().format(
    854                new Date(p1.getMiddleMillisecond()));
    855        String label2 = this.labelInfo[band].getDateFormat().format(
    856                new Date(p2.getMiddleMillisecond()));
    857        Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2,
    858                g2.getFontMetrics());
    859        Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2,
    860                g2.getFontMetrics());
    861        double w = Math.max(b1.getWidth(), b2.getWidth());
    862        long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0,
    863                dataArea, edge));
    864        if (isInverted()) {
    865            ww = axisMax - ww;
    866        }
    867        else {
    868            ww = ww - axisMin;
    869        }
    870        long length = p1.getLastMillisecond()
    871                      - p1.getFirstMillisecond();
    872        int periods = (int) (ww / length) + 1;
    873
    874        RegularTimePeriod p = this.labelInfo[band].createInstance(
    875                new Date(axisMin), this.timeZone, this.locale);
    876        Rectangle2D b = null;
    877        long lastXX = 0L;
    878        float y = (float) (state.getCursor());
    879        TextAnchor anchor = TextAnchor.TOP_CENTER;
    880        float yDelta = (float) b1.getHeight();
    881        if (edge == RectangleEdge.TOP) {
    882            anchor = TextAnchor.BOTTOM_CENTER;
    883            yDelta = -yDelta;
    884        }
    885        while (p.getFirstMillisecond() <= axisMax) {
    886            float x = (float) valueToJava2D(p.getMiddleMillisecond(), dataArea,
    887                    edge);
    888            DateFormat df = this.labelInfo[band].getDateFormat();
    889            String label = df.format(new Date(p.getMiddleMillisecond()));
    890            long first = p.getFirstMillisecond();
    891            long last = p.getLastMillisecond();
    892            if (last > axisMax) {
    893                // this is the last period, but it is only partially visible
    894                // so check that the label will fit before displaying it...
    895                Rectangle2D bb = TextUtilities.getTextBounds(label, g2,
    896                        g2.getFontMetrics());
    897                if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {
    898                    float xstart = (float) valueToJava2D(Math.max(first,
    899                            axisMin), dataArea, edge);
    900                    if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {
    901                        x = ((float) dataArea.getMaxX() + xstart) / 2.0f;
    902                    }
    903                    else {
    904                        label = null;
    905                    }
    906                }
    907            }
    908            if (first < axisMin) {
    909                // this is the first period, but it is only partially visible
    910                // so check that the label will fit before displaying it...
    911                Rectangle2D bb = TextUtilities.getTextBounds(label, g2,
    912                        g2.getFontMetrics());
    913                if ((x - bb.getWidth() / 2) < dataArea.getX()) {
    914                    float xlast = (float) valueToJava2D(Math.min(last,
    915                            axisMax), dataArea, edge);
    916                    if (bb.getWidth() < (xlast - dataArea.getX())) {
    917                        x = (xlast + (float) dataArea.getX()) / 2.0f;
    918                    }
    919                    else {
    920                        label = null;
    921                    }
    922                }
    923
    924            }
    925            if (label != null) {
    926                g2.setPaint(this.labelInfo[band].getLabelPaint());
    927                b = TextUtilities.drawAlignedString(label, g2, x, y, anchor);
    928            }
    929            if (lastXX > 0L) {
    930                if (this.labelInfo[band].getDrawDividers()) {
    931                    long nextXX = p.getFirstMillisecond();
    932                    long mid = (lastXX + nextXX) / 2;
    933                    float mid2d = (float) valueToJava2D(mid, dataArea, edge);
    934                    g2.setStroke(this.labelInfo[band].getDividerStroke());
    935                    g2.setPaint(this.labelInfo[band].getDividerPaint());
    936                    g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta));
    937                }
    938            }
    939            lastXX = last;
    940            for (int i = 0; i < periods; i++) {
    941                p = p.next();
    942            }
    943            p.peg(this.calendar);
    944        }
    945        double used = 0.0;
    946        if (b != null) {
    947            used = b.getHeight();
    948            // work out the trailing gap
    949            if (edge == RectangleEdge.BOTTOM) {
    950                used += this.labelInfo[band].getPadding().calculateBottomOutset(
    951                        fm.getHeight());
    952            }
    953            else if (edge == RectangleEdge.TOP) {
    954                used += this.labelInfo[band].getPadding().calculateTopOutset(
    955                        fm.getHeight());
    956            }
    957        }
    958        state.moveCursor(used, edge);
    959        return state;
    960    }
    961
    962    /**
    963     * Calculates the positions of the ticks for the axis, storing the results
    964     * in the tick list (ready for drawing).
    965     *
    966     * @param g2  the graphics device.
    967     * @param state  the axis state.
    968     * @param dataArea  the area inside the axes.
    969     * @param edge  the edge on which the axis is located.
    970     *
    971     * @return The list of ticks.
    972     */
    973    public List refreshTicks(Graphics2D g2, AxisState state,
    974            Rectangle2D dataArea, RectangleEdge edge) {
    975        return Collections.EMPTY_LIST;
    976    }
    977
    978    /**
    979     * Converts a data value to a coordinate in Java2D space, assuming that the
    980     * axis runs along one edge of the specified dataArea.
    981     * <p>
    982     * Note that it is possible for the coordinate to fall outside the area.
    983     *
    984     * @param value  the data value.
    985     * @param area  the area for plotting the data.
    986     * @param edge  the edge along which the axis lies.
    987     *
    988     * @return The Java2D coordinate.
    989     */
    990    public double valueToJava2D(double value, Rectangle2D area,
    991            RectangleEdge edge) {
    992
    993        double result = Double.NaN;
    994        double axisMin = this.first.getFirstMillisecond();
    995        double axisMax = this.last.getLastMillisecond();
    996        if (RectangleEdge.isTopOrBottom(edge)) {
    997            double minX = area.getX();
    998            double maxX = area.getMaxX();
    999            if (isInverted()) {
    1000                result = maxX + ((value - axisMin) / (axisMax - axisMin))
    1001                         * (minX - maxX);
    1002            }
    1003            else {
    1004                result = minX + ((value - axisMin) / (axisMax - axisMin))
    1005                         * (maxX - minX);
    1006            }
    1007        }
    1008        else if (RectangleEdge.isLeftOrRight(edge)) {
    1009            double minY = area.getMinY();
    1010            double maxY = area.getMaxY();
    1011            if (isInverted()) {
    1012                result = minY + (((value - axisMin) / (axisMax - axisMin))
    1013                         * (maxY - minY));
    1014            }
    1015            else {
    1016                result = maxY - (((value - axisMin) / (axisMax - axisMin))
    1017                         * (maxY - minY));
    1018            }
    1019        }
    1020        return result;
    1021
    1022    }
    1023
    1024    /**
    1025     * Converts a coordinate in Java2D space to the corresponding data value,
    1026     * assuming that the axis runs along one edge of the specified dataArea.
    1027     *
    1028     * @param java2DValue  the coordinate in Java2D space.
    1029     * @param area  the area in which the data is plotted.
    1030     * @param edge  the edge along which the axis lies.
    1031     *
    1032     * @return The data value.
    1033     */
    1034    public double java2DToValue(double java2DValue, Rectangle2D area,
    1035            RectangleEdge edge) {
    1036
    1037        double result = Double.NaN;
    1038        double min = 0.0;
    1039        double max = 0.0;
    1040        double axisMin = this.first.getFirstMillisecond();
    1041        double axisMax = this.last.getLastMillisecond();
    1042        if (RectangleEdge.isTopOrBottom(edge)) {
    1043            min = area.getX();
    1044            max = area.getMaxX();
    1045        }
    1046        else if (RectangleEdge.isLeftOrRight(edge)) {
    1047            min = area.getMaxY();
    1048            max = area.getY();
    1049        }
    1050        if (isInverted()) {
    1051             result = axisMax - ((java2DValue - min) / (max - min)
    1052                      * (axisMax - axisMin));
    1053        }
    1054        else {
    1055             result = axisMin + ((java2DValue - min) / (max - min)
    1056                      * (axisMax - axisMin));
    1057        }
    1058        return result;
    1059    }
    1060
    1061    /**
    1062     * Rescales the axis to ensure that all data is visible.
    1063     */
    1064    protected void autoAdjustRange() {
    1065
    1066        Plot plot = getPlot();
    1067        if (plot == null) {
    1068            return;  // no plot, no data
    1069        }
    1070
    1071        if (plot instanceof ValueAxisPlot) {
    1072            ValueAxisPlot vap = (ValueAxisPlot) plot;
    1073
    1074            Range r = vap.getDataRange(this);
    1075            if (r == null) {
    1076                r = getDefaultAutoRange();
    1077            }
    1078
    1079            long upper = Math.round(r.getUpperBound());
    1080            long lower = Math.round(r.getLowerBound());
    1081            this.first = createInstance(this.autoRangeTimePeriodClass,
    1082                    new Date(lower), this.timeZone, this.locale);
    1083            this.last = createInstance(this.autoRangeTimePeriodClass,
    1084                    new Date(upper), this.timeZone, this.locale);
    1085            setRange(r, false, false);
    1086        }
    1087
    1088    }
    1089
    1090    /**
    1091     * Tests the axis for equality with an arbitrary object.
    1092     *
    1093     * @param obj  the object (<code>null</code> permitted).
    1094     *
    1095     * @return A boolean.
    1096     */
    1097    public boolean equals(Object obj) {
    1098        if (obj == this) {
    1099            return true;
    1100        }
    1101        if (!(obj instanceof PeriodAxis)) {
    1102            return false;
    1103        }
    1104        PeriodAxis that = (PeriodAxis) obj;
    1105        if (!this.first.equals(that.first)) {
    1106            return false;
    1107        }
    1108        if (!this.last.equals(that.last)) {
    1109            return false;
    1110        }
    1111        if (!this.timeZone.equals(that.timeZone)) {
    1112            return false;
    1113        }
    1114        if (!this.locale.equals(that.locale)) {
    1115            return false;
    1116        }
    1117        if (!this.autoRangeTimePeriodClass.equals(
    1118                that.autoRangeTimePeriodClass)) {
    1119            return false;
    1120        }
    1121        if (!(isMinorTickMarksVisible() == that.isMinorTickMarksVisible())) {
    1122            return false;
    1123        }
    1124        if (!this.majorTickTimePeriodClass.equals(
    1125                that.majorTickTimePeriodClass)) {
    1126            return false;
    1127        }
    1128        if (!this.minorTickTimePeriodClass.equals(
    1129                that.minorTickTimePeriodClass)) {
    1130            return false;
    1131        }
    1132        if (!this.minorTickMarkPaint.equals(that.minorTickMarkPaint)) {
    1133            return false;
    1134        }
    1135        if (!this.minorTickMarkStroke.equals(that.minorTickMarkStroke)) {
    1136            return false;
    1137        }
    1138        if (!Arrays.equals(this.labelInfo, that.labelInfo)) {
    1139            return false;
    1140        }
    1141        return super.equals(obj);
    1142    }
    1143
    1144    /**
    1145     * Returns a hash code for this object.
    1146     *
    1147     * @return A hash code.
    1148     */
    1149    public int hashCode() {
    1150        if (getLabel() != null) {
    1151            return getLabel().hashCode();
    1152        }
    1153        else {
    1154            return 0;
    1155        }
    1156    }
    1157
    1158    /**
    1159     * Returns a clone of the axis.
    1160     *
    1161     * @return A clone.
    1162     *
    1163     * @throws CloneNotSupportedException  this class is cloneable, but
    1164     *         subclasses may not be.
    1165     */
    1166    public Object clone() throws CloneNotSupportedException {
    1167        PeriodAxis clone = (PeriodAxis) super.clone();
    1168        clone.timeZone = (TimeZone) this.timeZone.clone();
    1169        clone.labelInfo = new PeriodAxisLabelInfo[this.labelInfo.length];
    1170        for (int i = 0; i < this.labelInfo.length; i++) {
    1171            clone.labelInfo[i] = this.labelInfo[i];  // copy across references
    1172                                                     // to immutable objs
    1173        }
    1174        return clone;
    1175    }
    1176
    1177    /**
    1178     * A utility method used to create a particular subclass of the
    1179     * {@link RegularTimePeriod} class that includes the specified millisecond,
    1180     * assuming the specified time zone.
    1181     *
    1182     * @param periodClass  the class.
    1183     * @param millisecond  the time.
    1184     * @param zone  the time zone.
    1185     * @param locale  the locale.
    1186     *
    1187     * @return The time period.
    1188     */
    1189    private RegularTimePeriod createInstance(Class periodClass,
    1190            Date millisecond, TimeZone zone, Locale locale) {
    1191        RegularTimePeriod result = null;
    1192        try {
    1193            Constructor c = periodClass.getDeclaredConstructor(new Class[] {
    1194                    Date.class, TimeZone.class, Locale.class});
    1195            result = (RegularTimePeriod) c.newInstance(new Object[] {
    1196                    millisecond, zone, locale});
    1197        }
    1198        catch (Exception e) {
    1199            try {
    1200                Constructor c = periodClass.getDeclaredConstructor(new Class[] {
    1201                        Date.class});
    1202                result = (RegularTimePeriod) c.newInstance(new Object[] {
    1203                        millisecond});
    1204            }
    1205            catch (Exception e2) {
    1206                // do nothing
    1207            }
    1208        }
    1209        return result;
    1210    }
    1211
    1212    /**
    1213     * Provides serialization support.
    1214     *
    1215     * @param stream  the output stream.
    1216     *
    1217     * @throws IOException  if there is an I/O error.
    1218     */
    1219    private void writeObject(ObjectOutputStream stream) throws IOException {
    1220        stream.defaultWriteObject();
    1221        SerialUtilities.writeStroke(this.minorTickMarkStroke, stream);
    1222        SerialUtilities.writePaint(this.minorTickMarkPaint, stream);
    1223    }
    1224
    1225    /**
    1226     * Provides serialization support.
    1227     *
    1228     * @param stream  the input stream.
    1229     *
    1230     * @throws IOException  if there is an I/O error.
    1231     * @throws ClassNotFoundException  if there is a classpath problem.
    1232     */
    1233    private void readObject(ObjectInputStream stream)
    1234        throws IOException, ClassNotFoundException {
    1235        stream.defaultReadObject();
    1236        this.minorTickMarkStroke = SerialUtilities.readStroke(stream);
    1237        this.minorTickMarkPaint = SerialUtilities.readPaint(stream);
    1238    }
    1239
    1240}