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