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