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 }