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