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