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