/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * --------------- * ModuloAxis.java * --------------- * (C) Copyright 2004-2014, by Object Refinery Limited. * * Original Author: David Gilbert (for Object Refinery Limited); * Contributor(s): -; * * Changes * ------- * 13-Aug-2004 : Version 1 (DG); * 13-Nov-2007 : Implemented equals() (DG); * 17-Jun-2012 : Removed JCommon dependencies (DG); * */ package org.jfree.chart.axis; import java.awt.geom.Rectangle2D; import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.event.AxisChangeEvent; import org.jfree.data.Range; /** * An axis that displays numerical values within a fixed range using a modulo * calculation. */ public class ModuloAxis extends NumberAxis { /** * The fixed range for the axis - all data values will be mapped to this * range using a modulo calculation. */ private Range fixedRange; /** * The display start value (this will sometimes be after displayEnd, in * which case the axis wraps around at some point in the middle of the * axis). */ private double displayStart; /** * The display end value. */ private double displayEnd; /** * Creates a new axis. * * @param label the axis label (<code>null</code> permitted). * @param fixedRange the fixed range (<code>null</code> not permitted). */ public ModuloAxis(String label, Range fixedRange) { super(label); this.fixedRange = fixedRange; this.displayStart = 270.0; this.displayEnd = 90.0; } /** * Returns the display start value. * * @return The display start value. */ public double getDisplayStart() { return this.displayStart; } /** * Returns the display end value. * * @return The display end value. */ public double getDisplayEnd() { return this.displayEnd; } /** * Sets the display range. The values will be mapped to the fixed range if * necessary. * * @param start the start value. * @param end the end value. */ public void setDisplayRange(double start, double end) { this.displayStart = mapValueToFixedRange(start); this.displayEnd = mapValueToFixedRange(end); if (this.displayStart < this.displayEnd) { setRange(this.displayStart, this.displayEnd); } else { setRange(this.displayStart, this.fixedRange.getUpperBound() + (this.displayEnd - this.fixedRange.getLowerBound())); } notifyListeners(new AxisChangeEvent(this)); } /** * This method should calculate a range that will show all the data values. * For now, it just sets the axis range to the fixedRange. */ @Override protected void autoAdjustRange() { setRange(this.fixedRange, false, false); } /** * Translates a data value to a Java2D coordinate. * * @param value the value. * @param area the area. * @param edge the edge. * * @return A Java2D coordinate. */ @Override public double valueToJava2D(double value, Rectangle2D area, RectangleEdge edge) { double result = 0.0; double v = mapValueToFixedRange(value); if (this.displayStart < this.displayEnd) { // regular number axis result = trans(v, area, edge); } else { // displayStart > displayEnd, need to handle split double cutoff = (this.displayStart + this.displayEnd) / 2.0; double length1 = this.fixedRange.getUpperBound() - this.displayStart; double length2 = this.displayEnd - this.fixedRange.getLowerBound(); if (v > cutoff) { result = transStart(v, area, edge, length1, length2); } else { result = transEnd(v, area, edge, length1, length2); } } return result; } /** * A regular translation from a data value to a Java2D value. * * @param value the value. * @param area the data area. * @param edge the edge along which the axis lies. * * @return The Java2D coordinate. */ private double trans(double value, Rectangle2D area, RectangleEdge edge) { double min = 0.0; double max = 0.0; if (RectangleEdge.isTopOrBottom(edge)) { min = area.getX(); max = area.getX() + area.getWidth(); } else if (RectangleEdge.isLeftOrRight(edge)) { min = area.getMaxY(); max = area.getMaxY() - area.getHeight(); } if (isInverted()) { return max - ((value - this.displayStart) / (this.displayEnd - this.displayStart)) * (max - min); } else { return min + ((value - this.displayStart) / (this.displayEnd - this.displayStart)) * (max - min); } } /** * Translates a data value to a Java2D value for the first section of the * axis. * * @param value the value. * @param area the data area. * @param edge the edge along which the axis lies. * @param length1 the length of the first section. * @param length2 the length of the second section. * * @return The Java2D coordinate. */ private double transStart(double value, Rectangle2D area, RectangleEdge edge, double length1, double length2) { double min = 0.0; double max = 0.0; if (RectangleEdge.isTopOrBottom(edge)) { min = area.getX(); max = area.getX() + area.getWidth() * length1 / (length1 + length2); } else if (RectangleEdge.isLeftOrRight(edge)) { min = area.getMaxY(); max = area.getMaxY() - area.getHeight() * length1 / (length1 + length2); } if (isInverted()) { return max - ((value - this.displayStart) / (this.fixedRange.getUpperBound() - this.displayStart)) * (max - min); } else { return min + ((value - this.displayStart) / (this.fixedRange.getUpperBound() - this.displayStart)) * (max - min); } } /** * Translates a data value to a Java2D value for the second section of the * axis. * * @param value the value. * @param area the data area. * @param edge the edge along which the axis lies. * @param length1 the length of the first section. * @param length2 the length of the second section. * * @return The Java2D coordinate. */ private double transEnd(double value, Rectangle2D area, RectangleEdge edge, double length1, double length2) { double min = 0.0; double max = 0.0; if (RectangleEdge.isTopOrBottom(edge)) { max = area.getMaxX(); min = area.getMaxX() - area.getWidth() * length2 / (length1 + length2); } else if (RectangleEdge.isLeftOrRight(edge)) { max = area.getMinY(); min = area.getMinY() + area.getHeight() * length2 / (length1 + length2); } if (isInverted()) { return max - ((value - this.fixedRange.getLowerBound()) / (this.displayEnd - this.fixedRange.getLowerBound())) * (max - min); } else { return min + ((value - this.fixedRange.getLowerBound()) / (this.displayEnd - this.fixedRange.getLowerBound())) * (max - min); } } /** * Maps a data value into the fixed range. * * @param value the value. * * @return The mapped value. */ private double mapValueToFixedRange(double value) { double lower = this.fixedRange.getLowerBound(); double length = this.fixedRange.getLength(); if (value < lower) { return lower + length + ((value - lower) % length); } else { return lower + ((value - lower) % length); } } /** * Translates a Java2D coordinate into a data value. * * @param java2DValue the Java2D coordinate. * @param area the area. * @param edge the edge. * * @return The Java2D coordinate. */ @Override public double java2DToValue(double java2DValue, Rectangle2D area, RectangleEdge edge) { double result = 0.0; if (this.displayStart < this.displayEnd) { // regular number axis result = super.java2DToValue(java2DValue, area, edge); } else { // displayStart > displayEnd, need to handle split } return result; } /** * Returns the display length for the axis. * * @return The display length. */ private double getDisplayLength() { if (this.displayStart < this.displayEnd) { return (this.displayEnd - this.displayStart); } else { return (this.fixedRange.getUpperBound() - this.displayStart) + (this.displayEnd - this.fixedRange.getLowerBound()); } } /** * Returns the central value of the current display range. * * @return The central value. */ private double getDisplayCentralValue() { return mapValueToFixedRange( this.displayStart + (getDisplayLength() / 2) ); } /** * Increases or decreases the axis range by the specified percentage about * the central value and sends an {@link AxisChangeEvent} to all registered * listeners. * <P> * To double the length of the axis range, use 200% (2.0). * To halve the length of the axis range, use 50% (0.5). * * @param percent the resize factor. */ @Override public void resizeRange(double percent) { resizeRange(percent, getDisplayCentralValue()); } /** * Increases or decreases the axis range by the specified percentage about * the specified anchor value and sends an {@link AxisChangeEvent} to all * registered listeners. * <P> * To double the length of the axis range, use 200% (2.0). * To halve the length of the axis range, use 50% (0.5). * * @param percent the resize factor. * @param anchorValue the new central value after the resize. */ @Override public void resizeRange(double percent, double anchorValue) { if (percent > 0.0) { double halfLength = getDisplayLength() * percent / 2; setDisplayRange(anchorValue - halfLength, anchorValue + halfLength); } else { setAutoRange(true); } } /** * Converts a length in data coordinates into the corresponding length in * Java2D coordinates. * * @param length the length. * @param area the plot area. * @param edge the edge along which the axis lies. * * @return The length in Java2D coordinates. */ @Override public double lengthToJava2D(double length, Rectangle2D area, RectangleEdge edge) { double axisLength = 0.0; if (this.displayEnd > this.displayStart) { axisLength = this.displayEnd - this.displayStart; } else { axisLength = (this.fixedRange.getUpperBound() - this.displayStart) + (this.displayEnd - this.fixedRange.getLowerBound()); } double areaLength = 0.0; if (RectangleEdge.isLeftOrRight(edge)) { areaLength = area.getHeight(); } else { areaLength = area.getWidth(); } return (length / axisLength) * areaLength; } /** * Tests this axis for equality with an arbitrary object. * * @param obj the object (<code>null</code> permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof ModuloAxis)) { return false; } ModuloAxis that = (ModuloAxis) obj; if (this.displayStart != that.displayStart) { return false; } if (this.displayEnd != that.displayEnd) { return false; } if (!this.fixedRange.equals(that.fixedRange)) { return false; } return super.equals(obj); } }