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