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