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