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