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