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 * DateAxis.java
029 * -------------
030 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Jonathan Nash;
034 *                   David Li;
035 *                   Michael Rauch;
036 *                   Bill Kelemen;
037 *                   Pawel Pabis;
038 *                   Chris Boek;
039 *
040 * Changes (from 23-Jun-2001)
041 * --------------------------
042 * 23-Jun-2001 : Modified to work with null data source (DG);
043 * 18-Sep-2001 : Updated header (DG);
044 * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc 
045 *               comments (DG);
046 * 16-Jan-2002 : Added an optional crosshair, based on the implementation by 
047 *               Jonathan Nash (DG);
048 * 26-Feb-2002 : Updated import statements (DG);
049 * 22-Apr-2002 : Added a setRange() method (DG);
050 * 25-Jun-2002 : Removed redundant local variable (DG);
051 * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
052 * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit 
053 *               selection (fix for bug id 528885) (DG);
054 * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis 
055 *               class (DG);
056 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
057 * 25-Sep-2002 : Added new setRange() methods, and deprecated 
058 *               setAxisRange() (DG);
059 * 04-Oct-2002 : Changed auto tick selection to parallel number axis 
060 *               classes (DG);
061 * 24-Oct-2002 : Added a date format override (DG);
062 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
063 * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
064 *               crosshair settings to the plot (DG);
065 * 15-Jan-2003 : Removed anchor date (DG);
066 * 20-Jan-2003 : Removed unnecessary constructors (DG);
067 * 26-Mar-2003 : Implemented Serializable (DG);
068 * 02-May-2003 : Added additional units to createStandardDateTickUnits() 
069 *               method, as suggested by mhilpert in bug report 723187 (DG);
070 * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
071 * 24-May-2003 : Added support for underlying timeline for 
072 *               SegmentedTimeline (BK);
073 * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
074 * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
075 * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
076 * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
077 * 02-Sep-2003 : Fixes for bug report 790506 (DG);
078 * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
079 * 10-Sep-2003 : Fixes for segmented timeline (DG);
080 * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
081 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
082 * 07-Nov-2003 : Modified to use new tick classes (DG);
083 * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit 
084 *               when a calculated tick value is hidden (which can occur in 
085 *               segmented date axes) (DG);
086 * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and 
087 *               fixed bug 846277 (labels missing for inverted axis) (DG);
088 * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit 
089 *               (ex. 1st of month) was hidden, causing infinite loop (BK);
090 * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard 
091 *               Wardle) (DG);
092 * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and 
093 *               translateValueToJava2D --> valueToJava2D (DG); 
094 * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical 
095 *               axis (DG);
096 * 16-Mar-2004 : Added plotState to draw() method (DG);
097 * 07-Apr-2004 : Changed string width calculation (DG);
098 * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id 
099 *               939148) (DG);
100 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0 
101 *               release (DG);
102 * 13-Jan-2005 : Fixed bug (see 
103 *               http://www.jfree.org/forum/viewtopic.php?t=11330) (DG);
104 * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant 
105 *               argument from selectAutoTickUnit() (DG);
106 * ------------- JFREECHART 1.0.x ---------------------------------------------
107 * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG);
108 * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG);
109 * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG);
110 * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG);
111 * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in 
112 *               previousStandardDate() (DG);
113 * 04-Apr-2007 : Use time zone in date calculations (CB);
114 * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG);
115 * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit
116 *               tests (DG);
117 * 
118 */
119
120package org.jfree.chart.axis;
121
122import java.awt.Font;
123import java.awt.FontMetrics;
124import java.awt.Graphics2D;
125import java.awt.font.FontRenderContext;
126import java.awt.font.LineMetrics;
127import java.awt.geom.Rectangle2D;
128import java.io.Serializable;
129import java.text.DateFormat;
130import java.text.SimpleDateFormat;
131import java.util.Calendar;
132import java.util.Date;
133import java.util.List;
134import java.util.TimeZone;
135
136import org.jfree.chart.event.AxisChangeEvent;
137import org.jfree.chart.plot.Plot;
138import org.jfree.chart.plot.PlotRenderingInfo;
139import org.jfree.chart.plot.ValueAxisPlot;
140import org.jfree.data.Range;
141import org.jfree.data.time.DateRange;
142import org.jfree.data.time.Month;
143import org.jfree.data.time.RegularTimePeriod;
144import org.jfree.data.time.Year;
145import org.jfree.ui.RectangleEdge;
146import org.jfree.ui.RectangleInsets;
147import org.jfree.ui.TextAnchor;
148import org.jfree.util.ObjectUtilities;
149
150/**
151 * The base class for axes that display dates.  You will find it easier to 
152 * understand how this axis works if you bear in mind that it really 
153 * displays/measures integer (or long) data, where the integers are 
154 * milliseconds since midnight, 1-Jan-1970.  When displaying tick labels, the 
155 * millisecond values are converted back to dates using a 
156 * <code>DateFormat</code> instance.
157 * <P>
158 * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in 
159 * the constructor to create an axis that only contains certain domain values. 
160 * For example, this allows you to create a date axis that only contains 
161 * working days.
162 */
163public class DateAxis extends ValueAxis implements Cloneable, Serializable {
164
165    /** For serialization. */
166    private static final long serialVersionUID = -1013460999649007604L;
167    
168    /** The default axis range. */
169    public static final DateRange DEFAULT_DATE_RANGE = new DateRange();
170
171    /** The default minimum auto range size. */
172    public static final double 
173            DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0;
174
175    /** The default date tick unit. */
176    public static final DateTickUnit DEFAULT_DATE_TICK_UNIT
177            = new DateTickUnit(DateTickUnit.DAY, 1, new SimpleDateFormat());
178
179    /** The default anchor date. */
180    public static final Date DEFAULT_ANCHOR_DATE = new Date();
181
182    /** The current tick unit. */
183    private DateTickUnit tickUnit;
184
185    /** The override date format. */
186    private DateFormat dateFormatOverride;
187
188    /** 
189     * Tick marks can be displayed at the start or the middle of the time 
190     * period. 
191     */
192    private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START;
193
194    /**
195     * A timeline that includes all milliseconds (as defined by 
196     * <code>java.util.Date</code>) in the real time line.
197     */
198    private static class DefaultTimeline implements Timeline, Serializable {
199
200        /**
201         * Converts a millisecond into a timeline value.
202         *
203         * @param millisecond  the millisecond.
204         *
205         * @return The timeline value.
206         */
207        public long toTimelineValue(long millisecond) {
208            return millisecond;
209        }
210
211        /**
212         * Converts a date into a timeline value.
213         *
214         * @param date  the domain value.
215         *
216         * @return The timeline value.
217         */
218        public long toTimelineValue(Date date) {
219            return date.getTime();
220        }
221
222        /**
223         * Converts a timeline value into a millisecond (as encoded by 
224         * <code>java.util.Date</code>).
225         *
226         * @param value  the value.
227         *
228         * @return The millisecond.
229         */
230        public long toMillisecond(long value) {
231            return value;
232        }
233
234        /**
235         * Returns <code>true</code> if the timeline includes the specified 
236         * domain value.
237         *
238         * @param millisecond  the millisecond.
239         *
240         * @return <code>true</code>.
241         */
242        public boolean containsDomainValue(long millisecond) {
243            return true;
244        }
245
246        /**
247         * Returns <code>true</code> if the timeline includes the specified 
248         * domain value.
249         *
250         * @param date  the date.
251         *
252         * @return <code>true</code>.
253         */
254        public boolean containsDomainValue(Date date) {
255            return true;
256        }
257
258        /**
259         * Returns <code>true</code> if the timeline includes the specified 
260         * domain value range.
261         *
262         * @param from  the start value.
263         * @param to  the end value.
264         *
265         * @return <code>true</code>.
266         */
267        public boolean containsDomainRange(long from, long to) {
268            return true;
269        }
270
271        /**
272         * Returns <code>true</code> if the timeline includes the specified 
273         * domain value range.
274         *
275         * @param from  the start date.
276         * @param to  the end date.
277         *
278         * @return <code>true</code>.
279         */
280        public boolean containsDomainRange(Date from, Date to) {
281            return true;
282        }
283
284        /**
285         * Tests an object for equality with this instance.
286         *
287         * @param object  the object.
288         *
289         * @return A boolean.
290         */
291        public boolean equals(Object object) {
292            if (object == null) {
293                return false;
294            }
295            if (object == this) {
296                return true;
297            }
298            if (object instanceof DefaultTimeline) {
299                return true;
300            }
301            return false;
302        }
303    }
304
305    /** A static default timeline shared by all standard DateAxis */
306    private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
307
308    /** The time zone for the axis. */
309    private TimeZone timeZone;
310    
311    /** Our underlying timeline. */
312    private Timeline timeline;
313
314    /**
315     * Creates a date axis with no label.
316     */
317    public DateAxis() {
318        this(null);
319    }
320
321    /**
322     * Creates a date axis with the specified label.
323     *
324     * @param label  the axis label (<code>null</code> permitted).
325     */
326    public DateAxis(String label) {
327        this(label, TimeZone.getDefault());
328    }
329
330    /**
331     * Creates a date axis. A timeline is specified for the axis. This allows 
332     * special transformations to occur between a domain of values and the 
333     * values included in the axis.
334     *
335     * @see org.jfree.chart.axis.SegmentedTimeline
336     *
337     * @param label  the axis label (<code>null</code> permitted).
338     * @param zone  the time zone.
339     */
340    public DateAxis(String label, TimeZone zone) {
341        super(label, DateAxis.createStandardDateTickUnits(zone));
342        setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
343        setAutoRangeMinimumSize(
344                DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
345        setRange(DEFAULT_DATE_RANGE, false, false);
346        this.dateFormatOverride = null;
347        this.timeZone = zone;
348        this.timeline = DEFAULT_TIMELINE;
349    }
350
351    /**
352     * Returns the time zone for the axis.
353     * 
354     * @return The time zone.
355     * 
356     * @since 1.0.4
357     * @see #setTimeZone(TimeZone)
358     */
359    public TimeZone getTimeZone() {
360        return this.timeZone;
361    }
362    
363    /**
364     * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to
365     * all registered listeners.
366     * 
367     * @param zone  the time zone (<code>null</code> not permitted).
368     * 
369     * @since 1.0.4
370     * @see #getTimeZone()
371     */
372    public void setTimeZone(TimeZone zone) {
373        if (!this.timeZone.equals(zone)) {
374            this.timeZone = zone;
375            setStandardTickUnits(createStandardDateTickUnits(zone));
376            notifyListeners(new AxisChangeEvent(this));
377        }
378    } 
379    
380    /**
381     * Returns the underlying timeline used by this axis.
382     *
383     * @return The timeline.
384     */
385    public Timeline getTimeline() {
386        return this.timeline;
387    }
388
389    /**
390     * Sets the underlying timeline to use for this axis.
391     * <P>
392     * If the timeline is changed, an {@link AxisChangeEvent} is sent to all
393     * registered listeners.
394     *
395     * @param timeline  the timeline.
396     */
397    public void setTimeline(Timeline timeline) {
398        if (this.timeline != timeline) {
399            this.timeline = timeline;
400            notifyListeners(new AxisChangeEvent(this));
401        }
402    }
403
404    /**
405     * Returns the tick unit for the axis.
406     * <p>
407     * Note: if the <code>autoTickUnitSelection</code> flag is 
408     * <code>true</code> the tick unit may be changed while the axis is being 
409     * drawn, so in that case the return value from this method may be
410     * irrelevant if the method is called before the axis has been drawn.
411     *
412     * @return The tick unit (possibly <code>null</code>).
413     * 
414     * @see #setTickUnit(DateTickUnit)
415     * @see ValueAxis#isAutoTickUnitSelection()
416     */
417    public DateTickUnit getTickUnit() {
418        return this.tickUnit;
419    }
420
421    /**
422     * Sets the tick unit for the axis.  The auto-tick-unit-selection flag is 
423     * set to <code>false</code>, and registered listeners are notified that 
424     * the axis has been changed.
425     *
426     * @param unit  the tick unit.
427     * 
428     * @see #getTickUnit()
429     * @see #setTickUnit(DateTickUnit, boolean, boolean)
430     */
431    public void setTickUnit(DateTickUnit unit) {
432        setTickUnit(unit, true, true);
433    }
434
435    /**
436     * Sets the tick unit attribute.
437     *
438     * @param unit  the new tick unit.
439     * @param notify  notify registered listeners?
440     * @param turnOffAutoSelection  turn off auto selection?
441     * 
442     * @see #getTickUnit()
443     */
444    public void setTickUnit(DateTickUnit unit, boolean notify, 
445                            boolean turnOffAutoSelection) {
446
447        this.tickUnit = unit;
448        if (turnOffAutoSelection) {
449            setAutoTickUnitSelection(false, false);
450        }
451        if (notify) {
452            notifyListeners(new AxisChangeEvent(this));
453        }
454
455    }
456
457    /**
458     * Returns the date format override.  If this is non-null, then it will be
459     * used to format the dates on the axis.
460     *
461     * @return The formatter (possibly <code>null</code>).
462     */
463    public DateFormat getDateFormatOverride() {
464        return this.dateFormatOverride;
465    }
466
467    /**
468     * Sets the date format override.  If this is non-null, then it will be 
469     * used to format the dates on the axis.
470     *
471     * @param formatter  the date formatter (<code>null</code> permitted).
472     */
473    public void setDateFormatOverride(DateFormat formatter) {
474        this.dateFormatOverride = formatter;
475        notifyListeners(new AxisChangeEvent(this));
476    }
477
478    /**
479     * Sets the upper and lower bounds for the axis and sends an 
480     * {@link AxisChangeEvent} to all registered listeners.  As a side-effect, 
481     * the auto-range flag is set to false.
482     *
483     * @param range  the new range (<code>null</code> not permitted).
484     */
485    public void setRange(Range range) {
486        setRange(range, true, true);
487    }
488
489    /**
490     * Sets the range for the axis, if requested, sends an 
491     * {@link AxisChangeEvent} to all registered listeners.  As a side-effect, 
492     * the auto-range flag is set to <code>false</code> (optional).
493     *
494     * @param range  the range (<code>null</code> not permitted).
495     * @param turnOffAutoRange  a flag that controls whether or not the auto 
496     *                          range is turned off.
497     * @param notify  a flag that controls whether or not listeners are 
498     *                notified.
499     */
500    public void setRange(Range range, boolean turnOffAutoRange, 
501                         boolean notify) {
502        if (range == null) {
503            throw new IllegalArgumentException("Null 'range' argument.");
504        }
505        // usually the range will be a DateRange, but if it isn't do a 
506        // conversion...
507        if (!(range instanceof DateRange)) {
508            range = new DateRange(range);
509        }
510        super.setRange(range, turnOffAutoRange, notify);
511    }
512
513    /**
514     * Sets the axis range and sends an {@link AxisChangeEvent} to all 
515     * registered listeners.
516     *
517     * @param lower  the lower bound for the axis.
518     * @param upper  the upper bound for the axis.
519     */
520    public void setRange(Date lower, Date upper) {
521        if (lower.getTime() >= upper.getTime()) {
522            throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
523        }
524        setRange(new DateRange(lower, upper));
525    }
526
527    /**
528     * Sets the axis range and sends an {@link AxisChangeEvent} to all 
529     * registered listeners.
530     *
531     * @param lower  the lower bound for the axis.
532     * @param upper  the upper bound for the axis.
533     */
534    public void setRange(double lower, double upper) {
535        if (lower >= upper) {
536            throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
537        }
538        setRange(new DateRange(lower, upper));
539    }
540
541    /**
542     * Returns the earliest date visible on the axis.
543     *
544     * @return The date.
545     * 
546     * @see #setMinimumDate(Date)
547     * @see #getMaximumDate()
548     */
549    public Date getMinimumDate() {
550        Date result = null;
551        Range range = getRange();
552        if (range instanceof DateRange) {
553            DateRange r = (DateRange) range;
554            result = r.getLowerDate();
555        }
556        else {
557            result = new Date((long) range.getLowerBound());
558        }
559        return result;
560    }
561
562    /**
563     * Sets the minimum date visible on the axis and sends an 
564     * {@link AxisChangeEvent} to all registered listeners.  If 
565     * <code>date</code> is on or after the current maximum date for 
566     * the axis, the maximum date will be shifted to preserve the current
567     * length of the axis.
568     *
569     * @param date  the date (<code>null</code> not permitted).
570     * 
571     * @see #getMinimumDate()
572     * @see #setMaximumDate(Date)
573     */
574    public void setMinimumDate(Date date) {
575        if (date == null) {
576            throw new IllegalArgumentException("Null 'date' argument.");
577        }
578        // check the new minimum date relative to the current maximum date
579        Date maxDate = getMaximumDate();
580        long maxMillis = maxDate.getTime();
581        long newMinMillis = date.getTime();
582        if (maxMillis <= newMinMillis) {
583            Date oldMin = getMinimumDate();
584            long length = maxMillis - oldMin.getTime();
585            maxDate = new Date(newMinMillis + length);
586        }
587        setRange(new DateRange(date, maxDate), true, false);
588        notifyListeners(new AxisChangeEvent(this));
589    }
590
591    /**
592     * Returns the latest date visible on the axis.
593     *
594     * @return The date.
595     * 
596     * @see #setMaximumDate(Date)
597     * @see #getMinimumDate()
598     */
599    public Date getMaximumDate() {
600        Date result = null;
601        Range range = getRange();
602        if (range instanceof DateRange) {
603            DateRange r = (DateRange) range;
604            result = r.getUpperDate();
605        }
606        else {
607            result = new Date((long) range.getUpperBound());
608        }
609        return result;
610    }
611
612    /**
613     * Sets the maximum date visible on the axis and sends an 
614     * {@link AxisChangeEvent} to all registered listeners.  If 
615     * <code>maximumDate</code> is on or before the current minimum date for 
616     * the axis, the minimum date will be shifted to preserve the current
617     * length of the axis.
618     *
619     * @param maximumDate  the date (<code>null</code> not permitted).
620     * 
621     * @see #getMinimumDate()
622     * @see #setMinimumDate(Date)
623     */
624    public void setMaximumDate(Date maximumDate) {
625        if (maximumDate == null) {
626            throw new IllegalArgumentException("Null 'maximumDate' argument.");
627        }
628        // check the new maximum date relative to the current minimum date
629        Date minDate = getMinimumDate();
630        long minMillis = minDate.getTime();
631        long newMaxMillis = maximumDate.getTime();
632        if (minMillis >= newMaxMillis) {
633            Date oldMax = getMaximumDate();
634            long length = oldMax.getTime() - minMillis;
635            minDate = new Date(newMaxMillis - length);
636        }
637        setRange(new DateRange(minDate, maximumDate), true, false);
638        notifyListeners(new AxisChangeEvent(this));
639    }
640
641    /**
642     * Returns the tick mark position (start, middle or end of the time period).
643     *
644     * @return The position (never <code>null</code>).
645     */
646    public DateTickMarkPosition getTickMarkPosition() {
647        return this.tickMarkPosition;
648    }
649
650    /**
651     * Sets the tick mark position (start, middle or end of the time period) 
652     * and sends an {@link AxisChangeEvent} to all registered listeners.
653     *
654     * @param position  the position (<code>null</code> not permitted).
655     */
656    public void setTickMarkPosition(DateTickMarkPosition position) {
657        if (position == null) {
658            throw new IllegalArgumentException("Null 'position' argument.");
659        }
660        this.tickMarkPosition = position;
661        notifyListeners(new AxisChangeEvent(this));
662    }
663
664    /**
665     * Configures the axis to work with the specified plot.  If the axis has
666     * auto-scaling, then sets the maximum and minimum values.
667     */
668    public void configure() {
669        if (isAutoRange()) {
670            autoAdjustRange();
671        }
672    }
673
674    /**
675     * Returns <code>true</code> if the axis hides this value, and 
676     * <code>false</code> otherwise.
677     *
678     * @param millis  the data value.
679     *
680     * @return A value.
681     */
682    public boolean isHiddenValue(long millis) {
683        return (!this.timeline.containsDomainValue(new Date(millis)));
684    }
685
686    /**
687     * Translates the data value to the display coordinates (Java 2D User Space)
688     * of the chart.
689     *
690     * @param value  the date to be plotted.
691     * @param area  the rectangle (in Java2D space) where the data is to be 
692     *              plotted.
693     * @param edge  the axis location.
694     *
695     * @return The coordinate corresponding to the supplied data value.
696     */
697    public double valueToJava2D(double value, Rectangle2D area, 
698                                RectangleEdge edge) {
699        
700        value = this.timeline.toTimelineValue((long) value);
701
702        DateRange range = (DateRange) getRange();
703        double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
704        double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
705        double result = 0.0;
706        if (RectangleEdge.isTopOrBottom(edge)) {
707            double minX = area.getX();
708            double maxX = area.getMaxX();
709            if (isInverted()) {
710                result = maxX + ((value - axisMin) / (axisMax - axisMin)) 
711                         * (minX - maxX);
712            }
713            else {
714                result = minX + ((value - axisMin) / (axisMax - axisMin)) 
715                         * (maxX - minX);
716            }
717        }
718        else if (RectangleEdge.isLeftOrRight(edge)) {
719            double minY = area.getMinY();
720            double maxY = area.getMaxY();
721            if (isInverted()) {
722                result = minY + (((value - axisMin) / (axisMax - axisMin)) 
723                         * (maxY - minY));
724            }
725            else {
726                result = maxY - (((value - axisMin) / (axisMax - axisMin)) 
727                         * (maxY - minY));
728            }
729        }
730        return result;
731
732    }
733
734    /**
735     * Translates a date to Java2D coordinates, based on the range displayed by
736     * this axis for the specified data area.
737     *
738     * @param date  the date.
739     * @param area  the rectangle (in Java2D space) where the data is to be
740     *              plotted.
741     * @param edge  the axis location.
742     *
743     * @return The coordinate corresponding to the supplied date.
744     */
745    public double dateToJava2D(Date date, Rectangle2D area, 
746                               RectangleEdge edge) {  
747        double value = date.getTime();
748        return valueToJava2D(value, area, edge);
749    }
750
751    /**
752     * Translates a Java2D coordinate into the corresponding data value.  To 
753     * perform this translation, you need to know the area used for plotting 
754     * data, and which edge the axis is located on.
755     *
756     * @param java2DValue  the coordinate in Java2D space.
757     * @param area  the rectangle (in Java2D space) where the data is to be 
758     *              plotted.
759     * @param edge  the axis location.
760     *
761     * @return A data value.
762     */
763    public double java2DToValue(double java2DValue, Rectangle2D area, 
764                                RectangleEdge edge) {
765        
766        DateRange range = (DateRange) getRange();
767        double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
768        double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
769
770        double min = 0.0;
771        double max = 0.0;
772        if (RectangleEdge.isTopOrBottom(edge)) {
773            min = area.getX();
774            max = area.getMaxX();
775        }
776        else if (RectangleEdge.isLeftOrRight(edge)) {
777            min = area.getMaxY();
778            max = area.getY();
779        }
780
781        double result;
782        if (isInverted()) {
783             result = axisMax - ((java2DValue - min) / (max - min) 
784                      * (axisMax - axisMin));
785        }
786        else {
787             result = axisMin + ((java2DValue - min) / (max - min) 
788                      * (axisMax - axisMin));
789        }
790
791        return this.timeline.toMillisecond((long) result); 
792    }
793
794    /**
795     * Calculates the value of the lowest visible tick on the axis.
796     *
797     * @param unit  date unit to use.
798     *
799     * @return The value of the lowest visible tick on the axis.
800     */
801    public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
802        return nextStandardDate(getMinimumDate(), unit);
803    }
804
805    /**
806     * Calculates the value of the highest visible tick on the axis.
807     *
808     * @param unit  date unit to use.
809     *
810     * @return The value of the highest visible tick on the axis.
811     */
812    public Date calculateHighestVisibleTickValue(DateTickUnit unit) {
813        return previousStandardDate(getMaximumDate(), unit);
814    }
815    
816    /**
817     * Returns the previous "standard" date, for a given date and tick unit.
818     *
819     * @param date  the reference date.
820     * @param unit  the tick unit.
821     *
822     * @return The previous "standard" date.
823     */
824    protected Date previousStandardDate(Date date, DateTickUnit unit) {
825
826        int milliseconds;
827        int seconds;
828        int minutes;
829        int hours;
830        int days;
831        int months;
832        int years;
833
834        Calendar calendar = Calendar.getInstance(this.timeZone);
835        calendar.setTime(date);
836        int count = unit.getCount();
837        int current = calendar.get(unit.getCalendarField());
838        int value = count * (current / count);
839
840        switch (unit.getUnit()) {
841
842            case (DateTickUnit.MILLISECOND) :
843                years = calendar.get(Calendar.YEAR);
844                months = calendar.get(Calendar.MONTH);
845                days = calendar.get(Calendar.DATE);
846                hours = calendar.get(Calendar.HOUR_OF_DAY);
847                minutes = calendar.get(Calendar.MINUTE);
848                seconds = calendar.get(Calendar.SECOND);
849                calendar.set(years, months, days, hours, minutes, seconds);
850                calendar.set(Calendar.MILLISECOND, value);
851                Date mm = calendar.getTime();
852                if (mm.getTime() >= date.getTime()) {
853                    calendar.set(Calendar.MILLISECOND, value - 1);
854                    mm = calendar.getTime();
855                }
856                return calendar.getTime();
857
858            case (DateTickUnit.SECOND) :
859                years = calendar.get(Calendar.YEAR);
860                months = calendar.get(Calendar.MONTH);
861                days = calendar.get(Calendar.DATE);
862                hours = calendar.get(Calendar.HOUR_OF_DAY);
863                minutes = calendar.get(Calendar.MINUTE);
864                if (this.tickMarkPosition == DateTickMarkPosition.START) {
865                    milliseconds = 0;
866                }
867                else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
868                    milliseconds = 500;
869                }
870                else {
871                    milliseconds = 999;
872                }
873                calendar.set(Calendar.MILLISECOND, milliseconds);
874                calendar.set(years, months, days, hours, minutes, value);
875                Date dd = calendar.getTime();
876                if (dd.getTime() >= date.getTime()) {
877                    calendar.set(Calendar.SECOND, value - 1);
878                    dd = calendar.getTime();
879                }
880                return calendar.getTime();
881
882            case (DateTickUnit.MINUTE) :
883                years = calendar.get(Calendar.YEAR);
884                months = calendar.get(Calendar.MONTH);
885                days = calendar.get(Calendar.DATE);
886                hours = calendar.get(Calendar.HOUR_OF_DAY);
887                if (this.tickMarkPosition == DateTickMarkPosition.START) {
888                    seconds = 0;
889                }
890                else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
891                    seconds = 30;
892                }
893                else {
894                    seconds = 59;
895                }
896                calendar.clear(Calendar.MILLISECOND);
897                calendar.set(years, months, days, hours, value, seconds);
898                Date d0 = calendar.getTime();
899                if (d0.getTime() >= date.getTime()) {
900                    calendar.set(Calendar.MINUTE, value - 1);
901                    d0 = calendar.getTime();
902                }
903                return d0;
904
905            case (DateTickUnit.HOUR) :
906                years = calendar.get(Calendar.YEAR);
907                months = calendar.get(Calendar.MONTH);
908                days = calendar.get(Calendar.DATE);
909                if (this.tickMarkPosition == DateTickMarkPosition.START) {
910                    minutes = 0;
911                    seconds = 0;
912                }
913                else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
914                    minutes = 30;
915                    seconds = 0;
916                }
917                else {
918                    minutes = 59;
919                    seconds = 59;
920                }
921                calendar.clear(Calendar.MILLISECOND);
922                calendar.set(years, months, days, value, minutes, seconds);
923                Date d1 = calendar.getTime();
924                if (d1.getTime() >= date.getTime()) {
925                    calendar.set(Calendar.HOUR_OF_DAY, value - 1);
926                    d1 = calendar.getTime();
927                }
928                return d1;
929
930            case (DateTickUnit.DAY) :
931                years = calendar.get(Calendar.YEAR);
932                months = calendar.get(Calendar.MONTH);
933                if (this.tickMarkPosition == DateTickMarkPosition.START) {
934                    hours = 0;
935                    minutes = 0;
936                    seconds = 0;
937                }
938                else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
939                    hours = 12;
940                    minutes = 0;
941                    seconds = 0;
942                }
943                else {
944                    hours = 23;
945                    minutes = 59;
946                    seconds = 59;
947                }
948                calendar.clear(Calendar.MILLISECOND);
949                calendar.set(years, months, value, hours, 0, 0);
950                // long result = calendar.getTimeInMillis();  
951                    // won't work with JDK 1.3
952                Date d2 = calendar.getTime();
953                if (d2.getTime() >= date.getTime()) {
954                    calendar.set(Calendar.DATE, value - 1);
955                    d2 = calendar.getTime();
956                }
957                return d2;
958
959            case (DateTickUnit.MONTH) :
960                years = calendar.get(Calendar.YEAR);
961                calendar.clear(Calendar.MILLISECOND);
962                calendar.set(years, value, 1, 0, 0, 0);
963                Month month = new Month(calendar.getTime(), this.timeZone);
964                Date standardDate = calculateDateForPosition(
965                        month, this.tickMarkPosition);
966                long millis = standardDate.getTime();
967                if (millis >= date.getTime()) {
968                    month = (Month) month.previous();
969                    standardDate = calculateDateForPosition(
970                            month, this.tickMarkPosition);
971                }
972                return standardDate;
973
974            case(DateTickUnit.YEAR) :
975                if (this.tickMarkPosition == DateTickMarkPosition.START) {
976                    months = 0;
977                    days = 1;
978                }
979                else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
980                    months = 6;
981                    days = 1;
982                }
983                else {
984                    months = 11;
985                    days = 31;
986                }
987                calendar.clear(Calendar.MILLISECOND);
988                calendar.set(value, months, days, 0, 0, 0);
989                Date d3 = calendar.getTime();
990                if (d3.getTime() >= date.getTime()) {
991                    calendar.set(Calendar.YEAR, value - 1);
992                    d3 = calendar.getTime();
993                }
994                return d3;
995
996            default: return null;
997
998        }
999
1000    }
1001
1002    /**
1003     * Returns a {@link java.util.Date} corresponding to the specified position
1004     * within a {@link RegularTimePeriod}.
1005     *
1006     * @param period  the period.
1007     * @param position  the position (<code>null</code> not permitted).
1008     *
1009     * @return A date.
1010     */
1011    private Date calculateDateForPosition(RegularTimePeriod period, 
1012                                          DateTickMarkPosition position) {
1013        
1014        if (position == null) {
1015            throw new IllegalArgumentException("Null 'position' argument.");   
1016        }
1017        Date result = null;
1018        if (position == DateTickMarkPosition.START) {
1019            result = new Date(period.getFirstMillisecond());
1020        }
1021        else if (position == DateTickMarkPosition.MIDDLE) {
1022            result = new Date(period.getMiddleMillisecond());
1023        }
1024        else if (position == DateTickMarkPosition.END) {
1025            result = new Date(period.getLastMillisecond());
1026        }
1027        return result;
1028
1029    }
1030
1031    /**
1032     * Returns the first "standard" date (based on the specified field and 
1033     * units).
1034     *
1035     * @param date  the reference date.
1036     * @param unit  the date tick unit.
1037     *
1038     * @return The next "standard" date.
1039     */
1040    protected Date nextStandardDate(Date date, DateTickUnit unit) {
1041        Date previous = previousStandardDate(date, unit);
1042        Calendar calendar = Calendar.getInstance(this.timeZone);
1043        calendar.setTime(previous);
1044        calendar.add(unit.getCalendarField(), unit.getCount());
1045        return calendar.getTime();
1046    }
1047
1048    /**
1049     * Returns a collection of standard date tick units that uses the default 
1050     * time zone.  This collection will be used by default, but you are free 
1051     * to create your own collection if you want to (see the 
1052     * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 
1053     * from the {@link ValueAxis} class).
1054     *
1055     * @return A collection of standard date tick units.
1056     */
1057    public static TickUnitSource createStandardDateTickUnits() {
1058        return createStandardDateTickUnits(TimeZone.getDefault());
1059    }
1060
1061    /**
1062     * Returns a collection of standard date tick units.  This collection will 
1063     * be used by default, but you are free to create your own collection if 
1064     * you want to (see the 
1065     * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 
1066     * from the {@link ValueAxis} class).
1067     *
1068     * @param zone  the time zone (<code>null</code> not permitted).
1069     * 
1070     * @return A collection of standard date tick units.
1071     */
1072    public static TickUnitSource createStandardDateTickUnits(TimeZone zone) {
1073
1074        if (zone == null) {
1075            throw new IllegalArgumentException("Null 'zone' argument.");
1076        }
1077        TickUnits units = new TickUnits();
1078
1079        // date formatters
1080        DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
1081        DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
1082        DateFormat f3 = new SimpleDateFormat("HH:mm");
1083        DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
1084        DateFormat f5 = new SimpleDateFormat("d-MMM");
1085        DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
1086        DateFormat f7 = new SimpleDateFormat("yyyy");
1087        
1088        f1.setTimeZone(zone);
1089        f2.setTimeZone(zone);
1090        f3.setTimeZone(zone);
1091        f4.setTimeZone(zone);
1092        f5.setTimeZone(zone);
1093        f6.setTimeZone(zone);
1094        f7.setTimeZone(zone);
1095        
1096        // milliseconds
1097        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
1098        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5, 
1099                DateTickUnit.MILLISECOND, 1, f1));
1100        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10, 
1101                DateTickUnit.MILLISECOND, 1, f1));
1102        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25, 
1103                DateTickUnit.MILLISECOND, 5, f1));
1104        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50, 
1105                DateTickUnit.MILLISECOND, 10, f1));
1106        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100, 
1107                DateTickUnit.MILLISECOND, 10, f1));
1108        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250, 
1109                DateTickUnit.MILLISECOND, 10, f1));
1110        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500, 
1111                DateTickUnit.MILLISECOND, 50, f1));
1112
1113        // seconds
1114        units.add(new DateTickUnit(DateTickUnit.SECOND, 1, 
1115                DateTickUnit.MILLISECOND, 50, f2));
1116        units.add(new DateTickUnit(DateTickUnit.SECOND, 5, 
1117                DateTickUnit.SECOND, 1, f2));
1118        units.add(new DateTickUnit(DateTickUnit.SECOND, 10, 
1119                DateTickUnit.SECOND, 1, f2));
1120        units.add(new DateTickUnit(DateTickUnit.SECOND, 30, 
1121                DateTickUnit.SECOND, 5, f2));
1122
1123        // minutes
1124        units.add(new DateTickUnit(DateTickUnit.MINUTE, 1, 
1125                DateTickUnit.SECOND, 5, f3));
1126        units.add(new DateTickUnit(DateTickUnit.MINUTE, 2, 
1127                DateTickUnit.SECOND, 10, f3));
1128        units.add(new DateTickUnit(DateTickUnit.MINUTE, 5, 
1129                DateTickUnit.MINUTE, 1, f3));
1130        units.add(new DateTickUnit(DateTickUnit.MINUTE, 10, 
1131                DateTickUnit.MINUTE, 1, f3));
1132        units.add(new DateTickUnit(DateTickUnit.MINUTE, 15, 
1133                DateTickUnit.MINUTE, 5, f3));
1134        units.add(new DateTickUnit(DateTickUnit.MINUTE, 20, 
1135                DateTickUnit.MINUTE, 5, f3));
1136        units.add(new DateTickUnit(DateTickUnit.MINUTE, 30, 
1137                DateTickUnit.MINUTE, 5, f3));
1138
1139        // hours
1140        units.add(new DateTickUnit(DateTickUnit.HOUR, 1, 
1141                DateTickUnit.MINUTE, 5, f3));
1142        units.add(new DateTickUnit(DateTickUnit.HOUR, 2, 
1143                DateTickUnit.MINUTE, 10, f3));
1144        units.add(new DateTickUnit(DateTickUnit.HOUR, 4, 
1145                DateTickUnit.MINUTE, 30, f3));
1146        units.add(new DateTickUnit(DateTickUnit.HOUR, 6, 
1147                DateTickUnit.HOUR, 1, f3));
1148        units.add(new DateTickUnit(DateTickUnit.HOUR, 12, 
1149                DateTickUnit.HOUR, 1, f4));
1150
1151        // days
1152        units.add(new DateTickUnit(DateTickUnit.DAY, 1, 
1153                DateTickUnit.HOUR, 1, f5));
1154        units.add(new DateTickUnit(DateTickUnit.DAY, 2, 
1155                DateTickUnit.HOUR, 1, f5));
1156        units.add(new DateTickUnit(DateTickUnit.DAY, 7, 
1157                DateTickUnit.DAY, 1, f5));
1158        units.add(new DateTickUnit(DateTickUnit.DAY, 15, 
1159                DateTickUnit.DAY, 1, f5));
1160
1161        // months
1162        units.add(new DateTickUnit(DateTickUnit.MONTH, 1, 
1163                DateTickUnit.DAY, 1, f6));
1164        units.add(new DateTickUnit(DateTickUnit.MONTH, 2, 
1165                DateTickUnit.DAY, 1, f6));
1166        units.add(new DateTickUnit(DateTickUnit.MONTH, 3, 
1167                DateTickUnit.MONTH, 1, f6));
1168        units.add(new DateTickUnit(DateTickUnit.MONTH, 4,  
1169                DateTickUnit.MONTH, 1, f6));
1170        units.add(new DateTickUnit(DateTickUnit.MONTH, 6,  
1171                DateTickUnit.MONTH, 1, f6));
1172
1173        // years
1174        units.add(new DateTickUnit(DateTickUnit.YEAR, 1,  
1175                DateTickUnit.MONTH, 1, f7));
1176        units.add(new DateTickUnit(DateTickUnit.YEAR, 2,  
1177                DateTickUnit.MONTH, 3, f7));
1178        units.add(new DateTickUnit(DateTickUnit.YEAR, 5,  
1179                DateTickUnit.YEAR, 1, f7));
1180        units.add(new DateTickUnit(DateTickUnit.YEAR, 10,  
1181                DateTickUnit.YEAR, 1, f7));
1182        units.add(new DateTickUnit(DateTickUnit.YEAR, 25, 
1183                DateTickUnit.YEAR, 5, f7));
1184        units.add(new DateTickUnit(DateTickUnit.YEAR, 50, 
1185                DateTickUnit.YEAR, 10, f7));
1186        units.add(new DateTickUnit(DateTickUnit.YEAR, 100, 
1187                DateTickUnit.YEAR, 20, f7));
1188
1189        return units;
1190
1191    }
1192
1193    /**
1194     * Rescales the axis to ensure that all data is visible.
1195     */
1196    protected void autoAdjustRange() {
1197
1198        Plot plot = getPlot();
1199
1200        if (plot == null) {
1201            return;  // no plot, no data
1202        }
1203
1204        if (plot instanceof ValueAxisPlot) {
1205            ValueAxisPlot vap = (ValueAxisPlot) plot;
1206
1207            Range r = vap.getDataRange(this);
1208            if (r == null) {
1209                if (this.timeline instanceof SegmentedTimeline) { 
1210                    //Timeline hasn't method getStartTime()
1211                    r = new DateRange((
1212                            (SegmentedTimeline) this.timeline).getStartTime(),
1213                            ((SegmentedTimeline) this.timeline).getStartTime() 
1214                            + 1);
1215                } 
1216                else {
1217                    r = new DateRange();
1218                }
1219            }
1220
1221            long upper = this.timeline.toTimelineValue(
1222                    (long) r.getUpperBound());
1223            long lower;
1224            long fixedAutoRange = (long) getFixedAutoRange();
1225            if (fixedAutoRange > 0.0) {
1226                lower = upper - fixedAutoRange;
1227            }
1228            else {
1229                lower = this.timeline.toTimelineValue((long) r.getLowerBound());
1230                double range = upper - lower;
1231                long minRange = (long) getAutoRangeMinimumSize();
1232                if (range < minRange) {
1233                    long expand = (long) (minRange - range) / 2;
1234                    upper = upper + expand;
1235                    lower = lower - expand;
1236                }
1237                upper = upper + (long) (range * getUpperMargin());
1238                lower = lower - (long) (range * getLowerMargin());
1239            }
1240
1241            upper = this.timeline.toMillisecond(upper);
1242            lower = this.timeline.toMillisecond(lower);
1243            DateRange dr = new DateRange(new Date(lower), new Date(upper));
1244            setRange(dr, false, false);
1245        }
1246
1247    }
1248
1249    /**
1250     * Selects an appropriate tick value for the axis.  The strategy is to
1251     * display as many ticks as possible (selected from an array of 'standard'
1252     * tick units) without the labels overlapping.
1253     *
1254     * @param g2  the graphics device.
1255     * @param dataArea  the area defined by the axes.
1256     * @param edge  the axis location.
1257     */
1258    protected void selectAutoTickUnit(Graphics2D g2, 
1259                                      Rectangle2D dataArea,
1260                                      RectangleEdge edge) {
1261
1262        if (RectangleEdge.isTopOrBottom(edge)) {
1263            selectHorizontalAutoTickUnit(g2, dataArea, edge);
1264        }
1265        else if (RectangleEdge.isLeftOrRight(edge)) {
1266            selectVerticalAutoTickUnit(g2, dataArea, edge);
1267        }
1268
1269    }
1270
1271    /**
1272     * Selects an appropriate tick size for the axis.  The strategy is to
1273     * display as many ticks as possible (selected from a collection of 
1274     * 'standard' tick units) without the labels overlapping.
1275     *
1276     * @param g2  the graphics device.
1277     * @param dataArea  the area defined by the axes.
1278     * @param edge  the axis location.
1279     */
1280    protected void selectHorizontalAutoTickUnit(Graphics2D g2, 
1281                                                Rectangle2D dataArea, 
1282                                                RectangleEdge edge) {
1283
1284        long shift = 0;
1285        if (this.timeline instanceof SegmentedTimeline) {
1286            shift = ((SegmentedTimeline) this.timeline).getStartTime();
1287        }
1288        double zero = valueToJava2D(shift + 0.0, dataArea, edge);
1289        double tickLabelWidth 
1290            = estimateMaximumTickLabelWidth(g2, getTickUnit());
1291
1292        // start with the current tick unit...
1293        TickUnitSource tickUnits = getStandardTickUnits();
1294        TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
1295        double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge);
1296        double unit1Width = Math.abs(x1 - zero);
1297
1298        // then extrapolate...
1299        double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
1300        DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess);
1301        double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge);
1302        double unit2Width = Math.abs(x2 - zero);
1303        tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
1304        if (tickLabelWidth > unit2Width) {
1305            unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
1306        }
1307        setTickUnit(unit2, false, false);
1308    }
1309    
1310    /**
1311     * Selects an appropriate tick size for the axis.  The strategy is to
1312     * display as many ticks as possible (selected from a collection of 
1313     * 'standard' tick units) without the labels overlapping.
1314     *
1315     * @param g2  the graphics device.
1316     * @param dataArea  the area in which the plot should be drawn.
1317     * @param edge  the axis location.
1318     */
1319    protected void selectVerticalAutoTickUnit(Graphics2D g2,
1320                                              Rectangle2D dataArea,
1321                                              RectangleEdge edge) {
1322
1323        // start with the current tick unit...
1324        TickUnitSource tickUnits = getStandardTickUnits();
1325        double zero = valueToJava2D(0.0, dataArea, edge);
1326
1327        // start with a unit that is at least 1/10th of the axis length
1328        double estimate1 = getRange().getLength() / 10.0;
1329        DateTickUnit candidate1 
1330            = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1);
1331        double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1);
1332        double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge);
1333        double candidate1UnitHeight = Math.abs(y1 - zero);
1334
1335        // now extrapolate based on label height and unit height...
1336        double estimate2 
1337            = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize();
1338        DateTickUnit candidate2 
1339            = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2);
1340        double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2);
1341        double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge);
1342        double unit2Height = Math.abs(y2 - zero);
1343
1344       // make final selection...
1345       DateTickUnit finalUnit;
1346       if (labelHeight2 < unit2Height) {
1347           finalUnit = candidate2;
1348       }
1349       else {
1350           finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2);
1351       }
1352       setTickUnit(finalUnit, false, false);
1353
1354    }
1355
1356    /**
1357     * Estimates the maximum width of the tick labels, assuming the specified 
1358     * tick unit is used.
1359     * <P>
1360     * Rather than computing the string bounds of every tick on the axis, we
1361     * just look at two values: the lower bound and the upper bound for the 
1362     * axis.  These two values will usually be representative.
1363     *
1364     * @param g2  the graphics device.
1365     * @param unit  the tick unit to use for calculation.
1366     *
1367     * @return The estimated maximum width of the tick labels.
1368     */
1369    private double estimateMaximumTickLabelWidth(Graphics2D g2, 
1370                                                 DateTickUnit unit) {
1371
1372        RectangleInsets tickLabelInsets = getTickLabelInsets();
1373        double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
1374
1375        Font tickLabelFont = getTickLabelFont();
1376        FontRenderContext frc = g2.getFontRenderContext();
1377        LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1378        if (isVerticalTickLabels()) {
1379            // all tick labels have the same width (equal to the height of 
1380            // the font)...
1381            result += lm.getHeight();
1382        }
1383        else {
1384            // look at lower and upper bounds...
1385            DateRange range = (DateRange) getRange();
1386            Date lower = range.getLowerDate();
1387            Date upper = range.getUpperDate();
1388            String lowerStr = null;
1389            String upperStr = null;
1390            DateFormat formatter = getDateFormatOverride();
1391            if (formatter != null) {
1392                lowerStr = formatter.format(lower);
1393                upperStr = formatter.format(upper);
1394            }
1395            else {
1396                lowerStr = unit.dateToString(lower);
1397                upperStr = unit.dateToString(upper);
1398            }
1399            FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1400            double w1 = fm.stringWidth(lowerStr);
1401            double w2 = fm.stringWidth(upperStr);
1402            result += Math.max(w1, w2);
1403        }
1404
1405        return result;
1406
1407    }
1408
1409    /**
1410     * Estimates the maximum width of the tick labels, assuming the specified 
1411     * tick unit is used.
1412     * <P>
1413     * Rather than computing the string bounds of every tick on the axis, we 
1414     * just look at two values: the lower bound and the upper bound for the 
1415     * axis.  These two values will usually be representative.
1416     *
1417     * @param g2  the graphics device.
1418     * @param unit  the tick unit to use for calculation.
1419     *
1420     * @return The estimated maximum width of the tick labels.
1421     */
1422    private double estimateMaximumTickLabelHeight(Graphics2D g2, 
1423                                                  DateTickUnit unit) {
1424
1425        RectangleInsets tickLabelInsets = getTickLabelInsets();
1426        double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom();
1427
1428        Font tickLabelFont = getTickLabelFont();
1429        FontRenderContext frc = g2.getFontRenderContext();
1430        LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1431        if (!isVerticalTickLabels()) {
1432            // all tick labels have the same width (equal to the height of 
1433            // the font)...
1434            result += lm.getHeight();
1435        }
1436        else {
1437            // look at lower and upper bounds...
1438            DateRange range = (DateRange) getRange();
1439            Date lower = range.getLowerDate();
1440            Date upper = range.getUpperDate();
1441            String lowerStr = null;
1442            String upperStr = null;
1443            DateFormat formatter = getDateFormatOverride();
1444            if (formatter != null) {
1445                lowerStr = formatter.format(lower);
1446                upperStr = formatter.format(upper);
1447            }
1448            else {
1449                lowerStr = unit.dateToString(lower);
1450                upperStr = unit.dateToString(upper);
1451            }
1452            FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1453            double w1 = fm.stringWidth(lowerStr);
1454            double w2 = fm.stringWidth(upperStr);
1455            result += Math.max(w1, w2);
1456        }
1457
1458        return result;
1459
1460    }
1461
1462    /**
1463     * Calculates the positions of the tick labels for the axis, storing the 
1464     * results in the tick label list (ready for drawing).
1465     *
1466     * @param g2  the graphics device.
1467     * @param state  the axis state.
1468     * @param dataArea  the area in which the plot should be drawn.
1469     * @param edge  the location of the axis.
1470     *
1471     * @return A list of ticks.
1472     */
1473    public List refreshTicks(Graphics2D g2,
1474                             AxisState state,
1475                             Rectangle2D dataArea,
1476                             RectangleEdge edge) {
1477
1478        List result = null;
1479        if (RectangleEdge.isTopOrBottom(edge)) {
1480            result = refreshTicksHorizontal(g2, dataArea, edge);
1481        }
1482        else if (RectangleEdge.isLeftOrRight(edge)) {
1483            result = refreshTicksVertical(g2, dataArea, edge);
1484        }
1485        return result;
1486
1487    }
1488
1489    /**
1490     * Recalculates the ticks for the date axis.
1491     *
1492     * @param g2  the graphics device.
1493     * @param dataArea  the area in which the data is to be drawn.
1494     * @param edge  the location of the axis.
1495     *
1496     * @return A list of ticks.
1497     */
1498    protected List refreshTicksHorizontal(Graphics2D g2,
1499                                          Rectangle2D dataArea,
1500                                          RectangleEdge edge) {
1501
1502        List result = new java.util.ArrayList();
1503
1504        Font tickLabelFont = getTickLabelFont();
1505        g2.setFont(tickLabelFont);
1506
1507        if (isAutoTickUnitSelection()) {
1508            selectAutoTickUnit(g2, dataArea, edge);
1509        }
1510
1511        DateTickUnit unit = getTickUnit();
1512        Date tickDate = calculateLowestVisibleTickValue(unit);
1513        Date upperDate = getMaximumDate();
1514
1515        while (tickDate.before(upperDate)) {
1516
1517            if (!isHiddenValue(tickDate.getTime())) {
1518                // work out the value, label and position
1519                String tickLabel;
1520                DateFormat formatter = getDateFormatOverride();
1521                if (formatter != null) {
1522                    tickLabel = formatter.format(tickDate);
1523                }
1524                else {
1525                    tickLabel = this.tickUnit.dateToString(tickDate);
1526                }
1527                TextAnchor anchor = null;
1528                TextAnchor rotationAnchor = null;
1529                double angle = 0.0;
1530                if (isVerticalTickLabels()) {
1531                    anchor = TextAnchor.CENTER_RIGHT;
1532                    rotationAnchor = TextAnchor.CENTER_RIGHT;
1533                    if (edge == RectangleEdge.TOP) {
1534                        angle = Math.PI / 2.0;
1535                    }
1536                    else {
1537                        angle = -Math.PI / 2.0;
1538                    }
1539                }
1540                else {
1541                    if (edge == RectangleEdge.TOP) {
1542                        anchor = TextAnchor.BOTTOM_CENTER;
1543                        rotationAnchor = TextAnchor.BOTTOM_CENTER;
1544                    }
1545                    else {
1546                        anchor = TextAnchor.TOP_CENTER;
1547                        rotationAnchor = TextAnchor.TOP_CENTER;
1548                    }
1549                }
1550
1551                Tick tick = new DateTick(tickDate, tickLabel, anchor, 
1552                        rotationAnchor, angle);
1553                result.add(tick);
1554                tickDate = unit.addToDate(tickDate, this.timeZone);
1555            }
1556            else {
1557                tickDate = unit.rollDate(tickDate, this.timeZone);
1558                continue;
1559            }
1560
1561            // could add a flag to make the following correction optional...
1562            switch (unit.getUnit()) {
1563
1564                case (DateTickUnit.MILLISECOND) :
1565                case (DateTickUnit.SECOND) :
1566                case (DateTickUnit.MINUTE) :
1567                case (DateTickUnit.HOUR) :
1568                case (DateTickUnit.DAY) :
1569                    break;
1570                case (DateTickUnit.MONTH) :
1571                    tickDate = calculateDateForPosition(new Month(tickDate,
1572                            this.timeZone), this.tickMarkPosition);
1573                    break;
1574                case(DateTickUnit.YEAR) :
1575                    tickDate = calculateDateForPosition(new Year(tickDate, 
1576                            this.timeZone), this.tickMarkPosition);
1577                    break;
1578
1579                default: break;
1580
1581            }
1582
1583        }
1584        return result;
1585
1586    }
1587
1588    /**
1589     * Recalculates the ticks for the date axis.
1590     *
1591     * @param g2  the graphics device.
1592     * @param dataArea  the area in which the plot should be drawn.
1593     * @param edge  the location of the axis.
1594     *
1595     * @return A list of ticks.
1596     */
1597    protected List refreshTicksVertical(Graphics2D g2,
1598                                        Rectangle2D dataArea,
1599                                        RectangleEdge edge) {
1600
1601        List result = new java.util.ArrayList();
1602
1603        Font tickLabelFont = getTickLabelFont();
1604        g2.setFont(tickLabelFont);
1605
1606        if (isAutoTickUnitSelection()) {
1607            selectAutoTickUnit(g2, dataArea, edge);
1608        }
1609        DateTickUnit unit = getTickUnit();
1610        Date tickDate = calculateLowestVisibleTickValue(unit);
1611        //Date upperDate = calculateHighestVisibleTickValue(unit);
1612        Date upperDate = getMaximumDate();
1613        while (tickDate.before(upperDate)) {
1614
1615            if (!isHiddenValue(tickDate.getTime())) {
1616                // work out the value, label and position
1617                String tickLabel;
1618                DateFormat formatter = getDateFormatOverride();
1619                if (formatter != null) {
1620                    tickLabel = formatter.format(tickDate);
1621                }
1622                else {
1623                    tickLabel = this.tickUnit.dateToString(tickDate);
1624                }
1625                TextAnchor anchor = null;
1626                TextAnchor rotationAnchor = null;
1627                double angle = 0.0;
1628                if (isVerticalTickLabels()) {
1629                    anchor = TextAnchor.BOTTOM_CENTER;
1630                    rotationAnchor = TextAnchor.BOTTOM_CENTER;
1631                    if (edge == RectangleEdge.LEFT) {
1632                        angle = -Math.PI / 2.0;
1633                    }
1634                    else {
1635                        angle = Math.PI / 2.0;
1636                    }
1637                }
1638                else {
1639                    if (edge == RectangleEdge.LEFT) {
1640                        anchor = TextAnchor.CENTER_RIGHT;
1641                        rotationAnchor = TextAnchor.CENTER_RIGHT;
1642                    }
1643                    else {
1644                        anchor = TextAnchor.CENTER_LEFT;
1645                        rotationAnchor = TextAnchor.CENTER_LEFT;
1646                    }
1647                }
1648
1649                Tick tick = new DateTick(tickDate, tickLabel, anchor, 
1650                        rotationAnchor, angle);
1651                result.add(tick);
1652                tickDate = unit.addToDate(tickDate, this.timeZone);
1653            }
1654            else {
1655                tickDate = unit.rollDate(tickDate, this.timeZone);
1656            }
1657        }
1658        return result;
1659    }
1660
1661    /**
1662     * Draws the axis on a Java 2D graphics device (such as the screen or a 
1663     * printer).
1664     *
1665     * @param g2  the graphics device (<code>null</code> not permitted).
1666     * @param cursor  the cursor location.
1667     * @param plotArea  the area within which the axes and data should be 
1668     *                  drawn (<code>null</code> not permitted).
1669     * @param dataArea  the area within which the data should be drawn 
1670     *                  (<code>null</code> not permitted).
1671     * @param edge  the location of the axis (<code>null</code> not permitted).
1672     * @param plotState  collects information about the plot 
1673     *                   (<code>null</code> permitted).
1674     *
1675     * @return The axis state (never <code>null</code>).
1676     */
1677    public AxisState draw(Graphics2D g2, 
1678                          double cursor,
1679                          Rectangle2D plotArea, 
1680                          Rectangle2D dataArea, 
1681                          RectangleEdge edge,
1682                          PlotRenderingInfo plotState) {
1683
1684        // if the axis is not visible, don't draw it...
1685        if (!isVisible()) {
1686            AxisState state = new AxisState(cursor);
1687            // even though the axis is not visible, we need to refresh ticks in
1688            // case the grid is being drawn...
1689            List ticks = refreshTicks(g2, state, dataArea, edge);
1690            state.setTicks(ticks);
1691            return state;
1692        }
1693
1694        // draw the tick marks and labels...
1695        AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea, 
1696                dataArea, edge);
1697
1698        // draw the axis label (note that 'state' is passed in *and* 
1699        // returned)...
1700        state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
1701
1702        return state;
1703
1704    }
1705
1706    /**
1707     * Zooms in on the current range.
1708     *
1709     * @param lowerPercent  the new lower bound.
1710     * @param upperPercent  the new upper bound.
1711     */
1712    public void zoomRange(double lowerPercent, double upperPercent) {
1713        double start = this.timeline.toTimelineValue(
1714            (long) getRange().getLowerBound()
1715        );
1716        double length = (this.timeline.toTimelineValue(
1717                (long) getRange().getUpperBound()) 
1718                - this.timeline.toTimelineValue(
1719                    (long) getRange().getLowerBound()));
1720        Range adjusted = null;
1721        if (isInverted()) {
1722            adjusted = new DateRange(this.timeline.toMillisecond((long) (start 
1723                    + (length * (1 - upperPercent)))),
1724                    this.timeline.toMillisecond((long) (start + (length 
1725                    * (1 - lowerPercent)))));
1726        }
1727        else {
1728            adjusted = new DateRange(this.timeline.toMillisecond(
1729                    (long) (start + length * lowerPercent)), 
1730                    this.timeline.toMillisecond((long) (start + length 
1731                    * upperPercent)));
1732        }
1733        setRange(adjusted);
1734    } 
1735    
1736    /**
1737     * Tests this axis for equality with an arbitrary object.
1738     *
1739     * @param obj  the object (<code>null</code> permitted).
1740     *
1741     * @return A boolean.
1742     */
1743    public boolean equals(Object obj) {
1744        if (obj == this) {
1745            return true;
1746        }
1747        if (!(obj instanceof DateAxis)) {
1748            return false;
1749        }
1750        DateAxis that = (DateAxis) obj;
1751        if (!ObjectUtilities.equal(this.tickUnit, that.tickUnit)) {
1752            return false;
1753        }
1754        if (!ObjectUtilities.equal(this.dateFormatOverride, 
1755                that.dateFormatOverride)) {
1756            return false;
1757        }
1758        if (!ObjectUtilities.equal(this.tickMarkPosition, 
1759                that.tickMarkPosition)) {
1760            return false;
1761        }
1762        if (!ObjectUtilities.equal(this.timeline, that.timeline)) {
1763            return false;
1764        }
1765        if (!super.equals(obj)) {
1766            return false;
1767        }
1768        return true;
1769    }
1770
1771    /**
1772     * Returns a hash code for this object.
1773     * 
1774     * @return A hash code.
1775     */
1776    public int hashCode() {
1777        if (getLabel() != null) {
1778            return getLabel().hashCode();
1779        }
1780        else {
1781            return 0;
1782        }
1783    }
1784
1785    /**
1786     * Returns a clone of the object.
1787     *
1788     * @return A clone.
1789     *
1790     * @throws CloneNotSupportedException if some component of the axis does 
1791     *         not support cloning.
1792     */
1793    public Object clone() throws CloneNotSupportedException {
1794
1795        DateAxis clone = (DateAxis) super.clone();
1796
1797        // 'dateTickUnit' is immutable : no need to clone
1798        if (this.dateFormatOverride != null) {
1799            clone.dateFormatOverride 
1800                = (DateFormat) this.dateFormatOverride.clone();
1801        }
1802        // 'tickMarkPosition' is immutable : no need to clone
1803
1804        return clone;
1805
1806    }
1807            
1808}