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