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