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