/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
 *
 * Project Info:  http://www.jfree.org/jfreechart/index.html
 *
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published by 
 * the Free Software Foundation; either version 2.1 of the License, or 
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
 * USA.  
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
 * in the United States and other countries.]
 *
 * ---------------
 * PeriodAxis.java
 * ---------------
 * (C) Copyright 2004-2007, by Object Refinery Limited and Contributors.
 *
 * Original Author:  David Gilbert (for Object Refinery Limited);
 * Contributor(s):   -;
 *
 * Changes
 * -------
 * 01-Jun-2004 : Version 1 (DG);
 * 16-Sep-2004 : Fixed bug in equals() method, added clone() method and 
 *               PublicCloneable interface (DG);
 * 25-Nov-2004 : Updates to support major and minor tick marks (DG);
 * 25-Feb-2005 : Fixed some tick mark bugs (DG);
 * 15-Apr-2005 : Fixed some more tick mark bugs (DG);
 * 26-Apr-2005 : Removed LOGGER (DG);
 * 16-Jun-2005 : Fixed zooming (DG);
 * 15-Sep-2005 : Changed configure() method to check autoRange flag,
 *               and added ticks to state (DG);
 * ------------- JFREECHART 1.0.x ---------------------------------------------
 * 06-Oct-2006 : Updated for deprecations in RegularTimePeriod and 
 *               subclasses (DG);
 * 22-Mar-2007 : Use new defaultAutoRange attribute (DG);
 * 31-Jul-2007 : Fix for inverted axis labelling (see bug 1763413) (DG);
 *
 */

package org.jfree.chart.axis;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import org.jfree.chart.event.AxisChangeEvent;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.ValueAxisPlot;
import org.jfree.data.Range;
import org.jfree.data.time.Day;
import org.jfree.data.time.Month;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.Year;
import org.jfree.io.SerialUtilities;
import org.jfree.text.TextUtilities;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.TextAnchor;
import org.jfree.util.PublicCloneable;

/**
 * An axis that displays a date scale based on a 
 * {@link org.jfree.data.time.RegularTimePeriod}.  This axis works when
 * displayed across the bottom or top of a plot, but is broken for display at
 * the left or right of charts.
 */
public class PeriodAxis extends ValueAxis 
                        implements Cloneable, PublicCloneable, Serializable {
    
    /** For serialization. */
    private static final long serialVersionUID = 8353295532075872069L;
    
    /** The first time period in the overall range. */
    private RegularTimePeriod first;
    
    /** The last time period in the overall range. */
    private RegularTimePeriod last;
    
    /** 
     * The time zone used to convert 'first' and 'last' to absolute 
     * milliseconds. 
     */
    private TimeZone timeZone;
    
    /** 
     * A calendar used for date manipulations in the current time zone.
     */
    private Calendar calendar;
    
    /** 
     * The {@link RegularTimePeriod} subclass used to automatically determine 
     * the axis range. 
     */
    private Class autoRangeTimePeriodClass;
    
    /** 
     * Indicates the {@link RegularTimePeriod} subclass that is used to 
     * determine the spacing of the major tick marks.
     */
    private Class majorTickTimePeriodClass;
    
    /** 
     * A flag that indicates whether or not tick marks are visible for the 
     * axis. 
     */
    private boolean minorTickMarksVisible;

    /** 
     * Indicates the {@link RegularTimePeriod} subclass that is used to 
     * determine the spacing of the minor tick marks.
     */
    private Class minorTickTimePeriodClass;
    
    /** The length of the tick mark inside the data area (zero permitted). */
    private float minorTickMarkInsideLength = 0.0f;

    /** The length of the tick mark outside the data area (zero permitted). */
    private float minorTickMarkOutsideLength = 2.0f;

    /** The stroke used to draw tick marks. */
    private transient Stroke minorTickMarkStroke = new BasicStroke(0.5f);

    /** The paint used to draw tick marks. */
    private transient Paint minorTickMarkPaint = Color.black;
    
    /** Info for each labelling band. */
    private PeriodAxisLabelInfo[] labelInfo;

    /**
     * Creates a new axis.
     * 
     * @param label  the axis label.
     */
    public PeriodAxis(String label) {
        this(label, new Day(), new Day());
    }
    
    /**
     * Creates a new axis.
     * 
     * @param label  the axis label (<code>null</code> permitted).
     * @param first  the first time period in the axis range 
     *               (<code>null</code> not permitted).
     * @param last  the last time period in the axis range 
     *              (<code>null</code> not permitted).
     */
    public PeriodAxis(String label, 
                      RegularTimePeriod first, RegularTimePeriod last) {
        this(label, first, last, TimeZone.getDefault());
    }
    
    /**
     * Creates a new axis.
     * 
     * @param label  the axis label (<code>null</code> permitted).
     * @param first  the first time period in the axis range 
     *               (<code>null</code> not permitted).
     * @param last  the last time period in the axis range 
     *              (<code>null</code> not permitted).
     * @param timeZone  the time zone (<code>null</code> not permitted).
     */
    public PeriodAxis(String label, 
                      RegularTimePeriod first, RegularTimePeriod last, 
                      TimeZone timeZone) {
        
        super(label, null);
        this.first = first;
        this.last = last;
        this.timeZone = timeZone;
        this.calendar = Calendar.getInstance(timeZone);
        this.autoRangeTimePeriodClass = first.getClass();
        this.majorTickTimePeriodClass = first.getClass();
        this.minorTickMarksVisible = false;
        this.minorTickTimePeriodClass = RegularTimePeriod.downsize(
                this.majorTickTimePeriodClass);
        setAutoRange(true);
        this.labelInfo = new PeriodAxisLabelInfo[2];
        this.labelInfo[0] = new PeriodAxisLabelInfo(Month.class, 
                new SimpleDateFormat("MMM"));
        this.labelInfo[1] = new PeriodAxisLabelInfo(Year.class, 
                new SimpleDateFormat("yyyy"));
        
    }
    
    /**
     * Returns the first time period in the axis range.
     * 
     * @return The first time period (never <code>null</code>).
     */
    public RegularTimePeriod getFirst() {
        return this.first;
    }
    
    /**
     * Sets the first time period in the axis range and sends an 
     * {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param first  the time period (<code>null</code> not permitted).
     */
    public void setFirst(RegularTimePeriod first) {
        if (first == null) {
            throw new IllegalArgumentException("Null 'first' argument.");   
        }
        this.first = first;   
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the last time period in the axis range.
     * 
     * @return The last time period (never <code>null</code>).
     */
    public RegularTimePeriod getLast() {
        return this.last;
    }
    
    /**
     * Sets the last time period in the axis range and sends an 
     * {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param last  the time period (<code>null</code> not permitted).
     */
    public void setLast(RegularTimePeriod last) {
        if (last == null) {
            throw new IllegalArgumentException("Null 'last' argument.");   
        }
        this.last = last;   
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the time zone used to convert the periods defining the axis 
     * range into absolute milliseconds.
     * 
     * @return The time zone (never <code>null</code>).
     */
    public TimeZone getTimeZone() {
        return this.timeZone;   
    }
    
    /**
     * Sets the time zone that is used to convert the time periods into 
     * absolute milliseconds.
     * 
     * @param zone  the time zone (<code>null</code> not permitted).
     */
    public void setTimeZone(TimeZone zone) {
        if (zone == null) {
            throw new IllegalArgumentException("Null 'zone' argument.");   
        }
        this.timeZone = zone;
        this.calendar = Calendar.getInstance(zone);
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the class used to create the first and last time periods for 
     * the axis range when the auto-range flag is set to <code>true</code>.
     * 
     * @return The class (never <code>null</code>).
     */
    public Class getAutoRangeTimePeriodClass() {
        return this.autoRangeTimePeriodClass;   
    }
    
    /**
     * Sets the class used to create the first and last time periods for the 
     * axis range when the auto-range flag is set to <code>true</code> and 
     * sends an {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param c  the class (<code>null</code> not permitted).
     */
    public void setAutoRangeTimePeriodClass(Class c) {
        if (c == null) {
            throw new IllegalArgumentException("Null 'c' argument.");   
        }
        this.autoRangeTimePeriodClass = c;   
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the class that controls the spacing of the major tick marks.
     * 
     * @return The class (never <code>null</code>).
     */
    public Class getMajorTickTimePeriodClass() {
        return this.majorTickTimePeriodClass;
    }
    
    /**
     * Sets the class that controls the spacing of the major tick marks, and 
     * sends an {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param c  the class (a subclass of {@link RegularTimePeriod} is 
     *           expected).
     */
    public void setMajorTickTimePeriodClass(Class c) {
        if (c == null) {
            throw new IllegalArgumentException("Null 'c' argument.");
        }
        this.majorTickTimePeriodClass = c;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the flag that controls whether or not minor tick marks
     * are displayed for the axis.
     * 
     * @return A boolean.
     */
    public boolean isMinorTickMarksVisible() {
        return this.minorTickMarksVisible;
    }
    
    /**
     * Sets the flag that controls whether or not minor tick marks
     * are displayed for the axis, and sends a {@link AxisChangeEvent}
     * to all registered listeners.
     * 
     * @param visible  the flag.
     */
    public void setMinorTickMarksVisible(boolean visible) {
        this.minorTickMarksVisible = visible;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the class that controls the spacing of the minor tick marks.
     * 
     * @return The class (never <code>null</code>).
     */
    public Class getMinorTickTimePeriodClass() {
        return this.minorTickTimePeriodClass;
    }
    
    /**
     * Sets the class that controls the spacing of the minor tick marks, and 
     * sends an {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param c  the class (a subclass of {@link RegularTimePeriod} is 
     *           expected).
     */
    public void setMinorTickTimePeriodClass(Class c) {
        if (c == null) {
            throw new IllegalArgumentException("Null 'c' argument.");
        }
        this.minorTickTimePeriodClass = c;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the stroke used to display minor tick marks, if they are 
     * visible.
     * 
     * @return A stroke (never <code>null</code>).
     */
    public Stroke getMinorTickMarkStroke() {
        return this.minorTickMarkStroke;
    }
    
    /**
     * Sets the stroke used to display minor tick marks, if they are 
     * visible, and sends a {@link AxisChangeEvent} to all registered 
     * listeners.
     * 
     * @param stroke  the stroke (<code>null</code> not permitted).
     */
    public void setMinorTickMarkStroke(Stroke stroke) {
        if (stroke == null) {
            throw new IllegalArgumentException("Null 'stroke' argument.");
        }
        this.minorTickMarkStroke = stroke;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the paint used to display minor tick marks, if they are 
     * visible.
     * 
     * @return A paint (never <code>null</code>).
     */
    public Paint getMinorTickMarkPaint() {
        return this.minorTickMarkPaint;
    }
    
    /**
     * Sets the paint used to display minor tick marks, if they are 
     * visible, and sends a {@link AxisChangeEvent} to all registered 
     * listeners.
     * 
     * @param paint  the paint (<code>null</code> not permitted).
     */
    public void setMinorTickMarkPaint(Paint paint) {
        if (paint == null) {
            throw new IllegalArgumentException("Null 'paint' argument.");
        }
        this.minorTickMarkPaint = paint;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the inside length for the minor tick marks.
     * 
     * @return The length.
     */
    public float getMinorTickMarkInsideLength() {
        return this.minorTickMarkInsideLength;   
    }
    
    /**
     * Sets the inside length of the minor tick marks and sends an 
     * {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param length  the length.
     */
    public void setMinorTickMarkInsideLength(float length) {
        this.minorTickMarkInsideLength = length;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns the outside length for the minor tick marks.
     * 
     * @return The length.
     */
    public float getMinorTickMarkOutsideLength() {
        return this.minorTickMarkOutsideLength;   
    }
    
    /**
     * Sets the outside length of the minor tick marks and sends an 
     * {@link AxisChangeEvent} to all registered listeners.
     * 
     * @param length  the length.
     */
    public void setMinorTickMarkOutsideLength(float length) {
        this.minorTickMarkOutsideLength = length;
        notifyListeners(new AxisChangeEvent(this));
    }
    
    /**
     * Returns an array of label info records.
     * 
     * @return An array.
     */
    public PeriodAxisLabelInfo[] getLabelInfo() {
        return this.labelInfo;    
    }
    
    /**
     * Sets the array of label info records.
     * 
     * @param info  the info.
     */
    public void setLabelInfo(PeriodAxisLabelInfo[] info) {
        this.labelInfo = info;
        // FIXME: shouldn't this generate an event?
    }
    
    /**
     * Returns the range for the axis.
     *
     * @return The axis range (never <code>null</code>).
     */
    public Range getRange() {
        // TODO: find a cleaner way to do this...
        return new Range(this.first.getFirstMillisecond(this.calendar), 
                this.last.getLastMillisecond(this.calendar));
    }

    /**
     * Sets the range for the axis, if requested, sends an 
     * {@link AxisChangeEvent} to all registered listeners.  As a side-effect, 
     * the auto-range flag is set to <code>false</code> (optional).
     *
     * @param range  the range (<code>null</code> not permitted).
     * @param turnOffAutoRange  a flag that controls whether or not the auto 
     *                          range is turned off.         
     * @param notify  a flag that controls whether or not listeners are 
     *                notified.
     */
    public void setRange(Range range, boolean turnOffAutoRange, 
                         boolean notify) {
        super.setRange(range, turnOffAutoRange, false);
        long upper = Math.round(range.getUpperBound());
        long lower = Math.round(range.getLowerBound());
        this.first = createInstance(this.autoRangeTimePeriodClass, 
                new Date(lower), this.timeZone);
        this.last = createInstance(this.autoRangeTimePeriodClass, 
                new Date(upper), this.timeZone);        
    }

    /**
     * Configures the axis to work with the current plot.  Override this method
     * to perform any special processing (such as auto-rescaling).
     */
    public void configure() {
        if (this.isAutoRange()) {
            autoAdjustRange();
        }
    }

    /**
     * Estimates the space (height or width) required to draw the axis.
     *
     * @param g2  the graphics device.
     * @param plot  the plot that the axis belongs to.
     * @param plotArea  the area within which the plot (including axes) should 
     *                  be drawn.
     * @param edge  the axis location.
     * @param space  space already reserved.
     *
     * @return The space required to draw the axis (including pre-reserved 
     *         space).
     */
    public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 
                                  Rectangle2D plotArea, RectangleEdge edge, 
                                  AxisSpace space) {
        // create a new space object if one wasn't supplied...
        if (space == null) {
            space = new AxisSpace();
        }
        
        // if the axis is not visible, no additional space is required...
        if (!isVisible()) {
            return space;
        }

        // if the axis has a fixed dimension, return it...
        double dimension = getFixedDimension();
        if (dimension > 0.0) {
            space.ensureAtLeast(dimension, edge);
        }
        
        // get the axis label size and update the space object...
        Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
        double labelHeight = 0.0;
        double labelWidth = 0.0;
        double tickLabelBandsDimension = 0.0;
        
        for (int i = 0; i < this.labelInfo.length; i++) {
            PeriodAxisLabelInfo info = this.labelInfo[i];
            FontMetrics fm = g2.getFontMetrics(info.getLabelFont());
            tickLabelBandsDimension 
                += info.getPadding().extendHeight(fm.getHeight());
        }
        
        if (RectangleEdge.isTopOrBottom(edge)) {
            labelHeight = labelEnclosure.getHeight();
            space.add(labelHeight + tickLabelBandsDimension, edge);
        }
        else if (RectangleEdge.isLeftOrRight(edge)) {
            labelWidth = labelEnclosure.getWidth();
            space.add(labelWidth + tickLabelBandsDimension, edge);
        }

        // add space for the outer tick labels, if any...
        double tickMarkSpace = 0.0;
        if (isTickMarksVisible()) {
            tickMarkSpace = getTickMarkOutsideLength();
        }
        if (this.minorTickMarksVisible) {
            tickMarkSpace = Math.max(tickMarkSpace, 
                    this.minorTickMarkOutsideLength);
        }
        space.add(tickMarkSpace, edge);
        return space;
    }

    /**
     * Draws the axis on a Java 2D graphics device (such as the screen or a 
     * printer).
     *
     * @param g2  the graphics device (<code>null</code> not permitted).
     * @param cursor  the cursor location (determines where to draw the axis).
     * @param plotArea  the area within which the axes and plot should be drawn.
     * @param dataArea  the area within which the data should be drawn.
     * @param edge  the axis location (<code>null</code> not permitted).
     * @param plotState  collects information about the plot 
     *                   (<code>null</code> permitted).
     * 
     * @return The axis state (never <code>null</code>).
     */
    public AxisState draw(Graphics2D g2, 
                          double cursor,
                          Rectangle2D plotArea, 
                          Rectangle2D dataArea,
                          RectangleEdge edge,
                          PlotRenderingInfo plotState) {
        
        AxisState axisState = new AxisState(cursor);
        if (isAxisLineVisible()) {
            drawAxisLine(g2, cursor, dataArea, edge);
        }
        drawTickMarks(g2, axisState, dataArea, edge);
        for (int band = 0; band < this.labelInfo.length; band++) {
            axisState = drawTickLabels(band, g2, axisState, dataArea, edge);
        }
        
        // draw the axis label (note that 'state' is passed in *and* 
        // returned)...
        axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge, 
                axisState);
        return axisState;
        
    }
    
    /**
     * Draws the tick marks for the axis.
     * 
     * @param g2  the graphics device.
     * @param state  the axis state.
     * @param dataArea  the data area.
     * @param edge  the edge.
     */
    protected void drawTickMarks(Graphics2D g2, AxisState state, 
                                 Rectangle2D dataArea, 
                                 RectangleEdge edge) {
        if (RectangleEdge.isTopOrBottom(edge)) {
            drawTickMarksHorizontal(g2, state, dataArea, edge);
        }
        else if (RectangleEdge.isLeftOrRight(edge)) {
            drawTickMarksVertical(g2, state, dataArea, edge);
        }
    }
    
    /**
     * Draws the major and minor tick marks for an axis that lies at the top or 
     * bottom of the plot.
     * 
     * @param g2  the graphics device.
     * @param state  the axis state.
     * @param dataArea  the data area.
     * @param edge  the edge.
     */
    protected void drawTickMarksHorizontal(Graphics2D g2, AxisState state, 
                                           Rectangle2D dataArea, 
                                           RectangleEdge edge) {
        List ticks = new ArrayList();
        double x0 = dataArea.getX();
        double y0 = state.getCursor();
        double insideLength = getTickMarkInsideLength();
        double outsideLength = getTickMarkOutsideLength();
        RegularTimePeriod t = RegularTimePeriod.createInstance(
                this.majorTickTimePeriodClass, this.first.getStart(), 
                getTimeZone());
        long t0 = t.getFirstMillisecond(this.calendar);
        Line2D inside = null;
        Line2D outside = null;
        long firstOnAxis = getFirst().getFirstMillisecond(this.calendar);
        long lastOnAxis = getLast().getLastMillisecond(this.calendar);
        while (t0 <= lastOnAxis) {
            ticks.add(new NumberTick(new Double(t0), "", TextAnchor.CENTER, 
                    TextAnchor.CENTER, 0.0));
            x0 = valueToJava2D(t0, dataArea, edge);
            if (edge == RectangleEdge.TOP) {
                inside = new Line2D.Double(x0, y0, x0, y0 + insideLength);  
                outside = new Line2D.Double(x0, y0, x0, y0 - outsideLength);
            }
            else if (edge == RectangleEdge.BOTTOM) {
                inside = new Line2D.Double(x0, y0, x0, y0 - insideLength);
                outside = new Line2D.Double(x0, y0, x0, y0 + outsideLength);
            }
            if (t0 > firstOnAxis) {
                g2.setPaint(getTickMarkPaint());
                g2.setStroke(getTickMarkStroke());
                g2.draw(inside);
                g2.draw(outside);
            }
            // draw minor tick marks
            if (this.minorTickMarksVisible) {
                RegularTimePeriod tminor = RegularTimePeriod.createInstance(
                        this.minorTickTimePeriodClass, new Date(t0), 
                        getTimeZone());
                long tt0 = tminor.getFirstMillisecond(this.calendar);
                while (tt0 < t.getLastMillisecond(this.calendar) 
                        && tt0 < lastOnAxis) {
                    double xx0 = valueToJava2D(tt0, dataArea, edge);
                    if (edge == RectangleEdge.TOP) {
                        inside = new Line2D.Double(xx0, y0, xx0, 
                                y0 + this.minorTickMarkInsideLength);
                        outside = new Line2D.Double(xx0, y0, xx0, 
                                y0 - this.minorTickMarkOutsideLength);
                    }
                    else if (edge == RectangleEdge.BOTTOM) {
                        inside = new Line2D.Double(xx0, y0, xx0, 
                                y0 - this.minorTickMarkInsideLength);
                        outside = new Line2D.Double(xx0, y0, xx0, 
                                y0 + this.minorTickMarkOutsideLength);
                    }
                    if (tt0 >= firstOnAxis) {
                        g2.setPaint(this.minorTickMarkPaint);
                        g2.setStroke(this.minorTickMarkStroke);
                        g2.draw(inside);
                        g2.draw(outside);
                    }
                    tminor = tminor.next();
                    tt0 = tminor.getFirstMillisecond(this.calendar);
                }
            }            
            t = t.next();
            t0 = t.getFirstMillisecond(this.calendar);
        }
        if (edge == RectangleEdge.TOP) {
            state.cursorUp(Math.max(outsideLength, 
                    this.minorTickMarkOutsideLength));
        }
        else if (edge == RectangleEdge.BOTTOM) {
            state.cursorDown(Math.max(outsideLength, 
                    this.minorTickMarkOutsideLength));
        }
        state.setTicks(ticks);
    }
    
    /**
     * Draws the tick marks for a vertical axis.
     * 
     * @param g2  the graphics device.
     * @param state  the axis state.
     * @param dataArea  the data area.
     * @param edge  the edge.
     */
    protected void drawTickMarksVertical(Graphics2D g2, AxisState state, 
                                         Rectangle2D dataArea, 
                                         RectangleEdge edge) {
        // FIXME:  implement this...       
    }
    
    /**
     * Draws the tick labels for one "band" of time periods.
     * 
     * @param band  the band index (zero-based).
     * @param g2  the graphics device.
     * @param state  the axis state.
     * @param dataArea  the data area.
     * @param edge  the edge where the axis is located.
     * 
     * @return The updated axis state.
     */
    protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state,
                                       Rectangle2D dataArea, 
                                       RectangleEdge edge) {

        // work out the initial gap
        double delta1 = 0.0;
        FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont());
        if (edge == RectangleEdge.BOTTOM) {
            delta1 = this.labelInfo[band].getPadding().calculateTopOutset(
                    fm.getHeight());   
        }
        else if (edge == RectangleEdge.TOP) {
            delta1 = this.labelInfo[band].getPadding().calculateBottomOutset(
                    fm.getHeight());   
        }
        state.moveCursor(delta1, edge);
        long axisMin = this.first.getFirstMillisecond(this.calendar);
        long axisMax = this.last.getLastMillisecond(this.calendar);
        g2.setFont(this.labelInfo[band].getLabelFont());
        g2.setPaint(this.labelInfo[band].getLabelPaint());

        // work out the number of periods to skip for labelling
        RegularTimePeriod p1 = this.labelInfo[band].createInstance(
                new Date(axisMin), this.timeZone);
        RegularTimePeriod p2 = this.labelInfo[band].createInstance(
                new Date(axisMax), this.timeZone);
        String label1 = this.labelInfo[band].getDateFormat().format(
                new Date(p1.getMiddleMillisecond(this.calendar)));
        String label2 = this.labelInfo[band].getDateFormat().format(
                new Date(p2.getMiddleMillisecond(this.calendar)));
        Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2, 
                g2.getFontMetrics());
        Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2, 
                g2.getFontMetrics());
        double w = Math.max(b1.getWidth(), b2.getWidth());
        long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0, 
                dataArea, edge));
        if (isInverted()) {
            ww = axisMax - ww;
        }
        else {
            ww = ww - axisMin;
        }
        long length = p1.getLastMillisecond(this.calendar) 
                      - p1.getFirstMillisecond(this.calendar);
        int periods = (int) (ww / length) + 1;
        
        RegularTimePeriod p = this.labelInfo[band].createInstance(
                new Date(axisMin), this.timeZone);
        Rectangle2D b = null;
        long lastXX = 0L;
        float y = (float) (state.getCursor());
        TextAnchor anchor = TextAnchor.TOP_CENTER;
        float yDelta = (float) b1.getHeight();
        if (edge == RectangleEdge.TOP) {
            anchor = TextAnchor.BOTTOM_CENTER;
            yDelta = -yDelta;
        }
        while (p.getFirstMillisecond(this.calendar) <= axisMax) {
            float x = (float) valueToJava2D(p.getMiddleMillisecond(
                    this.calendar), dataArea, edge);
            DateFormat df = this.labelInfo[band].getDateFormat();
            String label = df.format(new Date(p.getMiddleMillisecond(
                    this.calendar)));
            long first = p.getFirstMillisecond(this.calendar);
            long last = p.getLastMillisecond(this.calendar);
            if (last > axisMax) {
                // this is the last period, but it is only partially visible 
                // so check that the label will fit before displaying it...
                Rectangle2D bb = TextUtilities.getTextBounds(label, g2, 
                        g2.getFontMetrics());
                if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {
                    float xstart = (float) valueToJava2D(Math.max(first, 
                            axisMin), dataArea, edge);
                    if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {
                        x = ((float) dataArea.getMaxX() + xstart) / 2.0f;   
                    }
                    else {
                        label = null;
                    }
                }
            }
            if (first < axisMin) {
                // this is the first period, but it is only partially visible 
                // so check that the label will fit before displaying it...
                Rectangle2D bb = TextUtilities.getTextBounds(label, g2, 
                        g2.getFontMetrics());
                if ((x - bb.getWidth() / 2) < dataArea.getX()) {
                    float xlast = (float) valueToJava2D(Math.min(last, 
                            axisMax), dataArea, edge);
                    if (bb.getWidth() < (xlast - dataArea.getX())) {
                        x = (xlast + (float) dataArea.getX()) / 2.0f;   
                    }
                    else {
                        label = null;
                    }
                }
                
            }
            if (label != null) {
                g2.setPaint(this.labelInfo[band].getLabelPaint());
                b = TextUtilities.drawAlignedString(label, g2, x, y, anchor);
            }
            if (lastXX > 0L) {
                if (this.labelInfo[band].getDrawDividers()) {
                    long nextXX = p.getFirstMillisecond(this.calendar);
                    long mid = (lastXX + nextXX) / 2;
                    float mid2d = (float) valueToJava2D(mid, dataArea, edge);
                    g2.setStroke(this.labelInfo[band].getDividerStroke());
                    g2.setPaint(this.labelInfo[band].getDividerPaint());
                    g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta));
                }
            }
            lastXX = last;
            for (int i = 0; i < periods; i++) {
                p = p.next();   
            }
        }
        double used = 0.0;
        if (b != null) {
            used = b.getHeight();
            // work out the trailing gap
            if (edge == RectangleEdge.BOTTOM) {
                used += this.labelInfo[band].getPadding().calculateBottomOutset(
                        fm.getHeight());   
            }
            else if (edge == RectangleEdge.TOP) {
                used += this.labelInfo[band].getPadding().calculateTopOutset(
                        fm.getHeight());   
            }
        }
        state.moveCursor(used, edge);        
        return state;    
    }

    /**
     * Calculates the positions of the ticks for the axis, storing the results
     * in the tick list (ready for drawing).
     *
     * @param g2  the graphics device.
     * @param state  the axis state.
     * @param dataArea  the area inside the axes.
     * @param edge  the edge on which the axis is located.
     * 
     * @return The list of ticks.
     */
    public List refreshTicks(Graphics2D g2, 
                             AxisState state,
                             Rectangle2D dataArea,
                             RectangleEdge edge) {
        return Collections.EMPTY_LIST;
    }
    
    /**
     * Converts a data value to a coordinate in Java2D space, assuming that the
     * axis runs along one edge of the specified dataArea.
     * <p>
     * Note that it is possible for the coordinate to fall outside the area.
     *
     * @param value  the data value.
     * @param area  the area for plotting the data.
     * @param edge  the edge along which the axis lies.
     *
     * @return The Java2D coordinate.
     */
    public double valueToJava2D(double value,
                                Rectangle2D area,
                                RectangleEdge edge) {
        
        double result = Double.NaN;
        double axisMin = this.first.getFirstMillisecond(this.calendar);
        double axisMax = this.last.getLastMillisecond(this.calendar);
        if (RectangleEdge.isTopOrBottom(edge)) {
            double minX = area.getX();
            double maxX = area.getMaxX();
            if (isInverted()) {
                result = maxX + ((value - axisMin) / (axisMax - axisMin)) 
                         * (minX - maxX);
            }
            else {
                result = minX + ((value - axisMin) / (axisMax - axisMin)) 
                         * (maxX - minX);
            }
        }
        else if (RectangleEdge.isLeftOrRight(edge)) {
            double minY = area.getMinY();
            double maxY = area.getMaxY();
            if (isInverted()) {
                result = minY + (((value - axisMin) / (axisMax - axisMin)) 
                         * (maxY - minY));
            }
            else {
                result = maxY - (((value - axisMin) / (axisMax - axisMin)) 
                         * (maxY - minY));
            }
        }
        return result;
        
    }

    /**
     * Converts a coordinate in Java2D space to the corresponding data value,
     * assuming that the axis runs along one edge of the specified dataArea.
     *
     * @param java2DValue  the coordinate in Java2D space.
     * @param area  the area in which the data is plotted.
     * @param edge  the edge along which the axis lies.
     *
     * @return The data value.
     */
    public double java2DToValue(double java2DValue,
                                Rectangle2D area,
                                RectangleEdge edge) {

        double result = Double.NaN;
        double min = 0.0;
        double max = 0.0;
        double axisMin = this.first.getFirstMillisecond(this.calendar);
        double axisMax = this.last.getLastMillisecond(this.calendar);
        if (RectangleEdge.isTopOrBottom(edge)) {
            min = area.getX();
            max = area.getMaxX();
        }
        else if (RectangleEdge.isLeftOrRight(edge)) {
            min = area.getMaxY();
            max = area.getY();
        }
        if (isInverted()) {
             result = axisMax - ((java2DValue - min) / (max - min) 
                      * (axisMax - axisMin));
        }
        else {
             result = axisMin + ((java2DValue - min) / (max - min) 
                      * (axisMax - axisMin));
        }
        return result;
    }

    /**
     * Rescales the axis to ensure that all data is visible.
     */
    protected void autoAdjustRange() {

        Plot plot = getPlot();
        if (plot == null) {
            return;  // no plot, no data
        }

        if (plot instanceof ValueAxisPlot) {
            ValueAxisPlot vap = (ValueAxisPlot) plot;

            Range r = vap.getDataRange(this);
            if (r == null) {
                r = getDefaultAutoRange();
            }
            
            long upper = Math.round(r.getUpperBound());
            long lower = Math.round(r.getLowerBound());
            this.first = createInstance(this.autoRangeTimePeriodClass, 
                    new Date(lower), this.timeZone);
            this.last = createInstance(this.autoRangeTimePeriodClass, 
                    new Date(upper), this.timeZone);
            setRange(r, false, false);
        }

    }
    
    /**
     * Tests the axis for equality with an arbitrary object.
     * 
     * @param obj  the object (<code>null</code> permitted).
     * 
     * @return A boolean.
     */
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;   
        }
        if (obj instanceof PeriodAxis && super.equals(obj)) {
            PeriodAxis that = (PeriodAxis) obj;
            if (!this.first.equals(that.first)) {
                return false;   
            }
            if (!this.last.equals(that.last)) {
                return false;   
            }
            if (!this.timeZone.equals(that.timeZone)) {
                return false;   
            }
            if (!this.autoRangeTimePeriodClass.equals(
                    that.autoRangeTimePeriodClass)) {
                return false;   
            }
            if (!(isMinorTickMarksVisible() 
                    == that.isMinorTickMarksVisible())) {
                return false;
            }
            if (!this.majorTickTimePeriodClass.equals(
                    that.majorTickTimePeriodClass)) {
                return false;
            }
            if (!this.minorTickTimePeriodClass.equals(
                    that.minorTickTimePeriodClass)) {
                return false;
            }
            if (!this.minorTickMarkPaint.equals(that.minorTickMarkPaint)) {
                return false;
            }
            if (!this.minorTickMarkStroke.equals(that.minorTickMarkStroke)) {
                return false;
            }
            if (!Arrays.equals(this.labelInfo, that.labelInfo)) {
                return false;   
            }
            return true;   
        }
        return false;
    }

    /**
     * Returns a hash code for this object.
     * 
     * @return A hash code.
     */
    public int hashCode() {
        if (getLabel() != null) {
            return getLabel().hashCode();
        }
        else {
            return 0;
        }
    }
    
    /**
     * Returns a clone of the axis.
     * 
     * @return A clone.
     * 
     * @throws CloneNotSupportedException  this class is cloneable, but 
     *         subclasses may not be.
     */
    public Object clone() throws CloneNotSupportedException {
        PeriodAxis clone = (PeriodAxis) super.clone();
        clone.timeZone = (TimeZone) this.timeZone.clone();
        clone.labelInfo = new PeriodAxisLabelInfo[this.labelInfo.length];
        for (int i = 0; i < this.labelInfo.length; i++) {
            clone.labelInfo[i] = this.labelInfo[i];  // copy across references 
                                                     // to immutable objs 
        }
        return clone;
    }
    
    /**
     * A utility method used to create a particular subclass of the 
     * {@link RegularTimePeriod} class that includes the specified millisecond, 
     * assuming the specified time zone.
     * 
     * @param periodClass  the class.
     * @param millisecond  the time.
     * @param zone  the time zone.
     * 
     * @return The time period.
     */
    private RegularTimePeriod createInstance(Class periodClass, 
                                             Date millisecond, TimeZone zone) {
        RegularTimePeriod result = null;
        try {
            Constructor c = periodClass.getDeclaredConstructor(new Class[] {
                    Date.class, TimeZone.class});
            result = (RegularTimePeriod) c.newInstance(new Object[] {
                    millisecond, zone});   
        }
        catch (Exception e) {
            // do nothing            
        }
        return result;
    }
    
    /**
     * Provides serialization support.
     *
     * @param stream  the output stream.
     *
     * @throws IOException  if there is an I/O error.
     */
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        SerialUtilities.writeStroke(this.minorTickMarkStroke, stream);
        SerialUtilities.writePaint(this.minorTickMarkPaint, stream);
    }

    /**
     * Provides serialization support.
     *
     * @param stream  the input stream.
     *
     * @throws IOException  if there is an I/O error.
     * @throws ClassNotFoundException  if there is a classpath problem.
     */
    private void readObject(ObjectInputStream stream) 
        throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        this.minorTickMarkStroke = SerialUtilities.readStroke(stream);
        this.minorTickMarkPaint = SerialUtilities.readPaint(stream);
    }

}
