0001: /* ===========================================================
0002: * JFreeChart : a free chart library for the Java(tm) platform
0003: * ===========================================================
0004: *
0005: * (C) Copyright 2000-2005, 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: * CyclicNumberAxis.java
0029: * ---------------------
0030: * (C) Copyright 2003, 2004, by Nicolas Brodu and Contributors.
0031: *
0032: * Original Author: Nicolas Brodu;
0033: * Contributor(s): David Gilbert (for Object Refinery Limited);
0034: *
0035: * $Id: CyclicNumberAxis.java,v 1.10.2.2 2005/10/25 20:37:34 mungady Exp $
0036: *
0037: * Changes
0038: * -------
0039: * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
0040: * 16-Mar-2004 : Added plotState to draw() method (DG);
0041: * 07-Apr-2004 : Modifed text bounds calculation (DG);
0042: * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
0043: * argument in selectAutoTickUnit() (DG);
0044: * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal
0045: * (for consistency with other classes) and removed unused
0046: * parameters (DG);
0047: * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
0048: *
0049: */
0050:
0051: package org.jfree.chart.axis;
0052:
0053: import java.awt.BasicStroke;
0054: import java.awt.Color;
0055: import java.awt.Font;
0056: import java.awt.FontMetrics;
0057: import java.awt.Graphics2D;
0058: import java.awt.Paint;
0059: import java.awt.Stroke;
0060: import java.awt.geom.Line2D;
0061: import java.awt.geom.Rectangle2D;
0062: import java.io.IOException;
0063: import java.io.ObjectInputStream;
0064: import java.io.ObjectOutputStream;
0065: import java.text.NumberFormat;
0066: import java.util.List;
0067:
0068: import org.jfree.chart.plot.Plot;
0069: import org.jfree.chart.plot.PlotRenderingInfo;
0070: import org.jfree.data.Range;
0071: import org.jfree.io.SerialUtilities;
0072: import org.jfree.text.TextUtilities;
0073: import org.jfree.ui.RectangleEdge;
0074: import org.jfree.ui.TextAnchor;
0075: import org.jfree.util.ObjectUtilities;
0076: import org.jfree.util.PaintUtilities;
0077:
0078: /**
0079: This class extends NumberAxis and handles cycling.
0080:
0081: Traditional representation of data in the range x0..x1
0082: <pre>
0083: |-------------------------|
0084: x0 x1
0085: </pre>
0086:
0087: Here, the range bounds are at the axis extremities.
0088: With cyclic axis, however, the time is split in
0089: "cycles", or "time frames", or the same duration : the period.
0090:
0091: A cycle axis cannot by definition handle a larger interval
0092: than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
0093: period can be represented with such an axis.
0094:
0095: The cycle bound is the number between x0 and x1 which marks
0096: the beginning of new time frame:
0097: <pre>
0098: |---------------------|----------------------------|
0099: x0 cb x1
0100: <---previous cycle---><-------current cycle-------->
0101: </pre>
0102:
0103: It is actually a multiple of the period, plus optionally
0104: a start offset: <pre>cb = n * period + offset</pre>
0105:
0106: Thus, by definition, two consecutive cycle bounds
0107: period apart, which is precisely why it is called a
0108: period.
0109:
0110: The visual representation of a cyclic axis is like that:
0111: <pre>
0112: |----------------------------|---------------------|
0113: cb x1|x0 cb
0114: <-------current cycle--------><---previous cycle--->
0115: </pre>
0116:
0117: The cycle bound is at the axis ends, then current
0118: cycle is shown, then the last cycle. When using
0119: dynamic data, the visual effect is the current cycle
0120: erases the last cycle as x grows. Then, the next cycle
0121: bound is reached, and the process starts over, erasing
0122: the previous cycle.
0123:
0124: A Cyclic item renderer is provided to do exactly this.
0125:
0126: */
0127: public class CyclicNumberAxis extends NumberAxis {
0128:
0129: /** The default axis line stroke. */
0130: public static Stroke DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke(
0131: 1.0f);
0132:
0133: /** The default axis line paint. */
0134: public static final Paint DEFAULT_ADVANCE_LINE_PAINT = Color.gray;
0135:
0136: /** The offset. */
0137: protected double offset;
0138:
0139: /** The period.*/
0140: protected double period;
0141:
0142: /** ??. */
0143: protected boolean boundMappedToLastCycle;
0144:
0145: /** A flag that controls whether or not the advance line is visible. */
0146: protected boolean advanceLineVisible;
0147:
0148: /** The advance line stroke. */
0149: protected transient Stroke advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE;
0150:
0151: /** The advance line paint. */
0152: protected transient Paint advanceLinePaint;
0153:
0154: private transient boolean internalMarkerWhenTicksOverlap;
0155: private transient Tick internalMarkerCycleBoundTick;
0156:
0157: /**
0158: * Creates a CycleNumberAxis with the given period.
0159: *
0160: * @param period the period.
0161: */
0162: public CyclicNumberAxis(double period) {
0163: this (period, 0.0);
0164: }
0165:
0166: /**
0167: * Creates a CycleNumberAxis with the given period and offset.
0168: *
0169: * @param period the period.
0170: * @param offset the offset.
0171: */
0172: public CyclicNumberAxis(double period, double offset) {
0173: this (period, offset, null);
0174: }
0175:
0176: /**
0177: * Creates a named CycleNumberAxis with the given period.
0178: *
0179: * @param period the period.
0180: * @param label the label.
0181: */
0182: public CyclicNumberAxis(double period, String label) {
0183: this (0, period, label);
0184: }
0185:
0186: /**
0187: * Creates a named CycleNumberAxis with the given period and offset.
0188: *
0189: * @param period the period.
0190: * @param offset the offset.
0191: * @param label the label.
0192: */
0193: public CyclicNumberAxis(double period, double offset, String label) {
0194: super (label);
0195: this .period = period;
0196: this .offset = offset;
0197: setFixedAutoRange(period);
0198: this .advanceLineVisible = true;
0199: this .advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT;
0200: }
0201:
0202: /**
0203: * The advance line is the line drawn at the limit of the current cycle,
0204: * when erasing the previous cycle.
0205: *
0206: * @return A boolean.
0207: */
0208: public boolean isAdvanceLineVisible() {
0209: return this .advanceLineVisible;
0210: }
0211:
0212: /**
0213: * The advance line is the line drawn at the limit of the current cycle,
0214: * when erasing the previous cycle.
0215: *
0216: * @param visible the flag.
0217: */
0218: public void setAdvanceLineVisible(boolean visible) {
0219: this .advanceLineVisible = visible;
0220: }
0221:
0222: /**
0223: * The advance line is the line drawn at the limit of the current cycle,
0224: * when erasing the previous cycle.
0225: *
0226: * @return The paint (never <code>null</code>).
0227: */
0228: public Paint getAdvanceLinePaint() {
0229: return this .advanceLinePaint;
0230: }
0231:
0232: /**
0233: * The advance line is the line drawn at the limit of the current cycle,
0234: * when erasing the previous cycle.
0235: *
0236: * @param paint the paint (<code>null</code> not permitted).
0237: */
0238: public void setAdvanceLinePaint(Paint paint) {
0239: if (paint == null) {
0240: throw new IllegalArgumentException("Null 'paint' argument.");
0241: }
0242: this .advanceLinePaint = paint;
0243: }
0244:
0245: /**
0246: * The advance line is the line drawn at the limit of the current cycle,
0247: * when erasing the previous cycle.
0248: *
0249: * @return The stroke (never <code>null</code>).
0250: */
0251: public Stroke getAdvanceLineStroke() {
0252: return this .advanceLineStroke;
0253: }
0254:
0255: /**
0256: * The advance line is the line drawn at the limit of the current cycle,
0257: * when erasing the previous cycle.
0258: *
0259: * @param stroke the stroke (<code>null</code> not permitted).
0260: */
0261: public void setAdvanceLineStroke(Stroke stroke) {
0262: if (stroke == null) {
0263: throw new IllegalArgumentException(
0264: "Null 'stroke' argument.");
0265: }
0266: this .advanceLineStroke = stroke;
0267: }
0268:
0269: /**
0270: * The cycle bound can be associated either with the current or with the
0271: * last cycle. It's up to the user's choice to decide which, as this is
0272: * just a convention. By default, the cycle bound is mapped to the current
0273: * cycle.
0274: * <br>
0275: * Note that this has no effect on visual appearance, as the cycle bound is
0276: * mapped successively for both axis ends. Use this function for correct
0277: * results in translateValueToJava2D.
0278: *
0279: * @return <code>true</code> if the cycle bound is mapped to the last
0280: * cycle, <code>false</code> if it is bound to the current cycle
0281: * (default)
0282: */
0283: public boolean isBoundMappedToLastCycle() {
0284: return this .boundMappedToLastCycle;
0285: }
0286:
0287: /**
0288: * The cycle bound can be associated either with the current or with the
0289: * last cycle. It's up to the user's choice to decide which, as this is
0290: * just a convention. By default, the cycle bound is mapped to the current
0291: * cycle.
0292: * <br>
0293: * Note that this has no effect on visual appearance, as the cycle bound is
0294: * mapped successively for both axis ends. Use this function for correct
0295: * results in valueToJava2D.
0296: *
0297: * @param boundMappedToLastCycle Set it to true to map the cycle bound to
0298: * the last cycle.
0299: */
0300: public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) {
0301: this .boundMappedToLastCycle = boundMappedToLastCycle;
0302: }
0303:
0304: /**
0305: * Selects a tick unit when the axis is displayed horizontally.
0306: *
0307: * @param g2 the graphics device.
0308: * @param drawArea the drawing area.
0309: * @param dataArea the data area.
0310: * @param edge the side of the rectangle on which the axis is displayed.
0311: */
0312: protected void selectHorizontalAutoTickUnit(Graphics2D g2,
0313: Rectangle2D drawArea, Rectangle2D dataArea,
0314: RectangleEdge edge) {
0315:
0316: double tickLabelWidth = estimateMaximumTickLabelWidth(g2,
0317: getTickUnit());
0318:
0319: // Compute number of labels
0320: double n = getRange().getLength() * tickLabelWidth
0321: / dataArea.getWidth();
0322:
0323: setTickUnit((NumberTickUnit) getStandardTickUnits()
0324: .getCeilingTickUnit(n), false, false);
0325:
0326: }
0327:
0328: /**
0329: * Selects a tick unit when the axis is displayed vertically.
0330: *
0331: * @param g2 the graphics device.
0332: * @param drawArea the drawing area.
0333: * @param dataArea the data area.
0334: * @param edge the side of the rectangle on which the axis is displayed.
0335: */
0336: protected void selectVerticalAutoTickUnit(Graphics2D g2,
0337: Rectangle2D drawArea, Rectangle2D dataArea,
0338: RectangleEdge edge) {
0339:
0340: double tickLabelWidth = estimateMaximumTickLabelWidth(g2,
0341: getTickUnit());
0342:
0343: // Compute number of labels
0344: double n = getRange().getLength() * tickLabelWidth
0345: / dataArea.getHeight();
0346:
0347: setTickUnit((NumberTickUnit) getStandardTickUnits()
0348: .getCeilingTickUnit(n), false, false);
0349:
0350: }
0351:
0352: /**
0353: * A special Number tick that also hold information about the cycle bound
0354: * mapping for this tick. This is especially useful for having a tick at
0355: * each axis end with the cycle bound value. See also
0356: * isBoundMappedToLastCycle()
0357: */
0358: protected static class CycleBoundTick extends NumberTick {
0359:
0360: /** Map to last cycle. */
0361: public boolean mapToLastCycle;
0362:
0363: /**
0364: * Creates a new tick.
0365: *
0366: * @param mapToLastCycle map to last cycle?
0367: * @param number the number.
0368: * @param label the label.
0369: * @param textAnchor the text anchor.
0370: * @param rotationAnchor the rotation anchor.
0371: * @param angle the rotation angle.
0372: */
0373: public CycleBoundTick(boolean mapToLastCycle, Number number,
0374: String label, TextAnchor textAnchor,
0375: TextAnchor rotationAnchor, double angle) {
0376: super (number, label, textAnchor, rotationAnchor, angle);
0377: this .mapToLastCycle = mapToLastCycle;
0378: }
0379: }
0380:
0381: /**
0382: * Calculates the anchor point for a tick.
0383: *
0384: * @param tick the tick.
0385: * @param cursor the cursor.
0386: * @param dataArea the data area.
0387: * @param edge the side on which the axis is displayed.
0388: *
0389: * @return The anchor point.
0390: */
0391: protected float[] calculateAnchorPoint(ValueTick tick,
0392: double cursor, Rectangle2D dataArea, RectangleEdge edge) {
0393: if (tick instanceof CycleBoundTick) {
0394: boolean mapsav = this .boundMappedToLastCycle;
0395: this .boundMappedToLastCycle = ((CycleBoundTick) tick).mapToLastCycle;
0396: float[] ret = super .calculateAnchorPoint(tick, cursor,
0397: dataArea, edge);
0398: this .boundMappedToLastCycle = mapsav;
0399: return ret;
0400: }
0401: return super .calculateAnchorPoint(tick, cursor, dataArea, edge);
0402: }
0403:
0404: /**
0405: * Builds a list of ticks for the axis. This method is called when the
0406: * axis is at the top or bottom of the chart (so the axis is "horizontal").
0407: *
0408: * @param g2 the graphics device.
0409: * @param dataArea the data area.
0410: * @param edge the edge.
0411: *
0412: * @return A list of ticks.
0413: */
0414: protected List refreshTicksHorizontal(Graphics2D g2,
0415: Rectangle2D dataArea, RectangleEdge edge) {
0416:
0417: List result = new java.util.ArrayList();
0418:
0419: Font tickLabelFont = getTickLabelFont();
0420: g2.setFont(tickLabelFont);
0421:
0422: if (isAutoTickUnitSelection()) {
0423: selectAutoTickUnit(g2, dataArea, edge);
0424: }
0425:
0426: double unit = getTickUnit().getSize();
0427: double cycleBound = getCycleBound();
0428: double currentTickValue = Math.ceil(cycleBound / unit) * unit;
0429: double upperValue = getRange().getUpperBound();
0430: boolean cycled = false;
0431:
0432: boolean boundMapping = this .boundMappedToLastCycle;
0433: this .boundMappedToLastCycle = false;
0434:
0435: CycleBoundTick lastTick = null;
0436: float lastX = 0.0f;
0437:
0438: if (upperValue == cycleBound) {
0439: currentTickValue = calculateLowestVisibleTickValue();
0440: cycled = true;
0441: this .boundMappedToLastCycle = true;
0442: }
0443:
0444: while (currentTickValue <= upperValue) {
0445:
0446: // Cycle when necessary
0447: boolean cyclenow = false;
0448: if ((currentTickValue + unit > upperValue) && !cycled) {
0449: cyclenow = true;
0450: }
0451:
0452: double xx = valueToJava2D(currentTickValue, dataArea, edge);
0453: String tickLabel;
0454: NumberFormat formatter = getNumberFormatOverride();
0455: if (formatter != null) {
0456: tickLabel = formatter.format(currentTickValue);
0457: } else {
0458: tickLabel = getTickUnit().valueToString(
0459: currentTickValue);
0460: }
0461: float x = (float) xx;
0462: TextAnchor anchor = null;
0463: TextAnchor rotationAnchor = null;
0464: double angle = 0.0;
0465: if (isVerticalTickLabels()) {
0466: if (edge == RectangleEdge.TOP) {
0467: angle = Math.PI / 2.0;
0468: } else {
0469: angle = -Math.PI / 2.0;
0470: }
0471: anchor = TextAnchor.CENTER_RIGHT;
0472: // If tick overlap when cycling, update last tick too
0473: if ((lastTick != null) && (lastX == x)
0474: && (currentTickValue != cycleBound)) {
0475: anchor = isInverted() ? TextAnchor.TOP_RIGHT
0476: : TextAnchor.BOTTOM_RIGHT;
0477: result.remove(result.size() - 1);
0478: result.add(new CycleBoundTick(
0479: this .boundMappedToLastCycle, lastTick
0480: .getNumber(), lastTick.getText(),
0481: anchor, anchor, lastTick.getAngle()));
0482: this .internalMarkerWhenTicksOverlap = true;
0483: anchor = isInverted() ? TextAnchor.BOTTOM_RIGHT
0484: : TextAnchor.TOP_RIGHT;
0485: }
0486: rotationAnchor = anchor;
0487: } else {
0488: if (edge == RectangleEdge.TOP) {
0489: anchor = TextAnchor.BOTTOM_CENTER;
0490: if ((lastTick != null) && (lastX == x)
0491: && (currentTickValue != cycleBound)) {
0492: anchor = isInverted() ? TextAnchor.BOTTOM_LEFT
0493: : TextAnchor.BOTTOM_RIGHT;
0494: result.remove(result.size() - 1);
0495: result.add(new CycleBoundTick(
0496: this .boundMappedToLastCycle, lastTick
0497: .getNumber(), lastTick
0498: .getText(), anchor, anchor,
0499: lastTick.getAngle()));
0500: this .internalMarkerWhenTicksOverlap = true;
0501: anchor = isInverted() ? TextAnchor.BOTTOM_RIGHT
0502: : TextAnchor.BOTTOM_LEFT;
0503: }
0504: rotationAnchor = anchor;
0505: } else {
0506: anchor = TextAnchor.TOP_CENTER;
0507: if ((lastTick != null) && (lastX == x)
0508: && (currentTickValue != cycleBound)) {
0509: anchor = isInverted() ? TextAnchor.TOP_LEFT
0510: : TextAnchor.TOP_RIGHT;
0511: result.remove(result.size() - 1);
0512: result.add(new CycleBoundTick(
0513: this .boundMappedToLastCycle, lastTick
0514: .getNumber(), lastTick
0515: .getText(), anchor, anchor,
0516: lastTick.getAngle()));
0517: this .internalMarkerWhenTicksOverlap = true;
0518: anchor = isInverted() ? TextAnchor.TOP_RIGHT
0519: : TextAnchor.TOP_LEFT;
0520: }
0521: rotationAnchor = anchor;
0522: }
0523: }
0524:
0525: CycleBoundTick tick = new CycleBoundTick(
0526: this .boundMappedToLastCycle, new Double(
0527: currentTickValue), tickLabel, anchor,
0528: rotationAnchor, angle);
0529: if (currentTickValue == cycleBound) {
0530: this .internalMarkerCycleBoundTick = tick;
0531: }
0532: result.add(tick);
0533: lastTick = tick;
0534: lastX = x;
0535:
0536: currentTickValue += unit;
0537:
0538: if (cyclenow) {
0539: currentTickValue = calculateLowestVisibleTickValue();
0540: upperValue = cycleBound;
0541: cycled = true;
0542: this .boundMappedToLastCycle = true;
0543: }
0544:
0545: }
0546: this .boundMappedToLastCycle = boundMapping;
0547: return result;
0548:
0549: }
0550:
0551: /**
0552: * Builds a list of ticks for the axis. This method is called when the
0553: * axis is at the left or right of the chart (so the axis is "vertical").
0554: *
0555: * @param g2 the graphics device.
0556: * @param dataArea the data area.
0557: * @param edge the edge.
0558: *
0559: * @return A list of ticks.
0560: */
0561: protected List refreshVerticalTicks(Graphics2D g2,
0562: Rectangle2D dataArea, RectangleEdge edge) {
0563:
0564: List result = new java.util.ArrayList();
0565: result.clear();
0566:
0567: Font tickLabelFont = getTickLabelFont();
0568: g2.setFont(tickLabelFont);
0569: if (isAutoTickUnitSelection()) {
0570: selectAutoTickUnit(g2, dataArea, edge);
0571: }
0572:
0573: double unit = getTickUnit().getSize();
0574: double cycleBound = getCycleBound();
0575: double currentTickValue = Math.ceil(cycleBound / unit) * unit;
0576: double upperValue = getRange().getUpperBound();
0577: boolean cycled = false;
0578:
0579: boolean boundMapping = this .boundMappedToLastCycle;
0580: this .boundMappedToLastCycle = true;
0581:
0582: NumberTick lastTick = null;
0583: float lastY = 0.0f;
0584:
0585: if (upperValue == cycleBound) {
0586: currentTickValue = calculateLowestVisibleTickValue();
0587: cycled = true;
0588: this .boundMappedToLastCycle = true;
0589: }
0590:
0591: while (currentTickValue <= upperValue) {
0592:
0593: // Cycle when necessary
0594: boolean cyclenow = false;
0595: if ((currentTickValue + unit > upperValue) && !cycled) {
0596: cyclenow = true;
0597: }
0598:
0599: double yy = valueToJava2D(currentTickValue, dataArea, edge);
0600: String tickLabel;
0601: NumberFormat formatter = getNumberFormatOverride();
0602: if (formatter != null) {
0603: tickLabel = formatter.format(currentTickValue);
0604: } else {
0605: tickLabel = getTickUnit().valueToString(
0606: currentTickValue);
0607: }
0608:
0609: float y = (float) yy;
0610: TextAnchor anchor = null;
0611: TextAnchor rotationAnchor = null;
0612: double angle = 0.0;
0613: if (isVerticalTickLabels()) {
0614:
0615: if (edge == RectangleEdge.LEFT) {
0616: anchor = TextAnchor.BOTTOM_CENTER;
0617: if ((lastTick != null) && (lastY == y)
0618: && (currentTickValue != cycleBound)) {
0619: anchor = isInverted() ? TextAnchor.BOTTOM_LEFT
0620: : TextAnchor.BOTTOM_RIGHT;
0621: result.remove(result.size() - 1);
0622: result.add(new CycleBoundTick(
0623: this .boundMappedToLastCycle, lastTick
0624: .getNumber(), lastTick
0625: .getText(), anchor, anchor,
0626: lastTick.getAngle()));
0627: this .internalMarkerWhenTicksOverlap = true;
0628: anchor = isInverted() ? TextAnchor.BOTTOM_RIGHT
0629: : TextAnchor.BOTTOM_LEFT;
0630: }
0631: rotationAnchor = anchor;
0632: angle = -Math.PI / 2.0;
0633: } else {
0634: anchor = TextAnchor.BOTTOM_CENTER;
0635: if ((lastTick != null) && (lastY == y)
0636: && (currentTickValue != cycleBound)) {
0637: anchor = isInverted() ? TextAnchor.BOTTOM_RIGHT
0638: : TextAnchor.BOTTOM_LEFT;
0639: result.remove(result.size() - 1);
0640: result.add(new CycleBoundTick(
0641: this .boundMappedToLastCycle, lastTick
0642: .getNumber(), lastTick
0643: .getText(), anchor, anchor,
0644: lastTick.getAngle()));
0645: this .internalMarkerWhenTicksOverlap = true;
0646: anchor = isInverted() ? TextAnchor.BOTTOM_LEFT
0647: : TextAnchor.BOTTOM_RIGHT;
0648: }
0649: rotationAnchor = anchor;
0650: angle = Math.PI / 2.0;
0651: }
0652: } else {
0653: if (edge == RectangleEdge.LEFT) {
0654: anchor = TextAnchor.CENTER_RIGHT;
0655: if ((lastTick != null) && (lastY == y)
0656: && (currentTickValue != cycleBound)) {
0657: anchor = isInverted() ? TextAnchor.BOTTOM_RIGHT
0658: : TextAnchor.TOP_RIGHT;
0659: result.remove(result.size() - 1);
0660: result.add(new CycleBoundTick(
0661: this .boundMappedToLastCycle, lastTick
0662: .getNumber(), lastTick
0663: .getText(), anchor, anchor,
0664: lastTick.getAngle()));
0665: this .internalMarkerWhenTicksOverlap = true;
0666: anchor = isInverted() ? TextAnchor.TOP_RIGHT
0667: : TextAnchor.BOTTOM_RIGHT;
0668: }
0669: rotationAnchor = anchor;
0670: } else {
0671: anchor = TextAnchor.CENTER_LEFT;
0672: if ((lastTick != null) && (lastY == y)
0673: && (currentTickValue != cycleBound)) {
0674: anchor = isInverted() ? TextAnchor.BOTTOM_LEFT
0675: : TextAnchor.TOP_LEFT;
0676: result.remove(result.size() - 1);
0677: result.add(new CycleBoundTick(
0678: this .boundMappedToLastCycle, lastTick
0679: .getNumber(), lastTick
0680: .getText(), anchor, anchor,
0681: lastTick.getAngle()));
0682: this .internalMarkerWhenTicksOverlap = true;
0683: anchor = isInverted() ? TextAnchor.TOP_LEFT
0684: : TextAnchor.BOTTOM_LEFT;
0685: }
0686: rotationAnchor = anchor;
0687: }
0688: }
0689:
0690: CycleBoundTick tick = new CycleBoundTick(
0691: this .boundMappedToLastCycle, new Double(
0692: currentTickValue), tickLabel, anchor,
0693: rotationAnchor, angle);
0694: if (currentTickValue == cycleBound) {
0695: this .internalMarkerCycleBoundTick = tick;
0696: }
0697: result.add(tick);
0698: lastTick = tick;
0699: lastY = y;
0700:
0701: if (currentTickValue == cycleBound) {
0702: this .internalMarkerCycleBoundTick = tick;
0703: }
0704:
0705: currentTickValue += unit;
0706:
0707: if (cyclenow) {
0708: currentTickValue = calculateLowestVisibleTickValue();
0709: upperValue = cycleBound;
0710: cycled = true;
0711: this .boundMappedToLastCycle = false;
0712: }
0713:
0714: }
0715: this .boundMappedToLastCycle = boundMapping;
0716: return result;
0717: }
0718:
0719: /**
0720: * Converts a coordinate from Java 2D space to data space.
0721: *
0722: * @param java2DValue the coordinate in Java2D space.
0723: * @param dataArea the data area.
0724: * @param edge the edge.
0725: *
0726: * @return The data value.
0727: */
0728: public double java2DToValue(double java2DValue,
0729: Rectangle2D dataArea, RectangleEdge edge) {
0730: Range range = getRange();
0731:
0732: double vmax = range.getUpperBound();
0733: double vp = getCycleBound();
0734:
0735: double jmin = 0.0;
0736: double jmax = 0.0;
0737: if (RectangleEdge.isTopOrBottom(edge)) {
0738: jmin = dataArea.getMinX();
0739: jmax = dataArea.getMaxX();
0740: } else if (RectangleEdge.isLeftOrRight(edge)) {
0741: jmin = dataArea.getMaxY();
0742: jmax = dataArea.getMinY();
0743: }
0744:
0745: if (isInverted()) {
0746: double jbreak = jmax - (vmax - vp) * (jmax - jmin)
0747: / this .period;
0748: if (java2DValue >= jbreak) {
0749: return vp + (jmax - java2DValue) * this .period
0750: / (jmax - jmin);
0751: } else {
0752: return vp - (java2DValue - jmin) * this .period
0753: / (jmax - jmin);
0754: }
0755: } else {
0756: double jbreak = (vmax - vp) * (jmax - jmin) / this .period
0757: + jmin;
0758: if (java2DValue <= jbreak) {
0759: return vp + (java2DValue - jmin) * this .period
0760: / (jmax - jmin);
0761: } else {
0762: return vp - (jmax - java2DValue) * this .period
0763: / (jmax - jmin);
0764: }
0765: }
0766: }
0767:
0768: /**
0769: * Translates a value from data space to Java 2D space.
0770: *
0771: * @param value the data value.
0772: * @param dataArea the data area.
0773: * @param edge the edge.
0774: *
0775: * @return The Java 2D value.
0776: */
0777: public double valueToJava2D(double value, Rectangle2D dataArea,
0778: RectangleEdge edge) {
0779: Range range = getRange();
0780:
0781: double vmin = range.getLowerBound();
0782: double vmax = range.getUpperBound();
0783: double vp = getCycleBound();
0784:
0785: if ((value < vmin) || (value > vmax)) {
0786: return Double.NaN;
0787: }
0788:
0789: double jmin = 0.0;
0790: double jmax = 0.0;
0791: if (RectangleEdge.isTopOrBottom(edge)) {
0792: jmin = dataArea.getMinX();
0793: jmax = dataArea.getMaxX();
0794: } else if (RectangleEdge.isLeftOrRight(edge)) {
0795: jmax = dataArea.getMinY();
0796: jmin = dataArea.getMaxY();
0797: }
0798:
0799: if (isInverted()) {
0800: if (value == vp) {
0801: return this .boundMappedToLastCycle ? jmin : jmax;
0802: } else if (value > vp) {
0803: return jmax - (value - vp) * (jmax - jmin)
0804: / this .period;
0805: } else {
0806: return jmin + (vp - value) * (jmax - jmin)
0807: / this .period;
0808: }
0809: } else {
0810: if (value == vp) {
0811: return this .boundMappedToLastCycle ? jmax : jmin;
0812: } else if (value >= vp) {
0813: return jmin + (value - vp) * (jmax - jmin)
0814: / this .period;
0815: } else {
0816: return jmax - (vp - value) * (jmax - jmin)
0817: / this .period;
0818: }
0819: }
0820: }
0821:
0822: /**
0823: * Centers the range about the given value.
0824: *
0825: * @param value the data value.
0826: */
0827: public void centerRange(double value) {
0828: setRange(value - this .period / 2.0, value + this .period / 2.0);
0829: }
0830:
0831: /**
0832: * This function is nearly useless since the auto range is fixed for this
0833: * class to the period. The period is extended if necessary to fit the
0834: * minimum size.
0835: *
0836: * @param size the size.
0837: * @param notify notify?
0838: *
0839: * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double,
0840: * boolean)
0841: */
0842: public void setAutoRangeMinimumSize(double size, boolean notify) {
0843: if (size > this .period) {
0844: this .period = size;
0845: }
0846: super .setAutoRangeMinimumSize(size, notify);
0847: }
0848:
0849: /**
0850: * The auto range is fixed for this class to the period by default.
0851: * This function will thus set a new period.
0852: *
0853: * @param length the length.
0854: *
0855: * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double)
0856: */
0857: public void setFixedAutoRange(double length) {
0858: this .period = length;
0859: super .setFixedAutoRange(length);
0860: }
0861:
0862: /**
0863: * Sets a new axis range. The period is extended to fit the range size, if
0864: * necessary.
0865: *
0866: * @param range the range.
0867: * @param turnOffAutoRange switch off the auto range.
0868: * @param notify notify?
0869: *
0870: * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean)
0871: */
0872: public void setRange(Range range, boolean turnOffAutoRange,
0873: boolean notify) {
0874: double size = range.getUpperBound() - range.getLowerBound();
0875: if (size > this .period) {
0876: this .period = size;
0877: }
0878: super .setRange(range, turnOffAutoRange, notify);
0879: }
0880:
0881: /**
0882: * The cycle bound is defined as the higest value x such that
0883: * "offset + period * i = x", with i and integer and x <
0884: * range.getUpperBound() This is the value which is at both ends of the
0885: * axis : x...up|low...x
0886: * The values from x to up are the valued in the current cycle.
0887: * The values from low to x are the valued in the previous cycle.
0888: *
0889: * @return The cycle bound.
0890: */
0891: public double getCycleBound() {
0892: return Math.floor((getRange().getUpperBound() - this .offset)
0893: / this .period)
0894: * this .period + this .offset;
0895: }
0896:
0897: /**
0898: * The cycle bound is a multiple of the period, plus optionally a start
0899: * offset.
0900: * <P>
0901: * <pre>cb = n * period + offset</pre><br>
0902: *
0903: * @return The current offset.
0904: *
0905: * @see #getCycleBound()
0906: */
0907: public double getOffset() {
0908: return this .offset;
0909: }
0910:
0911: /**
0912: * The cycle bound is a multiple of the period, plus optionally a start
0913: * offset.
0914: * <P>
0915: * <pre>cb = n * period + offset</pre><br>
0916: *
0917: * @param offset The offset to set.
0918: *
0919: * @see #getCycleBound()
0920: */
0921: public void setOffset(double offset) {
0922: this .offset = offset;
0923: }
0924:
0925: /**
0926: * The cycle bound is a multiple of the period, plus optionally a start
0927: * offset.
0928: * <P>
0929: * <pre>cb = n * period + offset</pre><br>
0930: *
0931: * @return The current period.
0932: *
0933: * @see #getCycleBound()
0934: */
0935: public double getPeriod() {
0936: return this .period;
0937: }
0938:
0939: /**
0940: * The cycle bound is a multiple of the period, plus optionally a start
0941: * offset.
0942: * <P>
0943: * <pre>cb = n * period + offset</pre><br>
0944: *
0945: * @param period The period to set.
0946: *
0947: * @see #getCycleBound()
0948: */
0949: public void setPeriod(double period) {
0950: this .period = period;
0951: }
0952:
0953: /**
0954: * Draws the tick marks and labels.
0955: *
0956: * @param g2 the graphics device.
0957: * @param cursor the cursor.
0958: * @param plotArea the plot area.
0959: * @param dataArea the area inside the axes.
0960: * @param edge the side on which the axis is displayed.
0961: *
0962: * @return The axis state.
0963: */
0964: protected AxisState drawTickMarksAndLabels(Graphics2D g2,
0965: double cursor, Rectangle2D plotArea, Rectangle2D dataArea,
0966: RectangleEdge edge) {
0967: this .internalMarkerWhenTicksOverlap = false;
0968: AxisState ret = super .drawTickMarksAndLabels(g2, cursor,
0969: plotArea, dataArea, edge);
0970:
0971: // continue and separate the labels only if necessary
0972: if (!this .internalMarkerWhenTicksOverlap) {
0973: return ret;
0974: }
0975:
0976: double ol = getTickMarkOutsideLength();
0977: FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
0978:
0979: if (isVerticalTickLabels()) {
0980: ol = fm.getMaxAdvance();
0981: } else {
0982: ol = fm.getHeight();
0983: }
0984:
0985: double il = 0;
0986: if (isTickMarksVisible()) {
0987: float xx = (float) valueToJava2D(
0988: getRange().getUpperBound(), dataArea, edge);
0989: Line2D mark = null;
0990: g2.setStroke(getTickMarkStroke());
0991: g2.setPaint(getTickMarkPaint());
0992: if (edge == RectangleEdge.LEFT) {
0993: mark = new Line2D.Double(cursor - ol, xx, cursor + il,
0994: xx);
0995: } else if (edge == RectangleEdge.RIGHT) {
0996: mark = new Line2D.Double(cursor + ol, xx, cursor - il,
0997: xx);
0998: } else if (edge == RectangleEdge.TOP) {
0999: mark = new Line2D.Double(xx, cursor - ol, xx, cursor
1000: + il);
1001: } else if (edge == RectangleEdge.BOTTOM) {
1002: mark = new Line2D.Double(xx, cursor + ol, xx, cursor
1003: - il);
1004: }
1005: g2.draw(mark);
1006: }
1007: return ret;
1008: }
1009:
1010: /**
1011: * Draws the axis.
1012: *
1013: * @param g2 the graphics device (<code>null</code> not permitted).
1014: * @param cursor the cursor position.
1015: * @param plotArea the plot area (<code>null</code> not permitted).
1016: * @param dataArea the data area (<code>null</code> not permitted).
1017: * @param edge the edge (<code>null</code> not permitted).
1018: * @param plotState collects information about the plot
1019: * (<code>null</code> permitted).
1020: *
1021: * @return The axis state (never <code>null</code>).
1022: */
1023: public AxisState draw(Graphics2D g2, double cursor,
1024: Rectangle2D plotArea, Rectangle2D dataArea,
1025: RectangleEdge edge, PlotRenderingInfo plotState) {
1026:
1027: AxisState ret = super .draw(g2, cursor, plotArea, dataArea,
1028: edge, plotState);
1029: if (isAdvanceLineVisible()) {
1030: double xx = valueToJava2D(getRange().getUpperBound(),
1031: dataArea, edge);
1032: Line2D mark = null;
1033: g2.setStroke(getAdvanceLineStroke());
1034: g2.setPaint(getAdvanceLinePaint());
1035: if (edge == RectangleEdge.LEFT) {
1036: mark = new Line2D.Double(cursor, xx, cursor
1037: + dataArea.getWidth(), xx);
1038: } else if (edge == RectangleEdge.RIGHT) {
1039: mark = new Line2D.Double(cursor - dataArea.getWidth(),
1040: xx, cursor, xx);
1041: } else if (edge == RectangleEdge.TOP) {
1042: mark = new Line2D.Double(xx, cursor
1043: + dataArea.getHeight(), xx, cursor);
1044: } else if (edge == RectangleEdge.BOTTOM) {
1045: mark = new Line2D.Double(xx, cursor, xx, cursor
1046: - dataArea.getHeight());
1047: }
1048: g2.draw(mark);
1049: }
1050: return ret;
1051: }
1052:
1053: /**
1054: * Reserve some space on each axis side because we draw a centered label at
1055: * each extremity.
1056: *
1057: * @param g2 the graphics device.
1058: * @param plot the plot.
1059: * @param plotArea the plot area.
1060: * @param edge the edge.
1061: * @param space the space already reserved.
1062: *
1063: * @return The reserved space.
1064: */
1065: public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
1066: Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
1067:
1068: this .internalMarkerCycleBoundTick = null;
1069: AxisSpace ret = super .reserveSpace(g2, plot, plotArea, edge,
1070: space);
1071: if (this .internalMarkerCycleBoundTick == null) {
1072: return ret;
1073: }
1074:
1075: FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
1076: Rectangle2D r = TextUtilities.getTextBounds(
1077: this .internalMarkerCycleBoundTick.getText(), g2, fm);
1078:
1079: if (RectangleEdge.isTopOrBottom(edge)) {
1080: if (isVerticalTickLabels()) {
1081: space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
1082: } else {
1083: space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
1084: }
1085: } else if (RectangleEdge.isLeftOrRight(edge)) {
1086: if (isVerticalTickLabels()) {
1087: space.add(r.getWidth() / 2, RectangleEdge.TOP);
1088: } else {
1089: space.add(r.getHeight() / 2, RectangleEdge.TOP);
1090: }
1091: }
1092:
1093: return ret;
1094:
1095: }
1096:
1097: /**
1098: * Provides serialization support.
1099: *
1100: * @param stream the output stream.
1101: *
1102: * @throws IOException if there is an I/O error.
1103: */
1104: private void writeObject(ObjectOutputStream stream)
1105: throws IOException {
1106:
1107: stream.defaultWriteObject();
1108: SerialUtilities.writePaint(this .advanceLinePaint, stream);
1109: SerialUtilities.writeStroke(this .advanceLineStroke, stream);
1110:
1111: }
1112:
1113: /**
1114: * Provides serialization support.
1115: *
1116: * @param stream the input stream.
1117: *
1118: * @throws IOException if there is an I/O error.
1119: * @throws ClassNotFoundException if there is a classpath problem.
1120: */
1121: private void readObject(ObjectInputStream stream)
1122: throws IOException, ClassNotFoundException {
1123:
1124: stream.defaultReadObject();
1125: this .advanceLinePaint = SerialUtilities.readPaint(stream);
1126: this .advanceLineStroke = SerialUtilities.readStroke(stream);
1127:
1128: }
1129:
1130: /**
1131: * Tests the axis for equality with another object.
1132: *
1133: * @param obj the object to test against.
1134: *
1135: * @return A boolean.
1136: */
1137: public boolean equals(Object obj) {
1138: if (obj == this ) {
1139: return true;
1140: }
1141: if (!(obj instanceof CyclicNumberAxis)) {
1142: return false;
1143: }
1144: if (!super .equals(obj)) {
1145: return false;
1146: }
1147: CyclicNumberAxis that = (CyclicNumberAxis) obj;
1148: if (this .period != that.period) {
1149: return false;
1150: }
1151: if (this .offset != that.offset) {
1152: return false;
1153: }
1154: if (!PaintUtilities.equal(this .advanceLinePaint,
1155: that.advanceLinePaint)) {
1156: return false;
1157: }
1158: if (!ObjectUtilities.equal(this .advanceLineStroke,
1159: that.advanceLineStroke)) {
1160: return false;
1161: }
1162: if (this .advanceLineVisible != that.advanceLineVisible) {
1163: return false;
1164: }
1165: if (this .boundMappedToLastCycle != that.boundMappedToLastCycle) {
1166: return false;
1167: }
1168: return true;
1169: }
1170: }
|