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