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