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