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