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