001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [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-2014, 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 042package org.jfree.chart.axis; 043 044import java.awt.geom.Rectangle2D; 045 046import org.jfree.chart.event.AxisChangeEvent; 047import org.jfree.data.Range; 048import org.jfree.ui.RectangleEdge; 049 050/** 051 * An axis that displays numerical values within a fixed range using a modulo 052 * calculation. 053 */ 054public 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 @Override 129 protected void autoAdjustRange() { 130 setRange(this.fixedRange, false, false); 131 } 132 133 /** 134 * Translates a data value to a Java2D coordinate. 135 * 136 * @param value the value. 137 * @param area the area. 138 * @param edge the edge. 139 * 140 * @return A Java2D coordinate. 141 */ 142 @Override 143 public double valueToJava2D(double value, Rectangle2D area, 144 RectangleEdge edge) { 145 double result; 146 double v = mapValueToFixedRange(value); 147 if (this.displayStart < this.displayEnd) { // regular number axis 148 result = trans(v, area, edge); 149 } 150 else { // displayStart > displayEnd, need to handle split 151 double cutoff = (this.displayStart + this.displayEnd) / 2.0; 152 double length1 = this.fixedRange.getUpperBound() 153 - this.displayStart; 154 double length2 = this.displayEnd - this.fixedRange.getLowerBound(); 155 if (v > cutoff) { 156 result = transStart(v, area, edge, length1, length2); 157 } 158 else { 159 result = transEnd(v, area, edge, length1, length2); 160 } 161 } 162 return result; 163 } 164 165 /** 166 * A regular translation from a data value to a Java2D value. 167 * 168 * @param value the value. 169 * @param area the data area. 170 * @param edge the edge along which the axis lies. 171 * 172 * @return The Java2D coordinate. 173 */ 174 private double trans(double value, Rectangle2D area, RectangleEdge edge) { 175 double min = 0.0; 176 double max = 0.0; 177 if (RectangleEdge.isTopOrBottom(edge)) { 178 min = area.getX(); 179 max = area.getX() + area.getWidth(); 180 } 181 else if (RectangleEdge.isLeftOrRight(edge)) { 182 min = area.getMaxY(); 183 max = area.getMaxY() - area.getHeight(); 184 } 185 if (isInverted()) { 186 return max - ((value - this.displayStart) 187 / (this.displayEnd - this.displayStart)) * (max - min); 188 } 189 else { 190 return min + ((value - this.displayStart) 191 / (this.displayEnd - this.displayStart)) * (max - min); 192 } 193 194 } 195 196 /** 197 * Translates a data value to a Java2D value for the first section of the 198 * axis. 199 * 200 * @param value the value. 201 * @param area the data area. 202 * @param edge the edge along which the axis lies. 203 * @param length1 the length of the first section. 204 * @param length2 the length of the second section. 205 * 206 * @return The Java2D coordinate. 207 */ 208 private double transStart(double value, Rectangle2D area, 209 RectangleEdge edge, 210 double length1, double length2) { 211 double min = 0.0; 212 double max = 0.0; 213 if (RectangleEdge.isTopOrBottom(edge)) { 214 min = area.getX(); 215 max = area.getX() + area.getWidth() * length1 / (length1 + length2); 216 } 217 else if (RectangleEdge.isLeftOrRight(edge)) { 218 min = area.getMaxY(); 219 max = area.getMaxY() - area.getHeight() * length1 220 / (length1 + length2); 221 } 222 if (isInverted()) { 223 return max - ((value - this.displayStart) 224 / (this.fixedRange.getUpperBound() - this.displayStart)) 225 * (max - min); 226 } 227 else { 228 return min + ((value - this.displayStart) 229 / (this.fixedRange.getUpperBound() - this.displayStart)) 230 * (max - min); 231 } 232 233 } 234 235 /** 236 * Translates a data value to a Java2D value for the second section of the 237 * axis. 238 * 239 * @param value the value. 240 * @param area the data area. 241 * @param edge the edge along which the axis lies. 242 * @param length1 the length of the first section. 243 * @param length2 the length of the second section. 244 * 245 * @return The Java2D coordinate. 246 */ 247 private double transEnd(double value, Rectangle2D area, RectangleEdge edge, 248 double length1, double length2) { 249 double min = 0.0; 250 double max = 0.0; 251 if (RectangleEdge.isTopOrBottom(edge)) { 252 max = area.getMaxX(); 253 min = area.getMaxX() - area.getWidth() * length2 254 / (length1 + length2); 255 } 256 else if (RectangleEdge.isLeftOrRight(edge)) { 257 max = area.getMinY(); 258 min = area.getMinY() + area.getHeight() * length2 259 / (length1 + length2); 260 } 261 if (isInverted()) { 262 return max - ((value - this.fixedRange.getLowerBound()) 263 / (this.displayEnd - this.fixedRange.getLowerBound())) 264 * (max - min); 265 } 266 else { 267 return min + ((value - this.fixedRange.getLowerBound()) 268 / (this.displayEnd - this.fixedRange.getLowerBound())) 269 * (max - min); 270 } 271 272 } 273 274 /** 275 * Maps a data value into the fixed range. 276 * 277 * @param value the value. 278 * 279 * @return The mapped value. 280 */ 281 private double mapValueToFixedRange(double value) { 282 double lower = this.fixedRange.getLowerBound(); 283 double length = this.fixedRange.getLength(); 284 if (value < lower) { 285 return lower + length + ((value - lower) % length); 286 } 287 else { 288 return lower + ((value - lower) % length); 289 } 290 } 291 292 /** 293 * Translates a Java2D coordinate into a data value. 294 * 295 * @param java2DValue the Java2D coordinate. 296 * @param area the area. 297 * @param edge the edge. 298 * 299 * @return The Java2D coordinate. 300 */ 301 @Override 302 public double java2DToValue(double java2DValue, Rectangle2D area, 303 RectangleEdge edge) { 304 double result = 0.0; 305 if (this.displayStart < this.displayEnd) { // regular number axis 306 result = super.java2DToValue(java2DValue, area, edge); 307 } 308 else { // displayStart > displayEnd, need to handle split 309 310 } 311 return result; 312 } 313 314 /** 315 * Returns the display length for the axis. 316 * 317 * @return The display length. 318 */ 319 private double getDisplayLength() { 320 if (this.displayStart < this.displayEnd) { 321 return (this.displayEnd - this.displayStart); 322 } 323 else { 324 return (this.fixedRange.getUpperBound() - this.displayStart) 325 + (this.displayEnd - this.fixedRange.getLowerBound()); 326 } 327 } 328 329 /** 330 * Returns the central value of the current display range. 331 * 332 * @return The central value. 333 */ 334 private double getDisplayCentralValue() { 335 return mapValueToFixedRange(this.displayStart 336 + (getDisplayLength() / 2)); 337 } 338 339 /** 340 * Increases or decreases the axis range by the specified percentage about 341 * the central value and sends an {@link AxisChangeEvent} to all registered 342 * listeners. 343 * <P> 344 * To double the length of the axis range, use 200% (2.0). 345 * To halve the length of the axis range, use 50% (0.5). 346 * 347 * @param percent the resize factor. 348 */ 349 @Override 350 public void resizeRange(double percent) { 351 resizeRange(percent, getDisplayCentralValue()); 352 } 353 354 /** 355 * Increases or decreases the axis range by the specified percentage about 356 * the specified anchor value and sends an {@link AxisChangeEvent} to all 357 * registered listeners. 358 * <P> 359 * To double the length of the axis range, use 200% (2.0). 360 * To halve the length of the axis range, use 50% (0.5). 361 * 362 * @param percent the resize factor. 363 * @param anchorValue the new central value after the resize. 364 */ 365 @Override 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 @Override 389 public double lengthToJava2D(double length, Rectangle2D area, 390 RectangleEdge edge) { 391 double axisLength = 0.0; 392 if (this.displayEnd > this.displayStart) { 393 axisLength = this.displayEnd - this.displayStart; 394 } 395 else { 396 axisLength = (this.fixedRange.getUpperBound() - this.displayStart) 397 + (this.displayEnd - this.fixedRange.getLowerBound()); 398 } 399 double areaLength; 400 if (RectangleEdge.isLeftOrRight(edge)) { 401 areaLength = area.getHeight(); 402 } 403 else { 404 areaLength = area.getWidth(); 405 } 406 return (length / axisLength) * areaLength; 407 } 408 409 /** 410 * Tests this axis for equality with an arbitrary object. 411 * 412 * @param obj the object (<code>null</code> permitted). 413 * 414 * @return A boolean. 415 */ 416 @Override 417 public boolean equals(Object obj) { 418 if (obj == this) { 419 return true; 420 } 421 if (!(obj instanceof ModuloAxis)) { 422 return false; 423 } 424 ModuloAxis that = (ModuloAxis) obj; 425 if (this.displayStart != that.displayStart) { 426 return false; 427 } 428 if (this.displayEnd != that.displayEnd) { 429 return false; 430 } 431 if (!this.fixedRange.equals(that.fixedRange)) { 432 return false; 433 } 434 return super.equals(obj); 435 } 436 437}