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