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