0001: /* ===========================================================
0002: * JFreeChart : a free chart library for the Java(tm) platform
0003: * ===========================================================
0004: *
0005: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
0006: *
0007: * Project Info: http://www.jfree.org/jfreechart/index.html
0008: *
0009: * This library is free software; you can redistribute it and/or modify it
0010: * under the terms of the GNU Lesser General Public License as published by
0011: * the Free Software Foundation; either version 2.1 of the License, or
0012: * (at your option) any later version.
0013: *
0014: * This library is distributed in the hope that it will be useful, but
0015: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
0016: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
0017: * License for more details.
0018: *
0019: * You should have received a copy of the GNU Lesser General Public
0020: * License along with this library; if not, write to the Free Software
0021: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
0022: * USA.
0023: *
0024: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
0025: * in the United States and other countries.]
0026: *
0027: * ---------------
0028: * PeriodAxis.java
0029: * ---------------
0030: * (C) Copyright 2004-2007, by Object Refinery Limited and Contributors.
0031: *
0032: * Original Author: David Gilbert (for Object Refinery Limited);
0033: * Contributor(s): -;
0034: *
0035: * $Id: PeriodAxis.java,v 1.16.2.7 2007/03/22 12:13:27 mungady Exp $
0036: *
0037: * Changes
0038: * -------
0039: * 01-Jun-2004 : Version 1 (DG);
0040: * 16-Sep-2004 : Fixed bug in equals() method, added clone() method and
0041: * PublicCloneable interface (DG);
0042: * 25-Nov-2004 : Updates to support major and minor tick marks (DG);
0043: * 25-Feb-2005 : Fixed some tick mark bugs (DG);
0044: * 15-Apr-2005 : Fixed some more tick mark bugs (DG);
0045: * 26-Apr-2005 : Removed LOGGER (DG);
0046: * 16-Jun-2005 : Fixed zooming (DG);
0047: * 15-Sep-2005 : Changed configure() method to check autoRange flag,
0048: * and added ticks to state (DG);
0049: * ------------- JFREECHART 1.0.x ---------------------------------------------
0050: * 06-Oct-2006 : Updated for deprecations in RegularTimePeriod and
0051: * subclasses (DG);
0052: * 22-Mar-2007 : Use new defaultAutoRange attribute (DG);
0053: *
0054: */
0055:
0056: package org.jfree.chart.axis;
0057:
0058: import java.awt.BasicStroke;
0059: import java.awt.Color;
0060: import java.awt.FontMetrics;
0061: import java.awt.Graphics2D;
0062: import java.awt.Paint;
0063: import java.awt.Stroke;
0064: import java.awt.geom.Line2D;
0065: import java.awt.geom.Rectangle2D;
0066: import java.io.IOException;
0067: import java.io.ObjectInputStream;
0068: import java.io.ObjectOutputStream;
0069: import java.io.Serializable;
0070: import java.lang.reflect.Constructor;
0071: import java.text.DateFormat;
0072: import java.text.SimpleDateFormat;
0073: import java.util.ArrayList;
0074: import java.util.Arrays;
0075: import java.util.Calendar;
0076: import java.util.Collections;
0077: import java.util.Date;
0078: import java.util.List;
0079: import java.util.TimeZone;
0080:
0081: import org.jfree.chart.event.AxisChangeEvent;
0082: import org.jfree.chart.plot.Plot;
0083: import org.jfree.chart.plot.PlotRenderingInfo;
0084: import org.jfree.chart.plot.ValueAxisPlot;
0085: import org.jfree.data.Range;
0086: import org.jfree.data.time.Day;
0087: import org.jfree.data.time.Month;
0088: import org.jfree.data.time.RegularTimePeriod;
0089: import org.jfree.data.time.Year;
0090: import org.jfree.io.SerialUtilities;
0091: import org.jfree.text.TextUtilities;
0092: import org.jfree.ui.RectangleEdge;
0093: import org.jfree.ui.TextAnchor;
0094: import org.jfree.util.PublicCloneable;
0095:
0096: /**
0097: * An axis that displays a date scale based on a
0098: * {@link org.jfree.data.time.RegularTimePeriod}. This axis works when
0099: * displayed across the bottom or top of a plot, but is broken for display at
0100: * the left or right of charts.
0101: */
0102: public class PeriodAxis extends ValueAxis implements Cloneable,
0103: PublicCloneable, Serializable {
0104:
0105: /** For serialization. */
0106: private static final long serialVersionUID = 8353295532075872069L;
0107:
0108: /** The first time period in the overall range. */
0109: private RegularTimePeriod first;
0110:
0111: /** The last time period in the overall range. */
0112: private RegularTimePeriod last;
0113:
0114: /**
0115: * The time zone used to convert 'first' and 'last' to absolute
0116: * milliseconds.
0117: */
0118: private TimeZone timeZone;
0119:
0120: /**
0121: * A calendar used for date manipulations in the current time zone.
0122: */
0123: private Calendar calendar;
0124:
0125: /**
0126: * The {@link RegularTimePeriod} subclass used to automatically determine
0127: * the axis range.
0128: */
0129: private Class autoRangeTimePeriodClass;
0130:
0131: /**
0132: * Indicates the {@link RegularTimePeriod} subclass that is used to
0133: * determine the spacing of the major tick marks.
0134: */
0135: private Class majorTickTimePeriodClass;
0136:
0137: /**
0138: * A flag that indicates whether or not tick marks are visible for the
0139: * axis.
0140: */
0141: private boolean minorTickMarksVisible;
0142:
0143: /**
0144: * Indicates the {@link RegularTimePeriod} subclass that is used to
0145: * determine the spacing of the minor tick marks.
0146: */
0147: private Class minorTickTimePeriodClass;
0148:
0149: /** The length of the tick mark inside the data area (zero permitted). */
0150: private float minorTickMarkInsideLength = 0.0f;
0151:
0152: /** The length of the tick mark outside the data area (zero permitted). */
0153: private float minorTickMarkOutsideLength = 2.0f;
0154:
0155: /** The stroke used to draw tick marks. */
0156: private transient Stroke minorTickMarkStroke = new BasicStroke(0.5f);
0157:
0158: /** The paint used to draw tick marks. */
0159: private transient Paint minorTickMarkPaint = Color.black;
0160:
0161: /** Info for each labelling band. */
0162: private PeriodAxisLabelInfo[] labelInfo;
0163:
0164: /**
0165: * Creates a new axis.
0166: *
0167: * @param label the axis label.
0168: */
0169: public PeriodAxis(String label) {
0170: this (label, new Day(), new Day());
0171: }
0172:
0173: /**
0174: * Creates a new axis.
0175: *
0176: * @param label the axis label (<code>null</code> permitted).
0177: * @param first the first time period in the axis range
0178: * (<code>null</code> not permitted).
0179: * @param last the last time period in the axis range
0180: * (<code>null</code> not permitted).
0181: */
0182: public PeriodAxis(String label, RegularTimePeriod first,
0183: RegularTimePeriod last) {
0184: this (label, first, last, TimeZone.getDefault());
0185: }
0186:
0187: /**
0188: * Creates a new axis.
0189: *
0190: * @param label the axis label (<code>null</code> permitted).
0191: * @param first the first time period in the axis range
0192: * (<code>null</code> not permitted).
0193: * @param last the last time period in the axis range
0194: * (<code>null</code> not permitted).
0195: * @param timeZone the time zone (<code>null</code> not permitted).
0196: */
0197: public PeriodAxis(String label, RegularTimePeriod first,
0198: RegularTimePeriod last, TimeZone timeZone) {
0199:
0200: super (label, null);
0201: this .first = first;
0202: this .last = last;
0203: this .timeZone = timeZone;
0204: this .calendar = Calendar.getInstance(timeZone);
0205: this .autoRangeTimePeriodClass = first.getClass();
0206: this .majorTickTimePeriodClass = first.getClass();
0207: this .minorTickMarksVisible = false;
0208: this .minorTickTimePeriodClass = RegularTimePeriod
0209: .downsize(this .majorTickTimePeriodClass);
0210: setAutoRange(true);
0211: this .labelInfo = new PeriodAxisLabelInfo[2];
0212: this .labelInfo[0] = new PeriodAxisLabelInfo(Month.class,
0213: new SimpleDateFormat("MMM"));
0214: this .labelInfo[1] = new PeriodAxisLabelInfo(Year.class,
0215: new SimpleDateFormat("yyyy"));
0216:
0217: }
0218:
0219: /**
0220: * Returns the first time period in the axis range.
0221: *
0222: * @return The first time period (never <code>null</code>).
0223: */
0224: public RegularTimePeriod getFirst() {
0225: return this .first;
0226: }
0227:
0228: /**
0229: * Sets the first time period in the axis range and sends an
0230: * {@link AxisChangeEvent} to all registered listeners.
0231: *
0232: * @param first the time period (<code>null</code> not permitted).
0233: */
0234: public void setFirst(RegularTimePeriod first) {
0235: if (first == null) {
0236: throw new IllegalArgumentException("Null 'first' argument.");
0237: }
0238: this .first = first;
0239: notifyListeners(new AxisChangeEvent(this ));
0240: }
0241:
0242: /**
0243: * Returns the last time period in the axis range.
0244: *
0245: * @return The last time period (never <code>null</code>).
0246: */
0247: public RegularTimePeriod getLast() {
0248: return this .last;
0249: }
0250:
0251: /**
0252: * Sets the last time period in the axis range and sends an
0253: * {@link AxisChangeEvent} to all registered listeners.
0254: *
0255: * @param last the time period (<code>null</code> not permitted).
0256: */
0257: public void setLast(RegularTimePeriod last) {
0258: if (last == null) {
0259: throw new IllegalArgumentException("Null 'last' argument.");
0260: }
0261: this .last = last;
0262: notifyListeners(new AxisChangeEvent(this ));
0263: }
0264:
0265: /**
0266: * Returns the time zone used to convert the periods defining the axis
0267: * range into absolute milliseconds.
0268: *
0269: * @return The time zone (never <code>null</code>).
0270: */
0271: public TimeZone getTimeZone() {
0272: return this .timeZone;
0273: }
0274:
0275: /**
0276: * Sets the time zone that is used to convert the time periods into
0277: * absolute milliseconds.
0278: *
0279: * @param zone the time zone (<code>null</code> not permitted).
0280: */
0281: public void setTimeZone(TimeZone zone) {
0282: if (zone == null) {
0283: throw new IllegalArgumentException("Null 'zone' argument.");
0284: }
0285: this .timeZone = zone;
0286: this .calendar = Calendar.getInstance(zone);
0287: notifyListeners(new AxisChangeEvent(this ));
0288: }
0289:
0290: /**
0291: * Returns the class used to create the first and last time periods for
0292: * the axis range when the auto-range flag is set to <code>true</code>.
0293: *
0294: * @return The class (never <code>null</code>).
0295: */
0296: public Class getAutoRangeTimePeriodClass() {
0297: return this .autoRangeTimePeriodClass;
0298: }
0299:
0300: /**
0301: * Sets the class used to create the first and last time periods for the
0302: * axis range when the auto-range flag is set to <code>true</code> and
0303: * sends an {@link AxisChangeEvent} to all registered listeners.
0304: *
0305: * @param c the class (<code>null</code> not permitted).
0306: */
0307: public void setAutoRangeTimePeriodClass(Class c) {
0308: if (c == null) {
0309: throw new IllegalArgumentException("Null 'c' argument.");
0310: }
0311: this .autoRangeTimePeriodClass = c;
0312: notifyListeners(new AxisChangeEvent(this ));
0313: }
0314:
0315: /**
0316: * Returns the class that controls the spacing of the major tick marks.
0317: *
0318: * @return The class (never <code>null</code>).
0319: */
0320: public Class getMajorTickTimePeriodClass() {
0321: return this .majorTickTimePeriodClass;
0322: }
0323:
0324: /**
0325: * Sets the class that controls the spacing of the major tick marks, and
0326: * sends an {@link AxisChangeEvent} to all registered listeners.
0327: *
0328: * @param c the class (a subclass of {@link RegularTimePeriod} is
0329: * expected).
0330: */
0331: public void setMajorTickTimePeriodClass(Class c) {
0332: if (c == null) {
0333: throw new IllegalArgumentException("Null 'c' argument.");
0334: }
0335: this .majorTickTimePeriodClass = c;
0336: notifyListeners(new AxisChangeEvent(this ));
0337: }
0338:
0339: /**
0340: * Returns the flag that controls whether or not minor tick marks
0341: * are displayed for the axis.
0342: *
0343: * @return A boolean.
0344: */
0345: public boolean isMinorTickMarksVisible() {
0346: return this .minorTickMarksVisible;
0347: }
0348:
0349: /**
0350: * Sets the flag that controls whether or not minor tick marks
0351: * are displayed for the axis, and sends a {@link AxisChangeEvent}
0352: * to all registered listeners.
0353: *
0354: * @param visible the flag.
0355: */
0356: public void setMinorTickMarksVisible(boolean visible) {
0357: this .minorTickMarksVisible = visible;
0358: notifyListeners(new AxisChangeEvent(this ));
0359: }
0360:
0361: /**
0362: * Returns the class that controls the spacing of the minor tick marks.
0363: *
0364: * @return The class (never <code>null</code>).
0365: */
0366: public Class getMinorTickTimePeriodClass() {
0367: return this .minorTickTimePeriodClass;
0368: }
0369:
0370: /**
0371: * Sets the class that controls the spacing of the minor tick marks, and
0372: * sends an {@link AxisChangeEvent} to all registered listeners.
0373: *
0374: * @param c the class (a subclass of {@link RegularTimePeriod} is
0375: * expected).
0376: */
0377: public void setMinorTickTimePeriodClass(Class c) {
0378: if (c == null) {
0379: throw new IllegalArgumentException("Null 'c' argument.");
0380: }
0381: this .minorTickTimePeriodClass = c;
0382: notifyListeners(new AxisChangeEvent(this ));
0383: }
0384:
0385: /**
0386: * Returns the stroke used to display minor tick marks, if they are
0387: * visible.
0388: *
0389: * @return A stroke (never <code>null</code>).
0390: */
0391: public Stroke getMinorTickMarkStroke() {
0392: return this .minorTickMarkStroke;
0393: }
0394:
0395: /**
0396: * Sets the stroke used to display minor tick marks, if they are
0397: * visible, and sends a {@link AxisChangeEvent} to all registered
0398: * listeners.
0399: *
0400: * @param stroke the stroke (<code>null</code> not permitted).
0401: */
0402: public void setMinorTickMarkStroke(Stroke stroke) {
0403: if (stroke == null) {
0404: throw new IllegalArgumentException(
0405: "Null 'stroke' argument.");
0406: }
0407: this .minorTickMarkStroke = stroke;
0408: notifyListeners(new AxisChangeEvent(this ));
0409: }
0410:
0411: /**
0412: * Returns the paint used to display minor tick marks, if they are
0413: * visible.
0414: *
0415: * @return A paint (never <code>null</code>).
0416: */
0417: public Paint getMinorTickMarkPaint() {
0418: return this .minorTickMarkPaint;
0419: }
0420:
0421: /**
0422: * Sets the paint used to display minor tick marks, if they are
0423: * visible, and sends a {@link AxisChangeEvent} to all registered
0424: * listeners.
0425: *
0426: * @param paint the paint (<code>null</code> not permitted).
0427: */
0428: public void setMinorTickMarkPaint(Paint paint) {
0429: if (paint == null) {
0430: throw new IllegalArgumentException("Null 'paint' argument.");
0431: }
0432: this .minorTickMarkPaint = paint;
0433: notifyListeners(new AxisChangeEvent(this ));
0434: }
0435:
0436: /**
0437: * Returns the inside length for the minor tick marks.
0438: *
0439: * @return The length.
0440: */
0441: public float getMinorTickMarkInsideLength() {
0442: return this .minorTickMarkInsideLength;
0443: }
0444:
0445: /**
0446: * Sets the inside length of the minor tick marks and sends an
0447: * {@link AxisChangeEvent} to all registered listeners.
0448: *
0449: * @param length the length.
0450: */
0451: public void setMinorTickMarkInsideLength(float length) {
0452: this .minorTickMarkInsideLength = length;
0453: notifyListeners(new AxisChangeEvent(this ));
0454: }
0455:
0456: /**
0457: * Returns the outside length for the minor tick marks.
0458: *
0459: * @return The length.
0460: */
0461: public float getMinorTickMarkOutsideLength() {
0462: return this .minorTickMarkOutsideLength;
0463: }
0464:
0465: /**
0466: * Sets the outside length of the minor tick marks and sends an
0467: * {@link AxisChangeEvent} to all registered listeners.
0468: *
0469: * @param length the length.
0470: */
0471: public void setMinorTickMarkOutsideLength(float length) {
0472: this .minorTickMarkOutsideLength = length;
0473: notifyListeners(new AxisChangeEvent(this ));
0474: }
0475:
0476: /**
0477: * Returns an array of label info records.
0478: *
0479: * @return An array.
0480: */
0481: public PeriodAxisLabelInfo[] getLabelInfo() {
0482: return this .labelInfo;
0483: }
0484:
0485: /**
0486: * Sets the array of label info records.
0487: *
0488: * @param info the info.
0489: */
0490: public void setLabelInfo(PeriodAxisLabelInfo[] info) {
0491: this .labelInfo = info;
0492: // FIXME: shouldn't this generate an event?
0493: }
0494:
0495: /**
0496: * Returns the range for the axis.
0497: *
0498: * @return The axis range (never <code>null</code>).
0499: */
0500: public Range getRange() {
0501: // TODO: find a cleaner way to do this...
0502: return new Range(this .first.getFirstMillisecond(this .calendar),
0503: this .last.getLastMillisecond(this .calendar));
0504: }
0505:
0506: /**
0507: * Sets the range for the axis, if requested, sends an
0508: * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
0509: * the auto-range flag is set to <code>false</code> (optional).
0510: *
0511: * @param range the range (<code>null</code> not permitted).
0512: * @param turnOffAutoRange a flag that controls whether or not the auto
0513: * range is turned off.
0514: * @param notify a flag that controls whether or not listeners are
0515: * notified.
0516: */
0517: public void setRange(Range range, boolean turnOffAutoRange,
0518: boolean notify) {
0519: super .setRange(range, turnOffAutoRange, false);
0520: long upper = Math.round(range.getUpperBound());
0521: long lower = Math.round(range.getLowerBound());
0522: this .first = createInstance(this .autoRangeTimePeriodClass,
0523: new Date(lower), this .timeZone);
0524: this .last = createInstance(this .autoRangeTimePeriodClass,
0525: new Date(upper), this .timeZone);
0526: }
0527:
0528: /**
0529: * Configures the axis to work with the current plot. Override this method
0530: * to perform any special processing (such as auto-rescaling).
0531: */
0532: public void configure() {
0533: if (this .isAutoRange()) {
0534: autoAdjustRange();
0535: }
0536: }
0537:
0538: /**
0539: * Estimates the space (height or width) required to draw the axis.
0540: *
0541: * @param g2 the graphics device.
0542: * @param plot the plot that the axis belongs to.
0543: * @param plotArea the area within which the plot (including axes) should
0544: * be drawn.
0545: * @param edge the axis location.
0546: * @param space space already reserved.
0547: *
0548: * @return The space required to draw the axis (including pre-reserved
0549: * space).
0550: */
0551: public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
0552: Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
0553: // create a new space object if one wasn't supplied...
0554: if (space == null) {
0555: space = new AxisSpace();
0556: }
0557:
0558: // if the axis is not visible, no additional space is required...
0559: if (!isVisible()) {
0560: return space;
0561: }
0562:
0563: // if the axis has a fixed dimension, return it...
0564: double dimension = getFixedDimension();
0565: if (dimension > 0.0) {
0566: space.ensureAtLeast(dimension, edge);
0567: }
0568:
0569: // get the axis label size and update the space object...
0570: Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
0571: double labelHeight = 0.0;
0572: double labelWidth = 0.0;
0573: double tickLabelBandsDimension = 0.0;
0574:
0575: for (int i = 0; i < this .labelInfo.length; i++) {
0576: PeriodAxisLabelInfo info = this .labelInfo[i];
0577: FontMetrics fm = g2.getFontMetrics(info.getLabelFont());
0578: tickLabelBandsDimension += info.getPadding().extendHeight(
0579: fm.getHeight());
0580: }
0581:
0582: if (RectangleEdge.isTopOrBottom(edge)) {
0583: labelHeight = labelEnclosure.getHeight();
0584: space.add(labelHeight + tickLabelBandsDimension, edge);
0585: } else if (RectangleEdge.isLeftOrRight(edge)) {
0586: labelWidth = labelEnclosure.getWidth();
0587: space.add(labelWidth + tickLabelBandsDimension, edge);
0588: }
0589:
0590: // add space for the outer tick labels, if any...
0591: double tickMarkSpace = 0.0;
0592: if (isTickMarksVisible()) {
0593: tickMarkSpace = getTickMarkOutsideLength();
0594: }
0595: if (this .minorTickMarksVisible) {
0596: tickMarkSpace = Math.max(tickMarkSpace,
0597: this .minorTickMarkOutsideLength);
0598: }
0599: space.add(tickMarkSpace, edge);
0600: return space;
0601: }
0602:
0603: /**
0604: * Draws the axis on a Java 2D graphics device (such as the screen or a
0605: * printer).
0606: *
0607: * @param g2 the graphics device (<code>null</code> not permitted).
0608: * @param cursor the cursor location (determines where to draw the axis).
0609: * @param plotArea the area within which the axes and plot should be drawn.
0610: * @param dataArea the area within which the data should be drawn.
0611: * @param edge the axis location (<code>null</code> not permitted).
0612: * @param plotState collects information about the plot
0613: * (<code>null</code> permitted).
0614: *
0615: * @return The axis state (never <code>null</code>).
0616: */
0617: public AxisState draw(Graphics2D g2, double cursor,
0618: Rectangle2D plotArea, Rectangle2D dataArea,
0619: RectangleEdge edge, PlotRenderingInfo plotState) {
0620:
0621: AxisState axisState = new AxisState(cursor);
0622: if (isAxisLineVisible()) {
0623: drawAxisLine(g2, cursor, dataArea, edge);
0624: }
0625: drawTickMarks(g2, axisState, dataArea, edge);
0626: for (int band = 0; band < this .labelInfo.length; band++) {
0627: axisState = drawTickLabels(band, g2, axisState, dataArea,
0628: edge);
0629: }
0630:
0631: // draw the axis label (note that 'state' is passed in *and*
0632: // returned)...
0633: axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge,
0634: axisState);
0635: return axisState;
0636:
0637: }
0638:
0639: /**
0640: * Draws the tick marks for the axis.
0641: *
0642: * @param g2 the graphics device.
0643: * @param state the axis state.
0644: * @param dataArea the data area.
0645: * @param edge the edge.
0646: */
0647: protected void drawTickMarks(Graphics2D g2, AxisState state,
0648: Rectangle2D dataArea, RectangleEdge edge) {
0649: if (RectangleEdge.isTopOrBottom(edge)) {
0650: drawTickMarksHorizontal(g2, state, dataArea, edge);
0651: } else if (RectangleEdge.isLeftOrRight(edge)) {
0652: drawTickMarksVertical(g2, state, dataArea, edge);
0653: }
0654: }
0655:
0656: /**
0657: * Draws the major and minor tick marks for an axis that lies at the top or
0658: * bottom of the plot.
0659: *
0660: * @param g2 the graphics device.
0661: * @param state the axis state.
0662: * @param dataArea the data area.
0663: * @param edge the edge.
0664: */
0665: protected void drawTickMarksHorizontal(Graphics2D g2,
0666: AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
0667: List ticks = new ArrayList();
0668: double x0 = dataArea.getX();
0669: double y0 = state.getCursor();
0670: double insideLength = getTickMarkInsideLength();
0671: double outsideLength = getTickMarkOutsideLength();
0672: RegularTimePeriod t = RegularTimePeriod.createInstance(
0673: this .majorTickTimePeriodClass, this .first.getStart(),
0674: getTimeZone());
0675: long t0 = t.getFirstMillisecond(this .calendar);
0676: Line2D inside = null;
0677: Line2D outside = null;
0678: long firstOnAxis = getFirst()
0679: .getFirstMillisecond(this .calendar);
0680: long lastOnAxis = getLast().getLastMillisecond(this .calendar);
0681: while (t0 <= lastOnAxis) {
0682: ticks.add(new NumberTick(new Double(t0), "",
0683: TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
0684: x0 = valueToJava2D(t0, dataArea, edge);
0685: if (edge == RectangleEdge.TOP) {
0686: inside = new Line2D.Double(x0, y0, x0, y0
0687: + insideLength);
0688: outside = new Line2D.Double(x0, y0, x0, y0
0689: - outsideLength);
0690: } else if (edge == RectangleEdge.BOTTOM) {
0691: inside = new Line2D.Double(x0, y0, x0, y0
0692: - insideLength);
0693: outside = new Line2D.Double(x0, y0, x0, y0
0694: + outsideLength);
0695: }
0696: if (t0 > firstOnAxis) {
0697: g2.setPaint(getTickMarkPaint());
0698: g2.setStroke(getTickMarkStroke());
0699: g2.draw(inside);
0700: g2.draw(outside);
0701: }
0702: // draw minor tick marks
0703: if (this .minorTickMarksVisible) {
0704: RegularTimePeriod tminor = RegularTimePeriod
0705: .createInstance(this .minorTickTimePeriodClass,
0706: new Date(t0), getTimeZone());
0707: long tt0 = tminor.getFirstMillisecond(this .calendar);
0708: while (tt0 < t.getLastMillisecond(this .calendar)
0709: && tt0 < lastOnAxis) {
0710: double xx0 = valueToJava2D(tt0, dataArea, edge);
0711: if (edge == RectangleEdge.TOP) {
0712: inside = new Line2D.Double(xx0, y0, xx0, y0
0713: + this .minorTickMarkInsideLength);
0714: outside = new Line2D.Double(xx0, y0, xx0, y0
0715: - this .minorTickMarkOutsideLength);
0716: } else if (edge == RectangleEdge.BOTTOM) {
0717: inside = new Line2D.Double(xx0, y0, xx0, y0
0718: - this .minorTickMarkInsideLength);
0719: outside = new Line2D.Double(xx0, y0, xx0, y0
0720: + this .minorTickMarkOutsideLength);
0721: }
0722: if (tt0 >= firstOnAxis) {
0723: g2.setPaint(this .minorTickMarkPaint);
0724: g2.setStroke(this .minorTickMarkStroke);
0725: g2.draw(inside);
0726: g2.draw(outside);
0727: }
0728: tminor = tminor.next();
0729: tt0 = tminor.getFirstMillisecond(this .calendar);
0730: }
0731: }
0732: t = t.next();
0733: t0 = t.getFirstMillisecond(this .calendar);
0734: }
0735: if (edge == RectangleEdge.TOP) {
0736: state.cursorUp(Math.max(outsideLength,
0737: this .minorTickMarkOutsideLength));
0738: } else if (edge == RectangleEdge.BOTTOM) {
0739: state.cursorDown(Math.max(outsideLength,
0740: this .minorTickMarkOutsideLength));
0741: }
0742: state.setTicks(ticks);
0743: }
0744:
0745: /**
0746: * Draws the tick marks for a vertical axis.
0747: *
0748: * @param g2 the graphics device.
0749: * @param state the axis state.
0750: * @param dataArea the data area.
0751: * @param edge the edge.
0752: */
0753: protected void drawTickMarksVertical(Graphics2D g2,
0754: AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
0755: // FIXME: implement this...
0756: }
0757:
0758: /**
0759: * Draws the tick labels for one "band" of time periods.
0760: *
0761: * @param band the band index (zero-based).
0762: * @param g2 the graphics device.
0763: * @param state the axis state.
0764: * @param dataArea the data area.
0765: * @param edge the edge where the axis is located.
0766: *
0767: * @return The updated axis state.
0768: */
0769: protected AxisState drawTickLabels(int band, Graphics2D g2,
0770: AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
0771:
0772: // work out the initial gap
0773: double delta1 = 0.0;
0774: FontMetrics fm = g2.getFontMetrics(this .labelInfo[band]
0775: .getLabelFont());
0776: if (edge == RectangleEdge.BOTTOM) {
0777: delta1 = this .labelInfo[band].getPadding()
0778: .calculateTopOutset(fm.getHeight());
0779: } else if (edge == RectangleEdge.TOP) {
0780: delta1 = this .labelInfo[band].getPadding()
0781: .calculateBottomOutset(fm.getHeight());
0782: }
0783: state.moveCursor(delta1, edge);
0784: long axisMin = this .first.getFirstMillisecond(this .calendar);
0785: long axisMax = this .last.getLastMillisecond(this .calendar);
0786: g2.setFont(this .labelInfo[band].getLabelFont());
0787: g2.setPaint(this .labelInfo[band].getLabelPaint());
0788:
0789: // work out the number of periods to skip for labelling
0790: RegularTimePeriod p1 = this .labelInfo[band].createInstance(
0791: new Date(axisMin), this .timeZone);
0792: RegularTimePeriod p2 = this .labelInfo[band].createInstance(
0793: new Date(axisMax), this .timeZone);
0794: String label1 = this .labelInfo[band].getDateFormat().format(
0795: new Date(p1.getMiddleMillisecond(this .calendar)));
0796: String label2 = this .labelInfo[band].getDateFormat().format(
0797: new Date(p2.getMiddleMillisecond(this .calendar)));
0798: Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2, g2
0799: .getFontMetrics());
0800: Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2, g2
0801: .getFontMetrics());
0802: double w = Math.max(b1.getWidth(), b2.getWidth());
0803: long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0,
0804: dataArea, edge))
0805: - axisMin;
0806: long length = p1.getLastMillisecond(this .calendar)
0807: - p1.getFirstMillisecond(this .calendar);
0808: int periods = (int) (ww / length) + 1;
0809:
0810: RegularTimePeriod p = this .labelInfo[band].createInstance(
0811: new Date(axisMin), this .timeZone);
0812: Rectangle2D b = null;
0813: long lastXX = 0L;
0814: float y = (float) (state.getCursor());
0815: TextAnchor anchor = TextAnchor.TOP_CENTER;
0816: float yDelta = (float) b1.getHeight();
0817: if (edge == RectangleEdge.TOP) {
0818: anchor = TextAnchor.BOTTOM_CENTER;
0819: yDelta = -yDelta;
0820: }
0821: while (p.getFirstMillisecond(this .calendar) <= axisMax) {
0822: float x = (float) valueToJava2D(p
0823: .getMiddleMillisecond(this .calendar), dataArea,
0824: edge);
0825: DateFormat df = this .labelInfo[band].getDateFormat();
0826: String label = df.format(new Date(p
0827: .getMiddleMillisecond(this .calendar)));
0828: long first = p.getFirstMillisecond(this .calendar);
0829: long last = p.getLastMillisecond(this .calendar);
0830: if (last > axisMax) {
0831: // this is the last period, but it is only partially visible
0832: // so check that the label will fit before displaying it...
0833: Rectangle2D bb = TextUtilities.getTextBounds(label, g2,
0834: g2.getFontMetrics());
0835: if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) {
0836: float xstart = (float) valueToJava2D(Math.max(
0837: first, axisMin), dataArea, edge);
0838: if (bb.getWidth() < (dataArea.getMaxX() - xstart)) {
0839: x = ((float) dataArea.getMaxX() + xstart) / 2.0f;
0840: } else {
0841: label = null;
0842: }
0843: }
0844: }
0845: if (first < axisMin) {
0846: // this is the first period, but it is only partially visible
0847: // so check that the label will fit before displaying it...
0848: Rectangle2D bb = TextUtilities.getTextBounds(label, g2,
0849: g2.getFontMetrics());
0850: if ((x - bb.getWidth() / 2) < dataArea.getX()) {
0851: float xlast = (float) valueToJava2D(Math.min(last,
0852: axisMax), dataArea, edge);
0853: if (bb.getWidth() < (xlast - dataArea.getX())) {
0854: x = (xlast + (float) dataArea.getX()) / 2.0f;
0855: } else {
0856: label = null;
0857: }
0858: }
0859:
0860: }
0861: if (label != null) {
0862: g2.setPaint(this .labelInfo[band].getLabelPaint());
0863: b = TextUtilities.drawAlignedString(label, g2, x, y,
0864: anchor);
0865: }
0866: if (lastXX > 0L) {
0867: if (this .labelInfo[band].getDrawDividers()) {
0868: long nextXX = p.getFirstMillisecond(this .calendar);
0869: long mid = (lastXX + nextXX) / 2;
0870: float mid2d = (float) valueToJava2D(mid, dataArea,
0871: edge);
0872: g2.setStroke(this .labelInfo[band]
0873: .getDividerStroke());
0874: g2.setPaint(this .labelInfo[band].getDividerPaint());
0875: g2.draw(new Line2D.Float(mid2d, y, mid2d, y
0876: + yDelta));
0877: }
0878: }
0879: lastXX = last;
0880: for (int i = 0; i < periods; i++) {
0881: p = p.next();
0882: }
0883: }
0884: double used = 0.0;
0885: if (b != null) {
0886: used = b.getHeight();
0887: // work out the trailing gap
0888: if (edge == RectangleEdge.BOTTOM) {
0889: used += this .labelInfo[band].getPadding()
0890: .calculateBottomOutset(fm.getHeight());
0891: } else if (edge == RectangleEdge.TOP) {
0892: used += this .labelInfo[band].getPadding()
0893: .calculateTopOutset(fm.getHeight());
0894: }
0895: }
0896: state.moveCursor(used, edge);
0897: return state;
0898: }
0899:
0900: /**
0901: * Calculates the positions of the ticks for the axis, storing the results
0902: * in the tick list (ready for drawing).
0903: *
0904: * @param g2 the graphics device.
0905: * @param state the axis state.
0906: * @param dataArea the area inside the axes.
0907: * @param edge the edge on which the axis is located.
0908: *
0909: * @return The list of ticks.
0910: */
0911: public List refreshTicks(Graphics2D g2, AxisState state,
0912: Rectangle2D dataArea, RectangleEdge edge) {
0913: return Collections.EMPTY_LIST;
0914: }
0915:
0916: /**
0917: * Converts a data value to a coordinate in Java2D space, assuming that the
0918: * axis runs along one edge of the specified dataArea.
0919: * <p>
0920: * Note that it is possible for the coordinate to fall outside the area.
0921: *
0922: * @param value the data value.
0923: * @param area the area for plotting the data.
0924: * @param edge the edge along which the axis lies.
0925: *
0926: * @return The Java2D coordinate.
0927: */
0928: public double valueToJava2D(double value, Rectangle2D area,
0929: RectangleEdge edge) {
0930:
0931: double result = Double.NaN;
0932: double axisMin = this .first.getFirstMillisecond(this .calendar);
0933: double axisMax = this .last.getLastMillisecond(this .calendar);
0934: if (RectangleEdge.isTopOrBottom(edge)) {
0935: double minX = area.getX();
0936: double maxX = area.getMaxX();
0937: if (isInverted()) {
0938: result = maxX
0939: + ((value - axisMin) / (axisMax - axisMin))
0940: * (minX - maxX);
0941: } else {
0942: result = minX
0943: + ((value - axisMin) / (axisMax - axisMin))
0944: * (maxX - minX);
0945: }
0946: } else if (RectangleEdge.isLeftOrRight(edge)) {
0947: double minY = area.getMinY();
0948: double maxY = area.getMaxY();
0949: if (isInverted()) {
0950: result = minY
0951: + (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
0952: } else {
0953: result = maxY
0954: - (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
0955: }
0956: }
0957: return result;
0958:
0959: }
0960:
0961: /**
0962: * Converts a coordinate in Java2D space to the corresponding data value,
0963: * assuming that the axis runs along one edge of the specified dataArea.
0964: *
0965: * @param java2DValue the coordinate in Java2D space.
0966: * @param area the area in which the data is plotted.
0967: * @param edge the edge along which the axis lies.
0968: *
0969: * @return The data value.
0970: */
0971: public double java2DToValue(double java2DValue, Rectangle2D area,
0972: RectangleEdge edge) {
0973:
0974: double result = Double.NaN;
0975: double min = 0.0;
0976: double max = 0.0;
0977: double axisMin = this .first.getFirstMillisecond(this .calendar);
0978: double axisMax = this .last.getLastMillisecond(this .calendar);
0979: if (RectangleEdge.isTopOrBottom(edge)) {
0980: min = area.getX();
0981: max = area.getMaxX();
0982: } else if (RectangleEdge.isLeftOrRight(edge)) {
0983: min = area.getMaxY();
0984: max = area.getY();
0985: }
0986: if (isInverted()) {
0987: result = axisMax
0988: - ((java2DValue - min) / (max - min) * (axisMax - axisMin));
0989: } else {
0990: result = axisMin
0991: + ((java2DValue - min) / (max - min) * (axisMax - axisMin));
0992: }
0993: return result;
0994: }
0995:
0996: /**
0997: * Rescales the axis to ensure that all data is visible.
0998: */
0999: protected void autoAdjustRange() {
1000:
1001: Plot plot = getPlot();
1002: if (plot == null) {
1003: return; // no plot, no data
1004: }
1005:
1006: if (plot instanceof ValueAxisPlot) {
1007: ValueAxisPlot vap = (ValueAxisPlot) plot;
1008:
1009: Range r = vap.getDataRange(this );
1010: if (r == null) {
1011: r = getDefaultAutoRange();
1012: }
1013:
1014: long upper = Math.round(r.getUpperBound());
1015: long lower = Math.round(r.getLowerBound());
1016: this .first = createInstance(this .autoRangeTimePeriodClass,
1017: new Date(lower), this .timeZone);
1018: this .last = createInstance(this .autoRangeTimePeriodClass,
1019: new Date(upper), this .timeZone);
1020: setRange(r, false, false);
1021: }
1022:
1023: }
1024:
1025: /**
1026: * Tests the axis for equality with an arbitrary object.
1027: *
1028: * @param obj the object (<code>null</code> permitted).
1029: *
1030: * @return A boolean.
1031: */
1032: public boolean equals(Object obj) {
1033: if (obj == this ) {
1034: return true;
1035: }
1036: if (obj instanceof PeriodAxis && super .equals(obj)) {
1037: PeriodAxis that = (PeriodAxis) obj;
1038: if (!this .first.equals(that.first)) {
1039: return false;
1040: }
1041: if (!this .last.equals(that.last)) {
1042: return false;
1043: }
1044: if (!this .timeZone.equals(that.timeZone)) {
1045: return false;
1046: }
1047: if (!this .autoRangeTimePeriodClass
1048: .equals(that.autoRangeTimePeriodClass)) {
1049: return false;
1050: }
1051: if (!(isMinorTickMarksVisible() == that
1052: .isMinorTickMarksVisible())) {
1053: return false;
1054: }
1055: if (!this .majorTickTimePeriodClass
1056: .equals(that.majorTickTimePeriodClass)) {
1057: return false;
1058: }
1059: if (!this .minorTickTimePeriodClass
1060: .equals(that.minorTickTimePeriodClass)) {
1061: return false;
1062: }
1063: if (!this .minorTickMarkPaint
1064: .equals(that.minorTickMarkPaint)) {
1065: return false;
1066: }
1067: if (!this .minorTickMarkStroke
1068: .equals(that.minorTickMarkStroke)) {
1069: return false;
1070: }
1071: if (!Arrays.equals(this .labelInfo, that.labelInfo)) {
1072: return false;
1073: }
1074: return true;
1075: }
1076: return false;
1077: }
1078:
1079: /**
1080: * Returns a hash code for this object.
1081: *
1082: * @return A hash code.
1083: */
1084: public int hashCode() {
1085: if (getLabel() != null) {
1086: return getLabel().hashCode();
1087: } else {
1088: return 0;
1089: }
1090: }
1091:
1092: /**
1093: * Returns a clone of the axis.
1094: *
1095: * @return A clone.
1096: *
1097: * @throws CloneNotSupportedException this class is cloneable, but
1098: * subclasses may not be.
1099: */
1100: public Object clone() throws CloneNotSupportedException {
1101: PeriodAxis clone = (PeriodAxis) super .clone();
1102: clone.timeZone = (TimeZone) this .timeZone.clone();
1103: clone.labelInfo = new PeriodAxisLabelInfo[this .labelInfo.length];
1104: for (int i = 0; i < this .labelInfo.length; i++) {
1105: clone.labelInfo[i] = this .labelInfo[i]; // copy across references
1106: // to immutable objs
1107: }
1108: return clone;
1109: }
1110:
1111: /**
1112: * A utility method used to create a particular subclass of the
1113: * {@link RegularTimePeriod} class that includes the specified millisecond,
1114: * assuming the specified time zone.
1115: *
1116: * @param periodClass the class.
1117: * @param millisecond the time.
1118: * @param zone the time zone.
1119: *
1120: * @return The time period.
1121: */
1122: private RegularTimePeriod createInstance(Class periodClass,
1123: Date millisecond, TimeZone zone) {
1124: RegularTimePeriod result = null;
1125: try {
1126: Constructor c = periodClass
1127: .getDeclaredConstructor(new Class[] { Date.class,
1128: TimeZone.class });
1129: result = (RegularTimePeriod) c.newInstance(new Object[] {
1130: millisecond, zone });
1131: } catch (Exception e) {
1132: // do nothing
1133: }
1134: return result;
1135: }
1136:
1137: /**
1138: * Provides serialization support.
1139: *
1140: * @param stream the output stream.
1141: *
1142: * @throws IOException if there is an I/O error.
1143: */
1144: private void writeObject(ObjectOutputStream stream)
1145: throws IOException {
1146: stream.defaultWriteObject();
1147: SerialUtilities.writeStroke(this .minorTickMarkStroke, stream);
1148: SerialUtilities.writePaint(this .minorTickMarkPaint, stream);
1149: }
1150:
1151: /**
1152: * Provides serialization support.
1153: *
1154: * @param stream the input stream.
1155: *
1156: * @throws IOException if there is an I/O error.
1157: * @throws ClassNotFoundException if there is a classpath problem.
1158: */
1159: private void readObject(ObjectInputStream stream)
1160: throws IOException, ClassNotFoundException {
1161: stream.defaultReadObject();
1162: this.minorTickMarkStroke = SerialUtilities.readStroke(stream);
1163: this.minorTickMarkPaint = SerialUtilities.readPaint(stream);
1164: }
1165:
1166: }
|