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    
131    package org.jfree.chart.axis;
132    
133    import java.awt.Font;
134    import java.awt.FontMetrics;
135    import java.awt.Graphics2D;
136    import java.awt.font.FontRenderContext;
137    import java.awt.font.LineMetrics;
138    import java.awt.geom.Rectangle2D;
139    import java.io.Serializable;
140    import java.text.DateFormat;
141    import java.text.SimpleDateFormat;
142    import java.util.Calendar;
143    import java.util.Date;
144    import java.util.List;
145    import java.util.Locale;
146    import java.util.TimeZone;
147    
148    import org.jfree.chart.event.AxisChangeEvent;
149    import org.jfree.chart.plot.Plot;
150    import org.jfree.chart.plot.PlotRenderingInfo;
151    import org.jfree.chart.plot.ValueAxisPlot;
152    import org.jfree.data.Range;
153    import org.jfree.data.time.DateRange;
154    import org.jfree.data.time.Month;
155    import org.jfree.data.time.RegularTimePeriod;
156    import org.jfree.data.time.Year;
157    import org.jfree.ui.RectangleEdge;
158    import org.jfree.ui.RectangleInsets;
159    import org.jfree.ui.TextAnchor;
160    import 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     */
175    public 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    }