001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, 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-2022, 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.api.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, RectangleEdge edge,
204            double length1, double length2) {
205        double min = 0.0;
206        double max = 0.0;
207        if (RectangleEdge.isTopOrBottom(edge)) {
208            min = area.getX();
209            max = area.getX() + area.getWidth() * length1 / (length1 + length2);
210        }
211        else if (RectangleEdge.isLeftOrRight(edge)) {
212            min = area.getMaxY();
213            max = area.getMaxY() - area.getHeight() * length1
214                  / (length1 + length2);
215        }
216        if (isInverted()) {
217            return max - ((value - this.displayStart)
218                / (this.fixedRange.getUpperBound() - this.displayStart))
219                * (max - min);
220        }
221        else {
222            return min + ((value - this.displayStart)
223                / (this.fixedRange.getUpperBound() - this.displayStart))
224                * (max - min);
225        }
226
227    }
228
229    /**
230     * Translates a data value to a Java2D value for the second section of the
231     * axis.
232     *
233     * @param value  the value.
234     * @param area  the data area.
235     * @param edge  the edge along which the axis lies.
236     * @param length1  the length of the first section.
237     * @param length2  the length of the second section.
238     *
239     * @return The Java2D coordinate.
240     */
241    private double transEnd(double value, Rectangle2D area, RectangleEdge edge,
242            double length1, double length2) {
243        double min = 0.0;
244        double max = 0.0;
245        if (RectangleEdge.isTopOrBottom(edge)) {
246            max = area.getMaxX();
247            min = area.getMaxX() - area.getWidth() * length2
248                  / (length1 + length2);
249        }
250        else if (RectangleEdge.isLeftOrRight(edge)) {
251            max = area.getMinY();
252            min = area.getMinY() + area.getHeight() * length2
253                  / (length1 + length2);
254        }
255        if (isInverted()) {
256            return max - ((value - this.fixedRange.getLowerBound())
257                    / (this.displayEnd - this.fixedRange.getLowerBound()))
258                    * (max - min);
259        }
260        else {
261            return min + ((value - this.fixedRange.getLowerBound())
262                    / (this.displayEnd - this.fixedRange.getLowerBound()))
263                    * (max - min);
264        }
265
266    }
267
268    /**
269     * Maps a data value into the fixed range.
270     *
271     * @param value  the value.
272     *
273     * @return The mapped value.
274     */
275    private double mapValueToFixedRange(double value) {
276        double lower = this.fixedRange.getLowerBound();
277        double length = this.fixedRange.getLength();
278        if (value < lower) {
279            return lower + length + ((value - lower) % length);
280        }
281        else {
282            return lower + ((value - lower) % length);
283        }
284    }
285
286    /**
287     * Translates a Java2D coordinate into a data value.
288     *
289     * @param java2DValue  the Java2D coordinate.
290     * @param area  the area.
291     * @param edge  the edge.
292     *
293     * @return The Java2D coordinate.
294     */
295    @Override
296    public double java2DToValue(double java2DValue, Rectangle2D area,
297            RectangleEdge edge) {
298        double result = 0.0;
299        if (this.displayStart < this.displayEnd) {  // regular number axis
300            result = super.java2DToValue(java2DValue, area, edge);
301        }
302        else {  // displayStart > displayEnd, need to handle split
303
304        }
305        return result;
306    }
307
308    /**
309     * Returns the display length for the axis.
310     *
311     * @return The display length.
312     */
313    private double getDisplayLength() {
314        if (this.displayStart < this.displayEnd) {
315            return (this.displayEnd - this.displayStart);
316        }
317        else {
318            return (this.fixedRange.getUpperBound() - this.displayStart)
319                + (this.displayEnd - this.fixedRange.getLowerBound());
320        }
321    }
322
323    /**
324     * Returns the central value of the current display range.
325     *
326     * @return The central value.
327     */
328    private double getDisplayCentralValue() {
329        return mapValueToFixedRange(this.displayStart 
330                + (getDisplayLength() / 2));
331    }
332
333    /**
334     * Increases or decreases the axis range by the specified percentage about
335     * the central value and sends an {@link AxisChangeEvent} to all registered
336     * listeners.
337     * <P>
338     * To double the length of the axis range, use 200% (2.0).
339     * To halve the length of the axis range, use 50% (0.5).
340     *
341     * @param percent  the resize factor.
342     */
343    @Override
344    public void resizeRange(double percent) {
345        resizeRange(percent, getDisplayCentralValue());
346    }
347
348    /**
349     * Increases or decreases the axis range by the specified percentage about
350     * the specified anchor value and sends an {@link AxisChangeEvent} to all
351     * registered listeners.
352     * <P>
353     * To double the length of the axis range, use 200% (2.0).
354     * To halve the length of the axis range, use 50% (0.5).
355     *
356     * @param percent  the resize factor.
357     * @param anchorValue  the new central value after the resize.
358     */
359    @Override
360    public void resizeRange(double percent, double anchorValue) {
361        if (percent > 0.0) {
362            double halfLength = getDisplayLength() * percent / 2;
363            setDisplayRange(anchorValue - halfLength, anchorValue + halfLength);
364        }
365        else {
366            setAutoRange(true);
367        }
368    }
369
370    /**
371     * Converts a length in data coordinates into the corresponding length in
372     * Java2D coordinates.
373     *
374     * @param length  the length.
375     * @param area  the plot area.
376     * @param edge  the edge along which the axis lies.
377     *
378     * @return The length in Java2D coordinates.
379     */
380    @Override
381    public double lengthToJava2D(double length, Rectangle2D area,
382            RectangleEdge edge) {
383        double axisLength;
384        if (this.displayEnd > this.displayStart) {
385            axisLength = this.displayEnd - this.displayStart;
386        } else {
387            axisLength = (this.fixedRange.getUpperBound() - this.displayStart)
388                + (this.displayEnd - this.fixedRange.getLowerBound());
389        }
390        double areaLength;
391        if (RectangleEdge.isLeftOrRight(edge)) {
392            areaLength = area.getHeight();
393        } else {
394            areaLength = area.getWidth();
395        }
396        return (length / axisLength) * areaLength;
397    }
398
399    /**
400     * Tests this axis for equality with an arbitrary object.
401     *
402     * @param obj  the object ({@code null} permitted).
403     *
404     * @return A boolean.
405     */
406    @Override
407    public boolean equals(Object obj) {
408        if (obj == this) {
409            return true;
410        }
411        if (!(obj instanceof ModuloAxis)) {
412            return false;
413        }
414        ModuloAxis that = (ModuloAxis) obj;
415        if (this.displayStart != that.displayStart) {
416            return false;
417        }
418        if (this.displayEnd != that.displayEnd) {
419            return false;
420        }
421        if (!this.fixedRange.equals(that.fixedRange)) {
422            return false;
423        }
424        return super.equals(obj);
425    }
426
427}