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 }