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