Source for org.jfree.chart.axis.ModuloAxis

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * ---------------
  28:  * ModuloAxis.java
  29:  * ---------------
  30:  * (C) Copyright 2004, 2007, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 13-Aug-2004 : Version 1 (DG);
  38:  * 13-Nov-2007 : Implemented equals() (DG);
  39:  * 
  40:  */
  41: 
  42: package org.jfree.chart.axis;
  43: 
  44: import java.awt.geom.Rectangle2D;
  45: 
  46: import org.jfree.chart.event.AxisChangeEvent;
  47: import org.jfree.data.Range;
  48: import org.jfree.ui.RectangleEdge;
  49: 
  50: /**
  51:  * An axis that displays numerical values within a fixed range using a modulo 
  52:  * calculation.
  53:  */
  54: public class ModuloAxis extends NumberAxis {
  55:     
  56:     /** 
  57:      * The fixed range for the axis - all data values will be mapped to this
  58:      * range using a modulo calculation. 
  59:      */
  60:     private Range fixedRange;
  61:     
  62:     /**
  63:      * The display start value (this will sometimes be > displayEnd, in which
  64:      * case the axis wraps around at some point in the middle of the axis).
  65:      */
  66:     private double displayStart;
  67:     
  68:     /**
  69:      * The display end value.
  70:      */
  71:     private double displayEnd;
  72:     
  73:     /**
  74:      * Creates a new axis.
  75:      * 
  76:      * @param label  the axis label (<code>null</code> permitted).
  77:      * @param fixedRange  the fixed range (<code>null</code> not permitted).
  78:      */
  79:     public ModuloAxis(String label, Range fixedRange) {
  80:         super(label);
  81:         this.fixedRange = fixedRange;
  82:         this.displayStart = 270.0;
  83:         this.displayEnd = 90.0;
  84:     }
  85: 
  86:     /**
  87:      * Returns the display start value.
  88:      * 
  89:      * @return The display start value.
  90:      */
  91:     public double getDisplayStart() {
  92:         return this.displayStart;
  93:     }
  94: 
  95:     /**
  96:      * Returns the display end value.
  97:      * 
  98:      * @return The display end value.
  99:      */
 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: }