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.
490         * 
491         * @param info  the info.
492         */
493        public void setLabelInfo(PeriodAxisLabelInfo[] info) {
494            this.labelInfo = info;
495            // FIXME: shouldn't this generate an event?
496        }
497        
498        /**
499         * Returns the range for the axis.
500         *
501         * @return The axis range (never <code>null</code>).
502         */
503        public Range getRange() {
504            // TODO: find a cleaner way to do this...
505            return new Range(this.first.getFirstMillisecond(this.calendar), 
506                    this.last.getLastMillisecond(this.calendar));
507        }
508    
509        /**
510         * Sets the range for the axis, if requested, sends an 
511         * {@link AxisChangeEvent} to all registered listeners.  As a side-effect, 
512         * the auto-range flag is set to <code>false</code> (optional).
513         *
514         * @param range  the range (<code>null</code> not permitted).
515         * @param turnOffAutoRange  a flag that controls whether or not the auto 
516         *                          range is turned off.         
517         * @param notify  a flag that controls whether or not listeners are 
518         *                notified.
519         */
520        public void setRange(Range range, boolean turnOffAutoRange, 
521                             boolean notify) {
522            super.setRange(range, turnOffAutoRange, false);
523            long upper = Math.round(range.getUpperBound());
524            long lower = Math.round(range.getLowerBound());
525            this.first = createInstance(this.autoRangeTimePeriodClass, 
526                    new Date(lower), this.timeZone);
527            this.last = createInstance(this.autoRangeTimePeriodClass, 
528                    new Date(upper), this.timeZone);
529            if (notify) {
530                notifyListeners(new AxisChangeEvent(this));
531            }
532        }
533    
534        /**
535         * Configures the axis to work with the current plot.  Override this method
536         * to perform any special processing (such as auto-rescaling).
537         */
538        public void configure() {
539            if (this.isAutoRange()) {
540                autoAdjustRange();
541            }
542        }
543    
544        /**
545         * Estimates the space (height or width) required to draw the axis.
546         *
547         * @param g2  the graphics device.
548         * @param plot  the plot that the axis belongs to.
549         * @param plotArea  the area within which the plot (including axes) should 
550         *                  be drawn.
551         * @param edge  the axis location.
552         * @param space  space already reserved.
553         *
554         * @return The space required to draw the axis (including pre-reserved 
555         *         space).
556         */
557        public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 
558                                      Rectangle2D plotArea, RectangleEdge edge, 
559                                      AxisSpace space) {
560            // create a new space object if one wasn't supplied...
561            if (space == null) {
562                space = new AxisSpace();
563            }
564            
565            // if the axis is not visible, no additional space is required...
566            if (!isVisible()) {
567                return space;
568            }
569    
570            // if the axis has a fixed dimension, return it...
571            double dimension = getFixedDimension();
572            if (dimension > 0.0) {
573                space.ensureAtLeast(dimension, edge);
574            }
575            
576            // get the axis label size and update the space object...
577            Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
578            double labelHeight = 0.0;
579            double labelWidth = 0.0;
580            double tickLabelBandsDimension = 0.0;
581            
582            for (int i = 0; i < this.labelInfo.length; i++) {
583                PeriodAxisLabelInfo info = this.labelInfo[i];
584                FontMetrics fm = g2.getFontMetrics(info.getLabelFont());
585                tickLabelBandsDimension 
586                    += info.getPadding().extendHeight(fm.getHeight());
587            }
588            
589            if (RectangleEdge.isTopOrBottom(edge)) {
590                labelHeight = labelEnclosure.getHeight();
591                space.add(labelHeight + tickLabelBandsDimension, edge);
592            }
593            else if (RectangleEdge.isLeftOrRight(edge)) {
594                labelWidth = labelEnclosure.getWidth();
595                space.add(labelWidth + tickLabelBandsDimension, edge);
596            }
597    
598            // add space for the outer tick labels, if any...
599            double tickMarkSpace = 0.0;
600            if (isTickMarksVisible()) {
601                tickMarkSpace = getTickMarkOutsideLength();
602            }
603            if (this.minorTickMarksVisible) {
604                tickMarkSpace = Math.max(tickMarkSpace, 
605                        this.minorTickMarkOutsideLength);
606            }
607            space.add(tickMarkSpace, edge);
608            return space;
609        }
610    
611        /**
612         * Draws the axis on a Java 2D graphics device (such as the screen or a 
613         * printer).
614         *
615         * @param g2  the graphics device (<code>null</code> not permitted).
616         * @param cursor  the cursor location (determines where to draw the axis).
617         * @param plotArea  the area within which the axes and plot should be drawn.
618         * @param dataArea  the area within which the data should be drawn.
619         * @param edge  the axis location (<code>null</code> not permitted).
620         * @param plotState  collects information about the plot 
621         *                   (<code>null</code> permitted).
622         * 
623         * @return The axis state (never <code>null</code>).
624         */
625        public AxisState draw(Graphics2D g2, 
626                              double cursor,
627                              Rectangle2D plotArea, 
628                              Rectangle2D dataArea,
629                              RectangleEdge edge,
630                              PlotRenderingInfo plotState) {
631            
632            AxisState axisState = new AxisState(cursor);
633            if (isAxisLineVisible()) {
634                drawAxisLine(g2, cursor, dataArea, edge);
635            }
636            drawTickMarks(g2, axisState, dataArea, edge);
637            for (int band = 0; band < this.labelInfo.length; band++) {
638                axisState = drawTickLabels(band, g2, axisState, dataArea, edge);
639            }
640            
641            // draw the axis label (note that 'state' is passed in *and* 
642            // returned)...
643            axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge, 
644                    axisState);
645            return axisState;
646            
647        }
648        
649        /**
650         * Draws the tick marks for the axis.
651         * 
652         * @param g2  the graphics device.
653         * @param state  the axis state.
654         * @param dataArea  the data area.
655         * @param edge  the edge.
656         */
657        protected void drawTickMarks(Graphics2D g2, AxisState state, 
658                                     Rectangle2D dataArea, 
659                                     RectangleEdge edge) {
660            if (RectangleEdge.isTopOrBottom(edge)) {
661                drawTickMarksHorizontal(g2, state, dataArea, edge);
662            }
663            else if (RectangleEdge.isLeftOrRight(edge)) {
664                drawTickMarksVertical(g2, state, dataArea, edge);
665            }
666        }
667        
668        /**
669         * Draws the major and minor tick marks for an axis that lies at the top or 
670         * bottom of the plot.
671         * 
672         * @param g2  the graphics device.
673         * @param state  the axis state.
674         * @param dataArea  the data area.
675         * @param edge  the edge.
676         */
677        protected void drawTickMarksHorizontal(Graphics2D g2, AxisState state, 
678                                               Rectangle2D dataArea, 
679                                               RectangleEdge edge) {
680            List ticks = new ArrayList();
681            double x0 = dataArea.getX();
682            double y0 = state.getCursor();
683            double insideLength = getTickMarkInsideLength();
684            double outsideLength = getTickMarkOutsideLength();
685            RegularTimePeriod t = RegularTimePeriod.createInstance(
686                    this.majorTickTimePeriodClass, this.first.getStart(), 
687                    getTimeZone());
688            long t0 = t.getFirstMillisecond(this.calendar);
689            Line2D inside = null;
690            Line2D outside = null;
691            long firstOnAxis = getFirst().getFirstMillisecond(this.calendar);
692            long lastOnAxis = getLast().getLastMillisecond(this.calendar);
693            while (t0 <= lastOnAxis) {
694                ticks.add(new NumberTick(new Double(t0), "", TextAnchor.CENTER, 
695                        TextAnchor.CENTER, 0.0));
696                x0 = valueToJava2D(t0, dataArea, edge);
697                if (edge == RectangleEdge.TOP) {
698                    inside = new Line2D.Double(x0, y0, x0, y0 + insideLength);  
699                    outside = new Line2D.Double(x0, y0, x0, y0 - outsideLength);
700                }
701                else if (edge == RectangleEdge.BOTTOM) {
702                    inside = new Line2D.Double(x0, y0, x0, y0 - insideLength);
703                    outside = new Line2D.Double(x0, y0, x0, y0 + outsideLength);
704                }
705                if (t0 > firstOnAxis) {
706                    g2.setPaint(getTickMarkPaint());
707                    g2.setStroke(getTickMarkStroke());
708                    g2.draw(inside);
709                    g2.draw(outside);
710                }
711                // draw minor tick marks
712                if (this.minorTickMarksVisible) {
713                    RegularTimePeriod tminor = RegularTimePeriod.createInstance(
714                            this.minorTickTimePeriodClass, new Date(t0), 
715                            getTimeZone());
716                    long tt0 = tminor.getFirstMillisecond(this.calendar);
717                    while (tt0 < t.getLastMillisecond(this.calendar) 
718                            && tt0 < lastOnAxis) {
719                        double xx0 = valueToJava2D(tt0, dataArea, edge);
720                        if (edge == RectangleEdge.TOP) {
721                            inside = new Line2D.Double(xx0, y0, xx0, 
722                                    y0 + this.minorTickMarkInsideLength);
723                            outside = new Line2D.Double(xx0, y0, xx0, 
724                                    y0 - this.minorTickMarkOutsideLength);
725                        }
726                        else if (edge == RectangleEdge.BOTTOM) {
727                            inside = new Line2D.Double(xx0, y0, xx0, 
728                                    y0 - this.minorTickMarkInsideLength);
729                            outside = new Line2D.Double(xx0, y0, xx0, 
730                                    y0 + this.minorTickMarkOutsideLength);
731                        }
732                        if (tt0 >= firstOnAxis) {
733                            g2.setPaint(this.minorTickMarkPaint);
734                            g2.setStroke(this.minorTickMarkStroke);
735                            g2.draw(inside);
736                            g2.draw(outside);
737                        }
738                        tminor = tminor.next();
739                        tt0 = tminor.getFirstMillisecond(this.calendar);
740                    }
741                }            
742                t = t.next();
743                t0 = t.getFirstMillisecond(this.calendar);
744            }
745            if (edge == RectangleEdge.TOP) {
746                state.cursorUp(Math.max(outsideLength, 
747                        this.minorTickMarkOutsideLength));
748            }
749            else if (edge == RectangleEdge.BOTTOM) {
750                state.cursorDown(Math.max(outsideLength, 
751                        this.minorTickMarkOutsideLength));
752            }
753            state.setTicks(ticks);
754        }
755        
756        /**
757         * Draws the tick marks for a vertical axis.
758         * 
759         * @param g2  the graphics device.
760         * @param state  the axis state.
761         * @param dataArea  the data area.
762         * @param edge  the edge.
763         */
764        protected void drawTickMarksVertical(Graphics2D g2, AxisState state, 
765                                             Rectangle2D dataArea, 
766                                             RectangleEdge edge) {
767            // FIXME:  implement this...       
768        }
769        
770        /**
771         * Draws the tick labels for one "band" of time periods.
772         * 
773         * @param band  the band index (zero-based).
774         * @param g2  the graphics device.
775         * @param state  the axis state.
776         * @param dataArea  the data area.
777         * @param edge  the edge where the axis is located.
778         * 
779         * @return The updated axis state.
780         */
781        protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state,
782                                           Rectangle2D dataArea, 
783                                           RectangleEdge edge) {
784    
785            // work out the initial gap
786            double delta1 = 0.0;
787            FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont());
788            if (edge == RectangleEdge.BOTTOM) {
789                delta1 = this.labelInfo[band].getPadding().calculateTopOutset(
790                        fm.getHeight());   
791            }
792            else if (edge == RectangleEdge.TOP) {
793                delta1 = this.labelInfo[band].getPadding().calculateBottomOutset(
794                        fm.getHeight());   
795            }
796            state.moveCursor(delta1, edge);
797            long axisMin = this.first.getFirstMillisecond(this.calendar);
798            long axisMax = this.last.getLastMillisecond(this.calendar);
799            g2.setFont(this.labelInfo[band].getLabelFont());
800            g2.setPaint(this.labelInfo[band].getLabelPaint());
801    
802            // work out the number of periods to skip for labelling
803            RegularTimePeriod p1 = this.labelInfo[band].createInstance(
804                    new Date(axisMin), this.timeZone);
805            RegularTimePeriod p2 = this.labelInfo[band].createInstance(
806                    new Date(axisMax), this.timeZone);
807            String label1 = this.labelInfo[band].getDateFormat().format(
808                    new Date(p1.getMiddleMillisecond(this.calendar)));
809            String label2 = this.labelInfo[band].getDateFormat().format(
810                    new Date(p2.getMiddleMillisecond(this.calendar)));
811            Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2, 
812                    g2.getFontMetrics());
813            Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2, 
814                    g2.getFontMetrics());
815            double w = Math.max(b1.getWidth(), b2.getWidth());
816            long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0, 
817                    dataArea, edge));
818            if (isInverted()) {
819                ww = axisMax - ww;
820            }
821            else {
822                ww = ww - axisMin;
823            }
824            long length = p1.getLastMillisecond(this.calendar) 
825                          - p1.getFirstMillisecond(this.calendar);
826            int periods = (int) (ww / length) + 1;
827            
828            RegularTimePeriod p = this.labelInfo[band].createInstance(
829                    new Date(axisMin), this.timeZone);
830            Rectangle2D b = null;
831            long lastXX = 0L;
832            float y = (float) (state.getCursor());
833            TextAnchor anchor = TextAnchor.TOP_CENTER;
834            float yDelta = (float) b1.getHeight();
835            if (edge == RectangleEdge.TOP) {
836                anchor = TextAnchor.BOTTOM_CENTER;
837                yDelta = -yDelta;
838            }
839            while (p.getFirstMillisecond(this.calendar) <= axisMax) {
840                float x = (float) valueToJava2D(p.getMiddleMillisecond(
841                        this.calendar), dataArea, edge);
842                DateFormat df = this.labelInfo[band].getDateFormat();
843                String label = df.format(new Date(p.getMiddleMillisecond(
844                        this.calendar)));
845                long first = p.getFirstMillisecond(this.calendar);
846                long last = p.getLastMillisecond(this.calendar);
847                if (last > axisMax) {
848                    // this is the last period, but it is only partially visible 
849                    // so check that the label will fit before displaying it...
850                    Rectangle2D bb = TextUtilities.getTextBounds(label, g2, 
851                            g2.getFontMetrics());
852                    if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {
853                        float xstart = (float) valueToJava2D(Math.max(first, 
854                                axisMin), dataArea, edge);
855                        if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {
856                            x = ((float) dataArea.getMaxX() + xstart) / 2.0f;   
857                        }
858                        else {
859                            label = null;
860                        }
861                    }
862                }
863                if (first < axisMin) {
864                    // this is the first period, but it is only partially visible 
865                    // so check that the label will fit before displaying it...
866                    Rectangle2D bb = TextUtilities.getTextBounds(label, g2, 
867                            g2.getFontMetrics());
868                    if ((x - bb.getWidth() / 2) < dataArea.getX()) {
869                        float xlast = (float) valueToJava2D(Math.min(last, 
870                                axisMax), dataArea, edge);
871                        if (bb.getWidth() < (xlast - dataArea.getX())) {
872                            x = (xlast + (float) dataArea.getX()) / 2.0f;   
873                        }
874                        else {
875                            label = null;
876                        }
877                    }
878                    
879                }
880                if (label != null) {
881                    g2.setPaint(this.labelInfo[band].getLabelPaint());
882                    b = TextUtilities.drawAlignedString(label, g2, x, y, anchor);
883                }
884                if (lastXX > 0L) {
885                    if (this.labelInfo[band].getDrawDividers()) {
886                        long nextXX = p.getFirstMillisecond(this.calendar);
887                        long mid = (lastXX + nextXX) / 2;
888                        float mid2d = (float) valueToJava2D(mid, dataArea, edge);
889                        g2.setStroke(this.labelInfo[band].getDividerStroke());
890                        g2.setPaint(this.labelInfo[band].getDividerPaint());
891                        g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta));
892                    }
893                }
894                lastXX = last;
895                for (int i = 0; i < periods; i++) {
896                    p = p.next();   
897                }
898            }
899            double used = 0.0;
900            if (b != null) {
901                used = b.getHeight();
902                // work out the trailing gap
903                if (edge == RectangleEdge.BOTTOM) {
904                    used += this.labelInfo[band].getPadding().calculateBottomOutset(
905                            fm.getHeight());   
906                }
907                else if (edge == RectangleEdge.TOP) {
908                    used += this.labelInfo[band].getPadding().calculateTopOutset(
909                            fm.getHeight());   
910                }
911            }
912            state.moveCursor(used, edge);        
913            return state;    
914        }
915    
916        /**
917         * Calculates the positions of the ticks for the axis, storing the results
918         * in the tick list (ready for drawing).
919         *
920         * @param g2  the graphics device.
921         * @param state  the axis state.
922         * @param dataArea  the area inside the axes.
923         * @param edge  the edge on which the axis is located.
924         * 
925         * @return The list of ticks.
926         */
927        public List refreshTicks(Graphics2D g2, 
928                                 AxisState state,
929                                 Rectangle2D dataArea,
930                                 RectangleEdge edge) {
931            return Collections.EMPTY_LIST;
932        }
933        
934        /**
935         * Converts a data value to a coordinate in Java2D space, assuming that the
936         * axis runs along one edge of the specified dataArea.
937         * <p>
938         * Note that it is possible for the coordinate to fall outside the area.
939         *
940         * @param value  the data value.
941         * @param area  the area for plotting the data.
942         * @param edge  the edge along which the axis lies.
943         *
944         * @return The Java2D coordinate.
945         */
946        public double valueToJava2D(double value,
947                                    Rectangle2D area,
948                                    RectangleEdge edge) {
949            
950            double result = Double.NaN;
951            double axisMin = this.first.getFirstMillisecond(this.calendar);
952            double axisMax = this.last.getLastMillisecond(this.calendar);
953            if (RectangleEdge.isTopOrBottom(edge)) {
954                double minX = area.getX();
955                double maxX = area.getMaxX();
956                if (isInverted()) {
957                    result = maxX + ((value - axisMin) / (axisMax - axisMin)) 
958                             * (minX - maxX);
959                }
960                else {
961                    result = minX + ((value - axisMin) / (axisMax - axisMin)) 
962                             * (maxX - minX);
963                }
964            }
965            else if (RectangleEdge.isLeftOrRight(edge)) {
966                double minY = area.getMinY();
967                double maxY = area.getMaxY();
968                if (isInverted()) {
969                    result = minY + (((value - axisMin) / (axisMax - axisMin)) 
970                             * (maxY - minY));
971                }
972                else {
973                    result = maxY - (((value - axisMin) / (axisMax - axisMin)) 
974                             * (maxY - minY));
975                }
976            }
977            return result;
978            
979        }
980    
981        /**
982         * Converts a coordinate in Java2D space to the corresponding data value,
983         * assuming that the axis runs along one edge of the specified dataArea.
984         *
985         * @param java2DValue  the coordinate in Java2D space.
986         * @param area  the area in which the data is plotted.
987         * @param edge  the edge along which the axis lies.
988         *
989         * @return The data value.
990         */
991        public double java2DToValue(double java2DValue,
992                                    Rectangle2D area,
993                                    RectangleEdge edge) {
994    
995            double result = Double.NaN;
996            double min = 0.0;
997            double max = 0.0;
998            double axisMin = this.first.getFirstMillisecond(this.calendar);
999            double axisMax = this.last.getLastMillisecond(this.calendar);
1000            if (RectangleEdge.isTopOrBottom(edge)) {
1001                min = area.getX();
1002                max = area.getMaxX();
1003            }
1004            else if (RectangleEdge.isLeftOrRight(edge)) {
1005                min = area.getMaxY();
1006                max = area.getY();
1007            }
1008            if (isInverted()) {
1009                 result = axisMax - ((java2DValue - min) / (max - min) 
1010                          * (axisMax - axisMin));
1011            }
1012            else {
1013                 result = axisMin + ((java2DValue - min) / (max - min) 
1014                          * (axisMax - axisMin));
1015            }
1016            return result;
1017        }
1018    
1019        /**
1020         * Rescales the axis to ensure that all data is visible.
1021         */
1022        protected void autoAdjustRange() {
1023    
1024            Plot plot = getPlot();
1025            if (plot == null) {
1026                return;  // no plot, no data
1027            }
1028    
1029            if (plot instanceof ValueAxisPlot) {
1030                ValueAxisPlot vap = (ValueAxisPlot) plot;
1031    
1032                Range r = vap.getDataRange(this);
1033                if (r == null) {
1034                    r = getDefaultAutoRange();
1035                }
1036                
1037                long upper = Math.round(r.getUpperBound());
1038                long lower = Math.round(r.getLowerBound());
1039                this.first = createInstance(this.autoRangeTimePeriodClass, 
1040                        new Date(lower), this.timeZone);
1041                this.last = createInstance(this.autoRangeTimePeriodClass, 
1042                        new Date(upper), this.timeZone);
1043                setRange(r, false, false);
1044            }
1045    
1046        }
1047        
1048        /**
1049         * Tests the axis for equality with an arbitrary object.
1050         * 
1051         * @param obj  the object (<code>null</code> permitted).
1052         * 
1053         * @return A boolean.
1054         */
1055        public boolean equals(Object obj) {
1056            if (obj == this) {
1057                return true;   
1058            }
1059            if (obj instanceof PeriodAxis && super.equals(obj)) {
1060                PeriodAxis that = (PeriodAxis) obj;
1061                if (!this.first.equals(that.first)) {
1062                    return false;   
1063                }
1064                if (!this.last.equals(that.last)) {
1065                    return false;   
1066                }
1067                if (!this.timeZone.equals(that.timeZone)) {
1068                    return false;   
1069                }
1070                if (!this.autoRangeTimePeriodClass.equals(
1071                        that.autoRangeTimePeriodClass)) {
1072                    return false;   
1073                }
1074                if (!(isMinorTickMarksVisible() 
1075                        == that.isMinorTickMarksVisible())) {
1076                    return false;
1077                }
1078                if (!this.majorTickTimePeriodClass.equals(
1079                        that.majorTickTimePeriodClass)) {
1080                    return false;
1081                }
1082                if (!this.minorTickTimePeriodClass.equals(
1083                        that.minorTickTimePeriodClass)) {
1084                    return false;
1085                }
1086                if (!this.minorTickMarkPaint.equals(that.minorTickMarkPaint)) {
1087                    return false;
1088                }
1089                if (!this.minorTickMarkStroke.equals(that.minorTickMarkStroke)) {
1090                    return false;
1091                }
1092                if (!Arrays.equals(this.labelInfo, that.labelInfo)) {
1093                    return false;   
1094                }
1095                return true;   
1096            }
1097            return false;
1098        }
1099    
1100        /**
1101         * Returns a hash code for this object.
1102         * 
1103         * @return A hash code.
1104         */
1105        public int hashCode() {
1106            if (getLabel() != null) {
1107                return getLabel().hashCode();
1108            }
1109            else {
1110                return 0;
1111            }
1112        }
1113        
1114        /**
1115         * Returns a clone of the axis.
1116         * 
1117         * @return A clone.
1118         * 
1119         * @throws CloneNotSupportedException  this class is cloneable, but 
1120         *         subclasses may not be.
1121         */
1122        public Object clone() throws CloneNotSupportedException {
1123            PeriodAxis clone = (PeriodAxis) super.clone();
1124            clone.timeZone = (TimeZone) this.timeZone.clone();
1125            clone.labelInfo = new PeriodAxisLabelInfo[this.labelInfo.length];
1126            for (int i = 0; i < this.labelInfo.length; i++) {
1127                clone.labelInfo[i] = this.labelInfo[i];  // copy across references 
1128                                                         // to immutable objs 
1129            }
1130            return clone;
1131        }
1132        
1133        /**
1134         * A utility method used to create a particular subclass of the 
1135         * {@link RegularTimePeriod} class that includes the specified millisecond, 
1136         * assuming the specified time zone.
1137         * 
1138         * @param periodClass  the class.
1139         * @param millisecond  the time.
1140         * @param zone  the time zone.
1141         * 
1142         * @return The time period.
1143         */
1144        private RegularTimePeriod createInstance(Class periodClass, 
1145                                                 Date millisecond, TimeZone zone) {
1146            RegularTimePeriod result = null;
1147            try {
1148                Constructor c = periodClass.getDeclaredConstructor(new Class[] {
1149                        Date.class, TimeZone.class});
1150                result = (RegularTimePeriod) c.newInstance(new Object[] {
1151                        millisecond, zone});   
1152            }
1153            catch (Exception e) {
1154                // do nothing            
1155            }
1156            return result;
1157        }
1158        
1159        /**
1160         * Provides serialization support.
1161         *
1162         * @param stream  the output stream.
1163         *
1164         * @throws IOException  if there is an I/O error.
1165         */
1166        private void writeObject(ObjectOutputStream stream) throws IOException {
1167            stream.defaultWriteObject();
1168            SerialUtilities.writeStroke(this.minorTickMarkStroke, stream);
1169            SerialUtilities.writePaint(this.minorTickMarkPaint, stream);
1170        }
1171    
1172        /**
1173         * Provides serialization support.
1174         *
1175         * @param stream  the input stream.
1176         *
1177         * @throws IOException  if there is an I/O error.
1178         * @throws ClassNotFoundException  if there is a classpath problem.
1179         */
1180        private void readObject(ObjectInputStream stream) 
1181            throws IOException, ClassNotFoundException {
1182            stream.defaultReadObject();
1183            this.minorTickMarkStroke = SerialUtilities.readStroke(stream);
1184            this.minorTickMarkPaint = SerialUtilities.readPaint(stream);
1185        }
1186    
1187    }