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