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