001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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, 2007, 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 }