001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------------
028 * ModuloAxis.java
029 * ---------------
030 * (C) Copyright 2004-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.chart.axis;
038
039import java.awt.geom.Rectangle2D;
040
041import org.jfree.chart.event.AxisChangeEvent;
042import org.jfree.chart.ui.RectangleEdge;
043import org.jfree.data.Range;
044
045/**
046 * An axis that displays numerical values within a fixed range using a modulo
047 * calculation.
048 */
049public class ModuloAxis extends NumberAxis {
050
051    /**
052     * The fixed range for the axis - all data values will be mapped to this
053     * range using a modulo calculation.
054     */
055    private Range fixedRange;
056
057    /**
058     * The display start value (this will sometimes be > displayEnd, in which
059     * case the axis wraps around at some point in the middle of the axis).
060     */
061    private double displayStart;
062
063    /**
064     * The display end value.
065     */
066    private double displayEnd;
067
068    /**
069     * Creates a new axis.
070     *
071     * @param label  the axis label ({@code null} permitted).
072     * @param fixedRange  the fixed range ({@code null} not permitted).
073     */
074    public ModuloAxis(String label, Range fixedRange) {
075        super(label);
076        this.fixedRange = fixedRange;
077        this.displayStart = 270.0;
078        this.displayEnd = 90.0;
079    }
080
081    /**
082     * Returns the display start value.
083     *
084     * @return The display start value.
085     */
086    public double getDisplayStart() {
087        return this.displayStart;
088    }
089
090    /**
091     * Returns the display end value.
092     *
093     * @return The display end value.
094     */
095    public double getDisplayEnd() {
096        return this.displayEnd;
097    }
098
099    /**
100     * Sets the display range.  The values will be mapped to the fixed range if
101     * necessary.
102     *
103     * @param start  the start value.
104     * @param end  the end value.
105     */
106    public void setDisplayRange(double start, double end) {
107        this.displayStart = mapValueToFixedRange(start);
108        this.displayEnd = mapValueToFixedRange(end);
109        if (this.displayStart < this.displayEnd) {
110            setRange(this.displayStart, this.displayEnd);
111        }
112        else {
113            setRange(this.displayStart, this.fixedRange.getUpperBound()
114                  + (this.displayEnd - this.fixedRange.getLowerBound()));
115        }
116        notifyListeners(new AxisChangeEvent(this));
117    }
118
119    /**
120     * This method should calculate a range that will show all the data values.
121     * For now, it just sets the axis range to the fixedRange.
122     */
123    @Override
124    protected void autoAdjustRange() {
125        setRange(this.fixedRange, false, false);
126    }
127
128    /**
129     * Translates a data value to a Java2D coordinate.
130     *
131     * @param value  the value.
132     * @param area  the area.
133     * @param edge  the edge.
134     *
135     * @return A Java2D coordinate.
136     */
137    @Override
138    public double valueToJava2D(double value, Rectangle2D area,
139                                RectangleEdge edge) {
140        double result;
141        double v = mapValueToFixedRange(value);
142        if (this.displayStart < this.displayEnd) {  // regular number axis
143            result = trans(v, area, edge);
144        }
145        else {  // displayStart > displayEnd, need to handle split
146            double cutoff = (this.displayStart + this.displayEnd) / 2.0;
147            double length1 = this.fixedRange.getUpperBound()
148                             - this.displayStart;
149            double length2 = this.displayEnd - this.fixedRange.getLowerBound();
150            if (v > cutoff) {
151                result = transStart(v, area, edge, length1, length2);
152            }
153            else {
154                result = transEnd(v, area, edge, length1, length2);
155            }
156        }
157        return result;
158    }
159
160    /**
161     * A regular translation from a data value to a Java2D value.
162     *
163     * @param value  the value.
164     * @param area  the data area.
165     * @param edge  the edge along which the axis lies.
166     *
167     * @return The Java2D coordinate.
168     */
169    private double trans(double value, Rectangle2D area, RectangleEdge edge) {
170        double min = 0.0;
171        double max = 0.0;
172        if (RectangleEdge.isTopOrBottom(edge)) {
173            min = area.getX();
174            max = area.getX() + area.getWidth();
175        }
176        else if (RectangleEdge.isLeftOrRight(edge)) {
177            min = area.getMaxY();
178            max = area.getMaxY() - area.getHeight();
179        }
180        if (isInverted()) {
181            return max - ((value - this.displayStart)
182                   / (this.displayEnd - this.displayStart)) * (max - min);
183        }
184        else {
185            return min + ((value - this.displayStart)
186                   / (this.displayEnd - this.displayStart)) * (max - min);
187        }
188
189    }
190
191    /**
192     * Translates a data value to a Java2D value for the first section of the
193     * axis.
194     *
195     * @param value  the value.
196     * @param area  the data area.
197     * @param edge  the edge along which the axis lies.
198     * @param length1  the length of the first section.
199     * @param length2  the length of the second section.
200     *
201     * @return The Java2D coordinate.
202     */
203    private double transStart(double value, Rectangle2D area,
204                              RectangleEdge edge,
205                              double length1, double length2) {
206        double min = 0.0;
207        double max = 0.0;
208        if (RectangleEdge.isTopOrBottom(edge)) {
209            min = area.getX();
210            max = area.getX() + area.getWidth() * length1 / (length1 + length2);
211        }
212        else if (RectangleEdge.isLeftOrRight(edge)) {
213            min = area.getMaxY();
214            max = area.getMaxY() - area.getHeight() * length1
215                  / (length1 + length2);
216        }
217        if (isInverted()) {
218            return max - ((value - this.displayStart)
219                / (this.fixedRange.getUpperBound() - this.displayStart))
220                * (max - min);
221        }
222        else {
223            return min + ((value - this.displayStart)
224                / (this.fixedRange.getUpperBound() - this.displayStart))
225                * (max - min);
226        }
227
228    }
229
230    /**
231     * Translates a data value to a Java2D value for the second section of the
232     * axis.
233     *
234     * @param value  the value.
235     * @param area  the data area.
236     * @param edge  the edge along which the axis lies.
237     * @param length1  the length of the first section.
238     * @param length2  the length of the second section.
239     *
240     * @return The Java2D coordinate.
241     */
242    private double transEnd(double value, Rectangle2D area, RectangleEdge edge,
243                            double length1, double length2) {
244        double min = 0.0;
245        double max = 0.0;
246        if (RectangleEdge.isTopOrBottom(edge)) {
247            max = area.getMaxX();
248            min = area.getMaxX() - area.getWidth() * length2
249                  / (length1 + length2);
250        }
251        else if (RectangleEdge.isLeftOrRight(edge)) {
252            max = area.getMinY();
253            min = area.getMinY() + area.getHeight() * length2
254                  / (length1 + length2);
255        }
256        if (isInverted()) {
257            return max - ((value - this.fixedRange.getLowerBound())
258                    / (this.displayEnd - this.fixedRange.getLowerBound()))
259                    * (max - min);
260        }
261        else {
262            return min + ((value - this.fixedRange.getLowerBound())
263                    / (this.displayEnd - this.fixedRange.getLowerBound()))
264                    * (max - min);
265        }
266
267    }
268
269    /**
270     * Maps a data value into the fixed range.
271     *
272     * @param value  the value.
273     *
274     * @return The mapped value.
275     */
276    private double mapValueToFixedRange(double value) {
277        double lower = this.fixedRange.getLowerBound();
278        double length = this.fixedRange.getLength();
279        if (value < lower) {
280            return lower + length + ((value - lower) % length);
281        }
282        else {
283            return lower + ((value - lower) % length);
284        }
285    }
286
287    /**
288     * Translates a Java2D coordinate into a data value.
289     *
290     * @param java2DValue  the Java2D coordinate.
291     * @param area  the area.
292     * @param edge  the edge.
293     *
294     * @return The Java2D coordinate.
295     */
296    @Override
297    public double java2DToValue(double java2DValue, Rectangle2D area,
298            RectangleEdge edge) {
299        double result = 0.0;
300        if (this.displayStart < this.displayEnd) {  // regular number axis
301            result = super.java2DToValue(java2DValue, area, edge);
302        }
303        else {  // displayStart > displayEnd, need to handle split
304
305        }
306        return result;
307    }
308
309    /**
310     * Returns the display length for the axis.
311     *
312     * @return The display length.
313     */
314    private double getDisplayLength() {
315        if (this.displayStart < this.displayEnd) {
316            return (this.displayEnd - this.displayStart);
317        }
318        else {
319            return (this.fixedRange.getUpperBound() - this.displayStart)
320                + (this.displayEnd - this.fixedRange.getLowerBound());
321        }
322    }
323
324    /**
325     * Returns the central value of the current display range.
326     *
327     * @return The central value.
328     */
329    private double getDisplayCentralValue() {
330        return mapValueToFixedRange(this.displayStart 
331                + (getDisplayLength() / 2));
332    }
333
334    /**
335     * Increases or decreases the axis range by the specified percentage about
336     * the central value and sends an {@link AxisChangeEvent} to all registered
337     * listeners.
338     * <P>
339     * To double the length of the axis range, use 200% (2.0).
340     * To halve the length of the axis range, use 50% (0.5).
341     *
342     * @param percent  the resize factor.
343     */
344    @Override
345    public void resizeRange(double percent) {
346        resizeRange(percent, getDisplayCentralValue());
347    }
348
349    /**
350     * Increases or decreases the axis range by the specified percentage about
351     * the specified anchor value and sends an {@link AxisChangeEvent} to all
352     * registered listeners.
353     * <P>
354     * To double the length of the axis range, use 200% (2.0).
355     * To halve the length of the axis range, use 50% (0.5).
356     *
357     * @param percent  the resize factor.
358     * @param anchorValue  the new central value after the resize.
359     */
360    @Override
361    public void resizeRange(double percent, double anchorValue) {
362
363        if (percent > 0.0) {
364            double halfLength = getDisplayLength() * percent / 2;
365            setDisplayRange(anchorValue - halfLength, anchorValue + halfLength);
366        }
367        else {
368            setAutoRange(true);
369        }
370
371    }
372
373    /**
374     * Converts a length in data coordinates into the corresponding length in
375     * Java2D coordinates.
376     *
377     * @param length  the length.
378     * @param area  the plot area.
379     * @param edge  the edge along which the axis lies.
380     *
381     * @return The length in Java2D coordinates.
382     */
383    @Override
384    public double lengthToJava2D(double length, Rectangle2D area,
385                                 RectangleEdge edge) {
386        double axisLength = 0.0;
387        if (this.displayEnd > this.displayStart) {
388            axisLength = this.displayEnd - this.displayStart;
389        }
390        else {
391            axisLength = (this.fixedRange.getUpperBound() - this.displayStart)
392                + (this.displayEnd - this.fixedRange.getLowerBound());
393        }
394        double areaLength;
395        if (RectangleEdge.isLeftOrRight(edge)) {
396            areaLength = area.getHeight();
397        }
398        else {
399            areaLength = area.getWidth();
400        }
401        return (length / axisLength) * areaLength;
402    }
403
404    /**
405     * Tests this axis for equality with an arbitrary object.
406     *
407     * @param obj  the object ({@code null} permitted).
408     *
409     * @return A boolean.
410     */
411    @Override
412    public boolean equals(Object obj) {
413        if (obj == this) {
414            return true;
415        }
416        if (!(obj instanceof ModuloAxis)) {
417            return false;
418        }
419        ModuloAxis that = (ModuloAxis) obj;
420        if (this.displayStart != that.displayStart) {
421            return false;
422        }
423        if (this.displayEnd != that.displayEnd) {
424            return false;
425        }
426        if (!this.fixedRange.equals(that.fixedRange)) {
427            return false;
428        }
429        return super.equals(obj);
430    }
431
432}