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