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