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