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