001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2011, 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     * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025     * Other names may be trademarks of their respective owners.]
026     *
027     * ---------------
028     * PeriodAxis.java
029     * ---------------
030     * (C) Copyright 2004-2009, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * Changes
036     * -------
037     * 01-Jun-2004 : Version 1 (DG);
038     * 16-Sep-2004 : Fixed bug in equals() method, added clone() method and
039     *               PublicCloneable interface (DG);
040     * 25-Nov-2004 : Updates to support major and minor tick marks (DG);
041     * 25-Feb-2005 : Fixed some tick mark bugs (DG);
042     * 15-Apr-2005 : Fixed some more tick mark bugs (DG);
043     * 26-Apr-2005 : Removed LOGGER (DG);
044     * 16-Jun-2005 : Fixed zooming (DG);
045     * 15-Sep-2005 : Changed configure() method to check autoRange flag,
046     *               and added ticks to state (DG);
047     * ------------- JFREECHART 1.0.x ---------------------------------------------
048     * 06-Oct-2006 : Updated for deprecations in RegularTimePeriod and
049     *               subclasses (DG);
050     * 22-Mar-2007 : Use new defaultAutoRange attribute (DG);
051     * 31-Jul-2007 : Fix for inverted axis labelling (see bug 1763413) (DG);
052     * 08-Apr-2008 : Notify listeners in setRange(Range, boolean, boolean) - fixes
053     *               bug 1932146 (DG);
054     * 16-Jan-2009 : Fixed bug 2490803, a problem in the setRange() method (DG);
055     * 02-Mar-2009 : Added locale - see patch 2569670 by Benjamin Bignell (DG);
056     * 02-Mar-2009 : Fixed draw() method to check tickMarksVisible and
057     *               tickLabelsVisible (DG);
058     * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG);
059     *
060     */
061    
062    package org.jfree.chart.axis;
063    
064    import java.awt.BasicStroke;
065    import java.awt.Color;
066    import java.awt.FontMetrics;
067    import java.awt.Graphics2D;
068    import java.awt.Paint;
069    import java.awt.Stroke;
070    import java.awt.geom.Line2D;
071    import java.awt.geom.Rectangle2D;
072    import java.io.IOException;
073    import java.io.ObjectInputStream;
074    import java.io.ObjectOutputStream;
075    import java.io.Serializable;
076    import java.lang.reflect.Constructor;
077    import java.text.DateFormat;
078    import java.text.SimpleDateFormat;
079    import java.util.ArrayList;
080    import java.util.Arrays;
081    import java.util.Calendar;
082    import java.util.Collections;
083    import java.util.Date;
084    import java.util.List;
085    import java.util.Locale;
086    import java.util.TimeZone;
087    
088    import org.jfree.chart.event.AxisChangeEvent;
089    import org.jfree.chart.plot.Plot;
090    import org.jfree.chart.plot.PlotRenderingInfo;
091    import org.jfree.chart.plot.ValueAxisPlot;
092    import org.jfree.data.Range;
093    import org.jfree.data.time.Day;
094    import org.jfree.data.time.Month;
095    import org.jfree.data.time.RegularTimePeriod;
096    import org.jfree.data.time.Year;
097    import org.jfree.io.SerialUtilities;
098    import org.jfree.text.TextUtilities;
099    import org.jfree.ui.RectangleEdge;
100    import org.jfree.ui.TextAnchor;
101    import org.jfree.util.PublicCloneable;
102    
103    /**
104     * An axis that displays a date scale based on a
105     * {@link org.jfree.data.time.RegularTimePeriod}.  This axis works when
106     * displayed across the bottom or top of a plot, but is broken for display at
107     * the left or right of charts.
108     */
109    public class PeriodAxis extends ValueAxis
110            implements Cloneable, PublicCloneable, Serializable {
111    
112        /** For serialization. */
113        private static final long serialVersionUID = 8353295532075872069L;
114    
115        /** The first time period in the overall range. */
116        private RegularTimePeriod first;
117    
118        /** The last time period in the overall range. */
119        private RegularTimePeriod last;
120    
121        /**
122         * The time zone used to convert 'first' and 'last' to absolute
123         * milliseconds.
124         */
125        private TimeZone timeZone;
126    
127        /**
128         * The locale (never <code>null</code>).
129         * 
130         * @since 1.0.13
131         */
132        private Locale locale;
133    
134        /**
135         * A calendar used for date manipulations in the current time zone and
136         * locale.
137         */
138        private Calendar calendar;
139    
140        /**
141         * The {@link RegularTimePeriod} subclass used to automatically determine
142         * the axis range.
143         */
144        private Class autoRangeTimePeriodClass;
145    
146        /**
147         * Indicates the {@link RegularTimePeriod} subclass that is used to
148         * determine the spacing of the major tick marks.
149         */
150        private Class majorTickTimePeriodClass;
151    
152        /**
153         * A flag that indicates whether or not tick marks are visible for the
154         * axis.
155         */
156        private boolean minorTickMarksVisible;
157    
158        /**
159         * Indicates the {@link RegularTimePeriod} subclass that is used to
160         * determine the spacing of the minor tick marks.
161         */
162        private Class minorTickTimePeriodClass;
163    
164        /** The length of the tick mark inside the data area (zero permitted). */
165        private float minorTickMarkInsideLength = 0.0f;
166    
167        /** The length of the tick mark outside the data area (zero permitted). */
168        private float minorTickMarkOutsideLength = 2.0f;
169    
170        /** The stroke used to draw tick marks. */
171        private transient Stroke minorTickMarkStroke = new BasicStroke(0.5f);
172    
173        /** The paint used to draw tick marks. */
174        private transient Paint minorTickMarkPaint = Color.black;
175    
176        /** Info for each labelling band. */
177        private PeriodAxisLabelInfo[] labelInfo;
178    
179        /**
180         * Creates a new axis.
181         *
182         * @param label  the axis label.
183         */
184        public PeriodAxis(String label) {
185            this(label, new Day(), new Day());
186        }
187    
188        /**
189         * Creates a new axis.
190         *
191         * @param label  the axis label (<code>null</code> permitted).
192         * @param first  the first time period in the axis range
193         *               (<code>null</code> not permitted).
194         * @param last  the last time period in the axis range
195         *              (<code>null</code> not permitted).
196         */
197        public PeriodAxis(String label,
198                          RegularTimePeriod first, RegularTimePeriod last) {
199            this(label, first, last, TimeZone.getDefault(), Locale.getDefault());
200        }
201    
202        /**
203         * Creates a new axis.
204         *
205         * @param label  the axis label (<code>null</code> permitted).
206         * @param first  the first time period in the axis range
207         *               (<code>null</code> not permitted).
208         * @param last  the last time period in the axis range
209         *              (<code>null</code> not permitted).
210         * @param timeZone  the time zone (<code>null</code> not permitted).
211         *
212         * @deprecated As of version 1.0.13, you should use the constructor that
213         *     specifies a Locale also.
214         */
215        public PeriodAxis(String label,
216                          RegularTimePeriod first, RegularTimePeriod last,
217                          TimeZone timeZone) {
218            this(label, first, last, timeZone, Locale.getDefault());
219        }
220    
221        /**
222         * Creates a new axis.
223         *
224         * @param label  the axis label (<code>null</code> permitted).
225         * @param first  the first time period in the axis range
226         *               (<code>null</code> not permitted).
227         * @param last  the last time period in the axis range
228         *              (<code>null</code> not permitted).
229         * @param timeZone  the time zone (<code>null</code> not permitted).
230         * @param locale  the locale (<code>null</code> not permitted).
231         *
232         * @since 1.0.13
233         */
234        public PeriodAxis(String label, RegularTimePeriod first,
235                RegularTimePeriod last, TimeZone timeZone, Locale locale) {
236            super(label, null);
237            if (timeZone == null) {
238                throw new IllegalArgumentException("Null 'timeZone' argument.");
239            }
240            if (locale == null) {
241                throw new IllegalArgumentException("Null 'locale' argument.");
242            }
243            this.first = first;
244            this.last = last;
245            this.timeZone = timeZone;
246            this.locale = locale;
247            this.calendar = Calendar.getInstance(timeZone, locale);
248            this.first.peg(this.calendar);
249            this.last.peg(this.calendar);
250            this.autoRangeTimePeriodClass = first.getClass();
251            this.majorTickTimePeriodClass = first.getClass();
252            this.minorTickMarksVisible = false;
253            this.minorTickTimePeriodClass = RegularTimePeriod.downsize(
254                    this.majorTickTimePeriodClass);
255            setAutoRange(true);
256            this.labelInfo = new PeriodAxisLabelInfo[2];
257            this.labelInfo[0] = new PeriodAxisLabelInfo(Month.class,
258                    new SimpleDateFormat("MMM", locale));
259            this.labelInfo[1] = new PeriodAxisLabelInfo(Year.class,
260                    new SimpleDateFormat("yyyy", locale));
261        }
262    
263        /**
264         * Returns the first time period in the axis range.
265         *
266         * @return The first time period (never <code>null</code>).
267         */
268        public RegularTimePeriod getFirst() {
269            return this.first;
270        }
271    
272        /**
273         * Sets the first time period in the axis range and sends an
274         * {@link AxisChangeEvent} to all registered listeners.
275         *
276         * @param first  the time period (<code>null</code> not permitted).
277         */
278        public void setFirst(RegularTimePeriod first) {
279            if (first == null) {
280                throw new IllegalArgumentException("Null 'first' argument.");
281            }
282            this.first = first;
283            this.first.peg(this.calendar);
284            notifyListeners(new AxisChangeEvent(this));
285        }
286    
287        /**
288         * Returns the last time period in the axis range.
289         *
290         * @return The last time period (never <code>null</code>).
291         */
292        public RegularTimePeriod getLast() {
293            return this.last;
294        }
295    
296        /**
297         * Sets the last time period in the axis range and sends an
298         * {@link AxisChangeEvent} to all registered listeners.
299         *
300         * @param last  the time period (<code>null</code> not permitted).
301         */
302        public void setLast(RegularTimePeriod last) {
303            if (last == null) {
304                throw new IllegalArgumentException("Null 'last' argument.");
305            }
306            this.last = last;
307            this.last.peg(this.calendar);
308            notifyListeners(new AxisChangeEvent(this));
309        }
310    
311        /**
312         * Returns the time zone used to convert the periods defining the axis
313         * range into absolute milliseconds.
314         *
315         * @return The time zone (never <code>null</code>).
316         */
317        public TimeZone getTimeZone() {
318            return this.timeZone;
319        }
320    
321        /**
322         * Sets the time zone that is used to convert the time periods into
323         * absolute milliseconds.
324         *
325         * @param zone  the time zone (<code>null</code> not permitted).
326         */
327        public void setTimeZone(TimeZone zone) {
328            if (zone == null) {
329                throw new IllegalArgumentException("Null 'zone' argument.");
330            }
331            this.timeZone = zone;
332            this.calendar = Calendar.getInstance(zone, this.locale);
333            this.first.peg(this.calendar);
334            this.last.peg(this.calendar);
335            notifyListeners(new AxisChangeEvent(this));
336        }
337    
338        /**
339         * Returns the locale for this axis.
340         *
341         * @return The locale (never (<code>null</code>).
342         *
343         * @since 1.0.13
344         */
345        public Locale getLocale() {
346            return this.locale;
347        }
348    
349        /**
350         * Returns the class used to create the first and last time periods for
351         * the axis range when the auto-range flag is set to <code>true</code>.
352         *
353         * @return The class (never <code>null</code>).
354         */
355        public Class getAutoRangeTimePeriodClass() {
356            return this.autoRangeTimePeriodClass;
357        }
358    
359        /**
360         * Sets the class used to create the first and last time periods for the
361         * axis range when the auto-range flag is set to <code>true</code> and
362         * sends an {@link AxisChangeEvent} to all registered listeners.
363         *
364         * @param c  the class (<code>null</code> not permitted).
365         */
366        public void setAutoRangeTimePeriodClass(Class c) {
367            if (c == null) {
368                throw new IllegalArgumentException("Null 'c' argument.");
369            }
370            this.autoRangeTimePeriodClass = c;
371            notifyListeners(new AxisChangeEvent(this));
372        }
373    
374        /**
375         * Returns the class that controls the spacing of the major tick marks.
376         *
377         * @return The class (never <code>null</code>).
378         */
379        public Class getMajorTickTimePeriodClass() {
380            return this.majorTickTimePeriodClass;
381        }
382    
383        /**
384         * Sets the class that controls the spacing of the major tick marks, and
385         * sends an {@link AxisChangeEvent} to all registered listeners.
386         *
387         * @param c  the class (a subclass of {@link RegularTimePeriod} is
388         *           expected).
389         */
390        public void setMajorTickTimePeriodClass(Class c) {
391            if (c == null) {
392                throw new IllegalArgumentException("Null 'c' argument.");
393            }
394            this.majorTickTimePeriodClass = c;
395            notifyListeners(new AxisChangeEvent(this));
396        }
397    
398        /**
399         * Returns the flag that controls whether or not minor tick marks
400         * are displayed for the axis.
401         *
402         * @return A boolean.
403         */
404        public boolean isMinorTickMarksVisible() {
405            return this.minorTickMarksVisible;
406        }
407    
408        /**
409         * Sets the flag that controls whether or not minor tick marks
410         * are displayed for the axis, and sends a {@link AxisChangeEvent}
411         * to all registered listeners.
412         *
413         * @param visible  the flag.
414         */
415        public void setMinorTickMarksVisible(boolean visible) {
416            this.minorTickMarksVisible = visible;
417            notifyListeners(new AxisChangeEvent(this));
418        }
419    
420        /**
421         * Returns the class that controls the spacing of the minor tick marks.
422         *
423         * @return The class (never <code>null</code>).
424         */
425        public Class getMinorTickTimePeriodClass() {
426            return this.minorTickTimePeriodClass;
427        }
428    
429        /**
430         * Sets the class that controls the spacing of the minor tick marks, and
431         * sends an {@link AxisChangeEvent} to all registered listeners.
432         *
433         * @param c  the class (a subclass of {@link RegularTimePeriod} is
434         *           expected).
435         */
436        public void setMinorTickTimePeriodClass(Class c) {
437            if (c == null) {
438                throw new IllegalArgumentException("Null 'c' argument.");
439            }
440            this.minorTickTimePeriodClass = c;
441            notifyListeners(new AxisChangeEvent(this));
442        }
443    
444        /**
445         * Returns the stroke used to display minor tick marks, if they are
446         * visible.
447         *
448         * @return A stroke (never <code>null</code>).
449         */
450        public Stroke getMinorTickMarkStroke() {
451            return this.minorTickMarkStroke;
452        }
453    
454        /**
455         * Sets the stroke used to display minor tick marks, if they are
456         * visible, and sends a {@link AxisChangeEvent} to all registered
457         * listeners.
458         *
459         * @param stroke  the stroke (<code>null</code> not permitted).
460         */
461        public void setMinorTickMarkStroke(Stroke stroke) {
462            if (stroke == null) {
463                throw new IllegalArgumentException("Null 'stroke' argument.");
464            }
465            this.minorTickMarkStroke = stroke;
466            notifyListeners(new AxisChangeEvent(this));
467        }
468    
469        /**
470         * Returns the paint used to display minor tick marks, if they are
471         * visible.
472         *
473         * @return A paint (never <code>null</code>).
474         */
475        public Paint getMinorTickMarkPaint() {
476            return this.minorTickMarkPaint;
477        }
478    
479        /**
480         * Sets the paint used to display minor tick marks, if they are
481         * visible, and sends a {@link AxisChangeEvent} to all registered
482         * listeners.
483         *
484         * @param paint  the paint (<code>null</code> not permitted).
485         */
486        public void setMinorTickMarkPaint(Paint paint) {
487            if (paint == null) {
488                throw new IllegalArgumentException("Null 'paint' argument.");
489            }
490            this.minorTickMarkPaint = paint;
491            notifyListeners(new AxisChangeEvent(this));
492        }
493    
494        /**
495         * Returns the inside length for the minor tick marks.
496         *
497         * @return The length.
498         */
499        public float getMinorTickMarkInsideLength() {
500            return this.minorTickMarkInsideLength;
501        }
502    
503        /**
504         * Sets the inside length of the minor tick marks and sends an
505         * {@link AxisChangeEvent} to all registered listeners.
506         *
507         * @param length  the length.
508         */
509        public void setMinorTickMarkInsideLength(float length) {
510            this.minorTickMarkInsideLength = length;
511            notifyListeners(new AxisChangeEvent(this));
512        }
513    
514        /**
515         * Returns the outside length for the minor tick marks.
516         *
517         * @return The length.
518         */
519        public float getMinorTickMarkOutsideLength() {
520            return this.minorTickMarkOutsideLength;
521        }
522    
523        /**
524         * Sets the outside length of the minor tick marks and sends an
525         * {@link AxisChangeEvent} to all registered listeners.
526         *
527         * @param length  the length.
528         */
529        public void setMinorTickMarkOutsideLength(float length) {
530            this.minorTickMarkOutsideLength = length;
531            notifyListeners(new AxisChangeEvent(this));
532        }
533    
534        /**
535         * Returns an array of label info records.
536         *
537         * @return An array.
538         */
539        public PeriodAxisLabelInfo[] getLabelInfo() {
540            return this.labelInfo;
541        }
542    
543        /**
544         * Sets the array of label info records and sends an
545         * {@link AxisChangeEvent} to all registered listeners.
546         *
547         * @param info  the info.
548         */
549        public void setLabelInfo(PeriodAxisLabelInfo[] info) {
550            this.labelInfo = info;
551            notifyListeners(new AxisChangeEvent(this));
552        }
553    
554        /**
555         * Sets the range for the axis, if requested, sends an
556         * {@link AxisChangeEvent} to all registered listeners.  As a side-effect,
557         * the auto-range flag is set to <code>false</code> (optional).
558         *
559         * @param range  the range (<code>null</code> not permitted).
560         * @param turnOffAutoRange  a flag that controls whether or not the auto
561         *                          range is turned off.
562         * @param notify  a flag that controls whether or not listeners are
563         *                notified.
564         */
565        public void setRange(Range range, boolean turnOffAutoRange,
566                             boolean notify) {
567            long upper = Math.round(range.getUpperBound());
568            long lower = Math.round(range.getLowerBound());
569            this.first = createInstance(this.autoRangeTimePeriodClass,
570                    new Date(lower), this.timeZone, this.locale);
571            this.last = createInstance(this.autoRangeTimePeriodClass,
572                    new Date(upper), this.timeZone, this.locale);
573            super.setRange(new Range(this.first.getFirstMillisecond(),
574                    this.last.getLastMillisecond() + 1.0), turnOffAutoRange,
575                    notify);
576        }
577    
578        /**
579         * Configures the axis to work with the current plot.  Override this method
580         * to perform any special processing (such as auto-rescaling).
581         */
582        public void configure() {
583            if (this.isAutoRange()) {
584                autoAdjustRange();
585            }
586        }
587    
588        /**
589         * Estimates the space (height or width) required to draw the axis.
590         *
591         * @param g2  the graphics device.
592         * @param plot  the plot that the axis belongs to.
593         * @param plotArea  the area within which the plot (including axes) should
594         *                  be drawn.
595         * @param edge  the axis location.
596         * @param space  space already reserved.
597         *
598         * @return The space required to draw the axis (including pre-reserved
599         *         space).
600         */
601        public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
602                                      Rectangle2D plotArea, RectangleEdge edge,
603                                      AxisSpace space) {
604            // create a new space object if one wasn't supplied...
605            if (space == null) {
606                space = new AxisSpace();
607            }
608    
609            // if the axis is not visible, no additional space is required...
610            if (!isVisible()) {
611                return space;
612            }
613    
614            // if the axis has a fixed dimension, return it...
615            double dimension = getFixedDimension();
616            if (dimension > 0.0) {
617                space.ensureAtLeast(dimension, edge);
618            }
619    
620            // get the axis label size and update the space object...
621            Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
622            double labelHeight = 0.0;
623            double labelWidth = 0.0;
624            double tickLabelBandsDimension = 0.0;
625    
626            for (int i = 0; i < this.labelInfo.length; i++) {
627                PeriodAxisLabelInfo info = this.labelInfo[i];
628                FontMetrics fm = g2.getFontMetrics(info.getLabelFont());
629                tickLabelBandsDimension
630                    += info.getPadding().extendHeight(fm.getHeight());
631            }
632    
633            if (RectangleEdge.isTopOrBottom(edge)) {
634                labelHeight = labelEnclosure.getHeight();
635                space.add(labelHeight + tickLabelBandsDimension, edge);
636            }
637            else if (RectangleEdge.isLeftOrRight(edge)) {
638                labelWidth = labelEnclosure.getWidth();
639                space.add(labelWidth + tickLabelBandsDimension, edge);
640            }
641    
642            // add space for the outer tick labels, if any...
643            double tickMarkSpace = 0.0;
644            if (isTickMarksVisible()) {
645                tickMarkSpace = getTickMarkOutsideLength();
646            }
647            if (this.minorTickMarksVisible) {
648                tickMarkSpace = Math.max(tickMarkSpace,
649                        this.minorTickMarkOutsideLength);
650            }
651            space.add(tickMarkSpace, edge);
652            return space;
653        }
654    
655        /**
656         * Draws the axis on a Java 2D graphics device (such as the screen or a
657         * printer).
658         *
659         * @param g2  the graphics device (<code>null</code> not permitted).
660         * @param cursor  the cursor location (determines where to draw the axis).
661         * @param plotArea  the area within which the axes and plot should be drawn.
662         * @param dataArea  the area within which the data should be drawn.
663         * @param edge  the axis location (<code>null</code> not permitted).
664         * @param plotState  collects information about the plot
665         *                   (<code>null</code> permitted).
666         *
667         * @return The axis state (never <code>null</code>).
668         */
669        public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
670                Rectangle2D dataArea, RectangleEdge edge,
671                PlotRenderingInfo plotState) {
672    
673            AxisState axisState = new AxisState(cursor);
674            if (isAxisLineVisible()) {
675                drawAxisLine(g2, cursor, dataArea, edge);
676            }
677            if (isTickMarksVisible()) {
678                drawTickMarks(g2, axisState, dataArea, edge);
679            }
680            if (isTickLabelsVisible()) {
681                for (int band = 0; band < this.labelInfo.length; band++) {
682                    axisState = drawTickLabels(band, g2, axisState, dataArea, edge);
683                }
684            }
685    
686            // draw the axis label (note that 'state' is passed in *and*
687            // returned)...
688            axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge,
689                    axisState);
690            return axisState;
691    
692        }
693    
694        /**
695         * Draws the tick marks for the axis.
696         *
697         * @param g2  the graphics device.
698         * @param state  the axis state.
699         * @param dataArea  the data area.
700         * @param edge  the edge.
701         */
702        protected void drawTickMarks(Graphics2D g2, AxisState state,
703                                     Rectangle2D dataArea,
704                                     RectangleEdge edge) {
705            if (RectangleEdge.isTopOrBottom(edge)) {
706                drawTickMarksHorizontal(g2, state, dataArea, edge);
707            }
708            else if (RectangleEdge.isLeftOrRight(edge)) {
709                drawTickMarksVertical(g2, state, dataArea, edge);
710            }
711        }
712    
713        /**
714         * Draws the major and minor tick marks for an axis that lies at the top or
715         * bottom of the plot.
716         *
717         * @param g2  the graphics device.
718         * @param state  the axis state.
719         * @param dataArea  the data area.
720         * @param edge  the edge.
721         */
722        protected void drawTickMarksHorizontal(Graphics2D g2, AxisState state,
723                                               Rectangle2D dataArea,
724                                               RectangleEdge edge) {
725            List ticks = new ArrayList();
726            double x0;
727            double y0 = state.getCursor();
728            double insideLength = getTickMarkInsideLength();
729            double outsideLength = getTickMarkOutsideLength();
730            RegularTimePeriod t = createInstance(this.majorTickTimePeriodClass, 
731                    this.first.getStart(), getTimeZone(), this.locale);
732            long t0 = t.getFirstMillisecond();
733            Line2D inside = null;
734            Line2D outside = null;
735            long firstOnAxis = getFirst().getFirstMillisecond();
736            long lastOnAxis = getLast().getLastMillisecond() + 1;
737            while (t0 <= lastOnAxis) {
738                ticks.add(new NumberTick(new Double(t0), "", TextAnchor.CENTER,
739                        TextAnchor.CENTER, 0.0));
740                x0 = valueToJava2D(t0, dataArea, edge);
741                if (edge == RectangleEdge.TOP) {
742                    inside = new Line2D.Double(x0, y0, x0, y0 + insideLength);
743                    outside = new Line2D.Double(x0, y0, x0, y0 - outsideLength);
744                }
745                else if (edge == RectangleEdge.BOTTOM) {
746                    inside = new Line2D.Double(x0, y0, x0, y0 - insideLength);
747                    outside = new Line2D.Double(x0, y0, x0, y0 + outsideLength);
748                }
749                if (t0 >= firstOnAxis) {
750                    g2.setPaint(getTickMarkPaint());
751                    g2.setStroke(getTickMarkStroke());
752                    g2.draw(inside);
753                    g2.draw(outside);
754                }
755                // draw minor tick marks
756                if (this.minorTickMarksVisible) {
757                    RegularTimePeriod tminor = createInstance(
758                            this.minorTickTimePeriodClass, new Date(t0),
759                            getTimeZone(), this.locale);
760                    long tt0 = tminor.getFirstMillisecond();
761                    while (tt0 < t.getLastMillisecond()
762                            && tt0 < lastOnAxis) {
763                        double xx0 = valueToJava2D(tt0, dataArea, edge);
764                        if (edge == RectangleEdge.TOP) {
765                            inside = new Line2D.Double(xx0, y0, xx0,
766                                    y0 + this.minorTickMarkInsideLength);
767                            outside = new Line2D.Double(xx0, y0, xx0,
768                                    y0 - this.minorTickMarkOutsideLength);
769                        }
770                        else if (edge == RectangleEdge.BOTTOM) {
771                            inside = new Line2D.Double(xx0, y0, xx0,
772                                    y0 - this.minorTickMarkInsideLength);
773                            outside = new Line2D.Double(xx0, y0, xx0,
774                                    y0 + this.minorTickMarkOutsideLength);
775                        }
776                        if (tt0 >= firstOnAxis) {
777                            g2.setPaint(this.minorTickMarkPaint);
778                            g2.setStroke(this.minorTickMarkStroke);
779                            g2.draw(inside);
780                            g2.draw(outside);
781                        }
782                        tminor = tminor.next();
783                        tminor.peg(this.calendar);
784                        tt0 = tminor.getFirstMillisecond();
785                    }
786                }
787                t = t.next();
788                t.peg(this.calendar);
789                t0 = t.getFirstMillisecond();
790            }
791            if (edge == RectangleEdge.TOP) {
792                state.cursorUp(Math.max(outsideLength,
793                        this.minorTickMarkOutsideLength));
794            }
795            else if (edge == RectangleEdge.BOTTOM) {
796                state.cursorDown(Math.max(outsideLength,
797                        this.minorTickMarkOutsideLength));
798            }
799            state.setTicks(ticks);
800        }
801    
802        /**
803         * Draws the tick marks for a vertical axis.
804         *
805         * @param g2  the graphics device.
806         * @param state  the axis state.
807         * @param dataArea  the data area.
808         * @param edge  the edge.
809         */
810        protected void drawTickMarksVertical(Graphics2D g2, AxisState state,
811                                             Rectangle2D dataArea,
812                                             RectangleEdge edge) {
813            // FIXME:  implement this...
814        }
815    
816        /**
817         * Draws the tick labels for one "band" of time periods.
818         *
819         * @param band  the band index (zero-based).
820         * @param g2  the graphics device.
821         * @param state  the axis state.
822         * @param dataArea  the data area.
823         * @param edge  the edge where the axis is located.
824         *
825         * @return The updated axis state.
826         */
827        protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state,
828                                           Rectangle2D dataArea,
829                                           RectangleEdge edge) {
830    
831            // work out the initial gap
832            double delta1 = 0.0;
833            FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont());
834            if (edge == RectangleEdge.BOTTOM) {
835                delta1 = this.labelInfo[band].getPadding().calculateTopOutset(
836                        fm.getHeight());
837            }
838            else if (edge == RectangleEdge.TOP) {
839                delta1 = this.labelInfo[band].getPadding().calculateBottomOutset(
840                        fm.getHeight());
841            }
842            state.moveCursor(delta1, edge);
843            long axisMin = this.first.getFirstMillisecond();
844            long axisMax = this.last.getLastMillisecond();
845            g2.setFont(this.labelInfo[band].getLabelFont());
846            g2.setPaint(this.labelInfo[band].getLabelPaint());
847    
848            // work out the number of periods to skip for labelling
849            RegularTimePeriod p1 = this.labelInfo[band].createInstance(
850                    new Date(axisMin), this.timeZone, this.locale);
851            RegularTimePeriod p2 = this.labelInfo[band].createInstance(
852                    new Date(axisMax), this.timeZone, this.locale);
853            String label1 = this.labelInfo[band].getDateFormat().format(
854                    new Date(p1.getMiddleMillisecond()));
855            String label2 = this.labelInfo[band].getDateFormat().format(
856                    new Date(p2.getMiddleMillisecond()));
857            Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2,
858                    g2.getFontMetrics());
859            Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2,
860                    g2.getFontMetrics());
861            double w = Math.max(b1.getWidth(), b2.getWidth());
862            long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0,
863                    dataArea, edge));
864            if (isInverted()) {
865                ww = axisMax - ww;
866            }
867            else {
868                ww = ww - axisMin;
869            }
870            long length = p1.getLastMillisecond()
871                          - p1.getFirstMillisecond();
872            int periods = (int) (ww / length) + 1;
873    
874            RegularTimePeriod p = this.labelInfo[band].createInstance(
875                    new Date(axisMin), this.timeZone, this.locale);
876            Rectangle2D b = null;
877            long lastXX = 0L;
878            float y = (float) (state.getCursor());
879            TextAnchor anchor = TextAnchor.TOP_CENTER;
880            float yDelta = (float) b1.getHeight();
881            if (edge == RectangleEdge.TOP) {
882                anchor = TextAnchor.BOTTOM_CENTER;
883                yDelta = -yDelta;
884            }
885            while (p.getFirstMillisecond() <= axisMax) {
886                float x = (float) valueToJava2D(p.getMiddleMillisecond(), dataArea,
887                        edge);
888                DateFormat df = this.labelInfo[band].getDateFormat();
889                String label = df.format(new Date(p.getMiddleMillisecond()));
890                long first = p.getFirstMillisecond();
891                long last = p.getLastMillisecond();
892                if (last > axisMax) {
893                    // this is the last period, but it is only partially visible
894                    // so check that the label will fit before displaying it...
895                    Rectangle2D bb = TextUtilities.getTextBounds(label, g2,
896                            g2.getFontMetrics());
897                    if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {
898                        float xstart = (float) valueToJava2D(Math.max(first,
899                                axisMin), dataArea, edge);
900                        if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {
901                            x = ((float) dataArea.getMaxX() + xstart) / 2.0f;
902                        }
903                        else {
904                            label = null;
905                        }
906                    }
907                }
908                if (first < axisMin) {
909                    // this is the first period, but it is only partially visible
910                    // so check that the label will fit before displaying it...
911                    Rectangle2D bb = TextUtilities.getTextBounds(label, g2,
912                            g2.getFontMetrics());
913                    if ((x - bb.getWidth() / 2) < dataArea.getX()) {
914                        float xlast = (float) valueToJava2D(Math.min(last,
915                                axisMax), dataArea, edge);
916                        if (bb.getWidth() < (xlast - dataArea.getX())) {
917                            x = (xlast + (float) dataArea.getX()) / 2.0f;
918                        }
919                        else {
920                            label = null;
921                        }
922                    }
923    
924                }
925                if (label != null) {
926                    g2.setPaint(this.labelInfo[band].getLabelPaint());
927                    b = TextUtilities.drawAlignedString(label, g2, x, y, anchor);
928                }
929                if (lastXX > 0L) {
930                    if (this.labelInfo[band].getDrawDividers()) {
931                        long nextXX = p.getFirstMillisecond();
932                        long mid = (lastXX + nextXX) / 2;
933                        float mid2d = (float) valueToJava2D(mid, dataArea, edge);
934                        g2.setStroke(this.labelInfo[band].getDividerStroke());
935                        g2.setPaint(this.labelInfo[band].getDividerPaint());
936                        g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta));
937                    }
938                }
939                lastXX = last;
940                for (int i = 0; i < periods; i++) {
941                    p = p.next();
942                }
943                p.peg(this.calendar);
944            }
945            double used = 0.0;
946            if (b != null) {
947                used = b.getHeight();
948                // work out the trailing gap
949                if (edge == RectangleEdge.BOTTOM) {
950                    used += this.labelInfo[band].getPadding().calculateBottomOutset(
951                            fm.getHeight());
952                }
953                else if (edge == RectangleEdge.TOP) {
954                    used += this.labelInfo[band].getPadding().calculateTopOutset(
955                            fm.getHeight());
956                }
957            }
958            state.moveCursor(used, edge);
959            return state;
960        }
961    
962        /**
963         * Calculates the positions of the ticks for the axis, storing the results
964         * in the tick list (ready for drawing).
965         *
966         * @param g2  the graphics device.
967         * @param state  the axis state.
968         * @param dataArea  the area inside the axes.
969         * @param edge  the edge on which the axis is located.
970         *
971         * @return The list of ticks.
972         */
973        public List refreshTicks(Graphics2D g2, AxisState state,
974                Rectangle2D dataArea, RectangleEdge edge) {
975            return Collections.EMPTY_LIST;
976        }
977    
978        /**
979         * Converts a data value to a coordinate in Java2D space, assuming that the
980         * axis runs along one edge of the specified dataArea.
981         * <p>
982         * Note that it is possible for the coordinate to fall outside the area.
983         *
984         * @param value  the data value.
985         * @param area  the area for plotting the data.
986         * @param edge  the edge along which the axis lies.
987         *
988         * @return The Java2D coordinate.
989         */
990        public double valueToJava2D(double value, Rectangle2D area,
991                RectangleEdge edge) {
992    
993            double result = Double.NaN;
994            double axisMin = this.first.getFirstMillisecond();
995            double axisMax = this.last.getLastMillisecond();
996            if (RectangleEdge.isTopOrBottom(edge)) {
997                double minX = area.getX();
998                double maxX = area.getMaxX();
999                if (isInverted()) {
1000                    result = maxX + ((value - axisMin) / (axisMax - axisMin))
1001                             * (minX - maxX);
1002                }
1003                else {
1004                    result = minX + ((value - axisMin) / (axisMax - axisMin))
1005                             * (maxX - minX);
1006                }
1007            }
1008            else if (RectangleEdge.isLeftOrRight(edge)) {
1009                double minY = area.getMinY();
1010                double maxY = area.getMaxY();
1011                if (isInverted()) {
1012                    result = minY + (((value - axisMin) / (axisMax - axisMin))
1013                             * (maxY - minY));
1014                }
1015                else {
1016                    result = maxY - (((value - axisMin) / (axisMax - axisMin))
1017                             * (maxY - minY));
1018                }
1019            }
1020            return result;
1021    
1022        }
1023    
1024        /**
1025         * Converts a coordinate in Java2D space to the corresponding data value,
1026         * assuming that the axis runs along one edge of the specified dataArea.
1027         *
1028         * @param java2DValue  the coordinate in Java2D space.
1029         * @param area  the area in which the data is plotted.
1030         * @param edge  the edge along which the axis lies.
1031         *
1032         * @return The data value.
1033         */
1034        public double java2DToValue(double java2DValue, Rectangle2D area,
1035                RectangleEdge edge) {
1036    
1037            double result = Double.NaN;
1038            double min = 0.0;
1039            double max = 0.0;
1040            double axisMin = this.first.getFirstMillisecond();
1041            double axisMax = this.last.getLastMillisecond();
1042            if (RectangleEdge.isTopOrBottom(edge)) {
1043                min = area.getX();
1044                max = area.getMaxX();
1045            }
1046            else if (RectangleEdge.isLeftOrRight(edge)) {
1047                min = area.getMaxY();
1048                max = area.getY();
1049            }
1050            if (isInverted()) {
1051                 result = axisMax - ((java2DValue - min) / (max - min)
1052                          * (axisMax - axisMin));
1053            }
1054            else {
1055                 result = axisMin + ((java2DValue - min) / (max - min)
1056                          * (axisMax - axisMin));
1057            }
1058            return result;
1059        }
1060    
1061        /**
1062         * Rescales the axis to ensure that all data is visible.
1063         */
1064        protected void autoAdjustRange() {
1065    
1066            Plot plot = getPlot();
1067            if (plot == null) {
1068                return;  // no plot, no data
1069            }
1070    
1071            if (plot instanceof ValueAxisPlot) {
1072                ValueAxisPlot vap = (ValueAxisPlot) plot;
1073    
1074                Range r = vap.getDataRange(this);
1075                if (r == null) {
1076                    r = getDefaultAutoRange();
1077                }
1078    
1079                long upper = Math.round(r.getUpperBound());
1080                long lower = Math.round(r.getLowerBound());
1081                this.first = createInstance(this.autoRangeTimePeriodClass,
1082                        new Date(lower), this.timeZone, this.locale);
1083                this.last = createInstance(this.autoRangeTimePeriodClass,
1084                        new Date(upper), this.timeZone, this.locale);
1085                setRange(r, false, false);
1086            }
1087    
1088        }
1089    
1090        /**
1091         * Tests the axis for equality with an arbitrary object.
1092         *
1093         * @param obj  the object (<code>null</code> permitted).
1094         *
1095         * @return A boolean.
1096         */
1097        public boolean equals(Object obj) {
1098            if (obj == this) {
1099                return true;
1100            }
1101            if (!(obj instanceof PeriodAxis)) {
1102                return false;
1103            }
1104            PeriodAxis that = (PeriodAxis) obj;
1105            if (!this.first.equals(that.first)) {
1106                return false;
1107            }
1108            if (!this.last.equals(that.last)) {
1109                return false;
1110            }
1111            if (!this.timeZone.equals(that.timeZone)) {
1112                return false;
1113            }
1114            if (!this.locale.equals(that.locale)) {
1115                return false;
1116            }
1117            if (!this.autoRangeTimePeriodClass.equals(
1118                    that.autoRangeTimePeriodClass)) {
1119                return false;
1120            }
1121            if (!(isMinorTickMarksVisible() == that.isMinorTickMarksVisible())) {
1122                return false;
1123            }
1124            if (!this.majorTickTimePeriodClass.equals(
1125                    that.majorTickTimePeriodClass)) {
1126                return false;
1127            }
1128            if (!this.minorTickTimePeriodClass.equals(
1129                    that.minorTickTimePeriodClass)) {
1130                return false;
1131            }
1132            if (!this.minorTickMarkPaint.equals(that.minorTickMarkPaint)) {
1133                return false;
1134            }
1135            if (!this.minorTickMarkStroke.equals(that.minorTickMarkStroke)) {
1136                return false;
1137            }
1138            if (!Arrays.equals(this.labelInfo, that.labelInfo)) {
1139                return false;
1140            }
1141            return super.equals(obj);
1142        }
1143    
1144        /**
1145         * Returns a hash code for this object.
1146         *
1147         * @return A hash code.
1148         */
1149        public int hashCode() {
1150            if (getLabel() != null) {
1151                return getLabel().hashCode();
1152            }
1153            else {
1154                return 0;
1155            }
1156        }
1157    
1158        /**
1159         * Returns a clone of the axis.
1160         *
1161         * @return A clone.
1162         *
1163         * @throws CloneNotSupportedException  this class is cloneable, but
1164         *         subclasses may not be.
1165         */
1166        public Object clone() throws CloneNotSupportedException {
1167            PeriodAxis clone = (PeriodAxis) super.clone();
1168            clone.timeZone = (TimeZone) this.timeZone.clone();
1169            clone.labelInfo = new PeriodAxisLabelInfo[this.labelInfo.length];
1170            for (int i = 0; i < this.labelInfo.length; i++) {
1171                clone.labelInfo[i] = this.labelInfo[i];  // copy across references
1172                                                         // to immutable objs
1173            }
1174            return clone;
1175        }
1176    
1177        /**
1178         * A utility method used to create a particular subclass of the
1179         * {@link RegularTimePeriod} class that includes the specified millisecond,
1180         * assuming the specified time zone.
1181         *
1182         * @param periodClass  the class.
1183         * @param millisecond  the time.
1184         * @param zone  the time zone.
1185         * @param locale  the locale.
1186         *
1187         * @return The time period.
1188         */
1189        private RegularTimePeriod createInstance(Class periodClass, 
1190                Date millisecond, TimeZone zone, Locale locale) {
1191            RegularTimePeriod result = null;
1192            try {
1193                Constructor c = periodClass.getDeclaredConstructor(new Class[] {
1194                        Date.class, TimeZone.class, Locale.class});
1195                result = (RegularTimePeriod) c.newInstance(new Object[] {
1196                        millisecond, zone, locale});
1197            }
1198            catch (Exception e) {
1199                try {
1200                    Constructor c = periodClass.getDeclaredConstructor(new Class[] {
1201                            Date.class});
1202                    result = (RegularTimePeriod) c.newInstance(new Object[] {
1203                            millisecond});
1204                }
1205                catch (Exception e2) {
1206                    // do nothing
1207                }
1208            }
1209            return result;
1210        }
1211    
1212        /**
1213         * Provides serialization support.
1214         *
1215         * @param stream  the output stream.
1216         *
1217         * @throws IOException  if there is an I/O error.
1218         */
1219        private void writeObject(ObjectOutputStream stream) throws IOException {
1220            stream.defaultWriteObject();
1221            SerialUtilities.writeStroke(this.minorTickMarkStroke, stream);
1222            SerialUtilities.writePaint(this.minorTickMarkPaint, stream);
1223        }
1224    
1225        /**
1226         * Provides serialization support.
1227         *
1228         * @param stream  the input stream.
1229         *
1230         * @throws IOException  if there is an I/O error.
1231         * @throws ClassNotFoundException  if there is a classpath problem.
1232         */
1233        private void readObject(ObjectInputStream stream)
1234            throws IOException, ClassNotFoundException {
1235            stream.defaultReadObject();
1236            this.minorTickMarkStroke = SerialUtilities.readStroke(stream);
1237            this.minorTickMarkPaint = SerialUtilities.readPaint(stream);
1238        }
1239    
1240    }