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