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