Source for org.jfree.chart.axis.CyclicNumberAxis

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2005, 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:  * CyclicNumberAxis.java
  29:  * ---------------------
  30:  * (C) Copyright 2003, 2004, by Nicolas Brodu and Contributors.
  31:  *
  32:  * Original Author:  Nicolas Brodu;
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *
  35:  * $Id: CyclicNumberAxis.java,v 1.10.2.2 2005/10/25 20:37:34 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
  40:  * 16-Mar-2004 : Added plotState to draw() method (DG);
  41:  * 07-Apr-2004 : Modifed text bounds calculation (DG);
  42:  * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
  43:  *               argument in selectAutoTickUnit() (DG);
  44:  * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal
  45:  *               (for consistency with other classes) and removed unused
  46:  *               parameters (DG);
  47:  * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
  48:  *
  49:  */
  50: 
  51: package org.jfree.chart.axis;
  52: 
  53: import java.awt.BasicStroke;
  54: import java.awt.Color;
  55: import java.awt.Font;
  56: import java.awt.FontMetrics;
  57: import java.awt.Graphics2D;
  58: import java.awt.Paint;
  59: import java.awt.Stroke;
  60: import java.awt.geom.Line2D;
  61: import java.awt.geom.Rectangle2D;
  62: import java.io.IOException;
  63: import java.io.ObjectInputStream;
  64: import java.io.ObjectOutputStream;
  65: import java.text.NumberFormat;
  66: import java.util.List;
  67: 
  68: import org.jfree.chart.plot.Plot;
  69: import org.jfree.chart.plot.PlotRenderingInfo;
  70: import org.jfree.data.Range;
  71: import org.jfree.io.SerialUtilities;
  72: import org.jfree.text.TextUtilities;
  73: import org.jfree.ui.RectangleEdge;
  74: import org.jfree.ui.TextAnchor;
  75: import org.jfree.util.ObjectUtilities;
  76: import org.jfree.util.PaintUtilities;
  77: 
  78: /**
  79: This class extends NumberAxis and handles cycling.
  80:  
  81: Traditional representation of data in the range x0..x1
  82: <pre>
  83: |-------------------------|
  84: x0                       x1
  85: </pre> 
  86: 
  87: Here, the range bounds are at the axis extremities.
  88: With cyclic axis, however, the time is split in 
  89: "cycles", or "time frames", or the same duration : the period.
  90: 
  91: A cycle axis cannot by definition handle a larger interval 
  92: than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full 
  93: period can be represented with such an axis.
  94: 
  95: The cycle bound is the number between x0 and x1 which marks 
  96: the beginning of new time frame:
  97: <pre>
  98: |---------------------|----------------------------|
  99: x0                   cb                           x1
 100: <---previous cycle---><-------current cycle-------->
 101: </pre>
 102: 
 103: It is actually a multiple of the period, plus optionally 
 104: a start offset: <pre>cb = n * period + offset</pre>
 105: 
 106: Thus, by definition, two consecutive cycle bounds 
 107: period apart, which is precisely why it is called a 
 108: period.
 109: 
 110: The visual representation of a cyclic axis is like that:
 111: <pre>
 112: |----------------------------|---------------------|
 113: cb                         x1|x0                  cb
 114: <-------current cycle--------><---previous cycle--->
 115: </pre>
 116: 
 117: The cycle bound is at the axis ends, then current 
 118: cycle is shown, then the last cycle. When using 
 119: dynamic data, the visual effect is the current cycle 
 120: erases the last cycle as x grows. Then, the next cycle 
 121: bound is reached, and the process starts over, erasing 
 122: the previous cycle.
 123: 
 124: A Cyclic item renderer is provided to do exactly this.
 125: 
 126:  */
 127: public class CyclicNumberAxis extends NumberAxis {
 128: 
 129:     /** The default axis line stroke. */
 130:     public static Stroke DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke(1.0f);
 131:     
 132:     /** The default axis line paint. */
 133:     public static final Paint DEFAULT_ADVANCE_LINE_PAINT = Color.gray;
 134:     
 135:     /** The offset. */
 136:     protected double offset;
 137:     
 138:     /** The period.*/
 139:     protected double period;
 140:     
 141:     /** ??. */
 142:     protected boolean boundMappedToLastCycle;
 143:     
 144:     /** A flag that controls whether or not the advance line is visible. */
 145:     protected boolean advanceLineVisible;
 146: 
 147:     /** The advance line stroke. */
 148:     protected transient Stroke advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE;
 149:     
 150:     /** The advance line paint. */
 151:     protected transient Paint advanceLinePaint;
 152:     
 153:     private transient boolean internalMarkerWhenTicksOverlap;
 154:     private transient Tick internalMarkerCycleBoundTick;
 155:     
 156:     /** 
 157:      * Creates a CycleNumberAxis with the given period.
 158:      * 
 159:      * @param period  the period.
 160:      */
 161:     public CyclicNumberAxis(double period) {
 162:         this(period, 0.0);
 163:     }
 164: 
 165:     /** 
 166:      * Creates a CycleNumberAxis with the given period and offset.
 167:      * 
 168:      * @param period  the period.
 169:      * @param offset  the offset.
 170:      */
 171:     public CyclicNumberAxis(double period, double offset) {
 172:         this(period, offset, null);
 173:     }
 174: 
 175:     /** 
 176:      * Creates a named CycleNumberAxis with the given period.
 177:      * 
 178:      * @param period  the period.
 179:      * @param label  the label.
 180:      */
 181:     public CyclicNumberAxis(double period, String label) {
 182:         this(0, period, label);
 183:     }
 184:     
 185:     /** 
 186:      * Creates a named CycleNumberAxis with the given period and offset.
 187:      * 
 188:      * @param period  the period.
 189:      * @param offset  the offset.
 190:      * @param label  the label.
 191:      */
 192:     public CyclicNumberAxis(double period, double offset, String label) {
 193:         super(label);
 194:         this.period = period;
 195:         this.offset = offset;
 196:         setFixedAutoRange(period);
 197:         this.advanceLineVisible = true;
 198:         this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT;
 199:     }
 200:         
 201:     /**
 202:      * The advance line is the line drawn at the limit of the current cycle, 
 203:      * when erasing the previous cycle. 
 204:      * 
 205:      * @return A boolean.
 206:      */
 207:     public boolean isAdvanceLineVisible() {
 208:         return this.advanceLineVisible;
 209:     }
 210:     
 211:     /**
 212:      * The advance line is the line drawn at the limit of the current cycle, 
 213:      * when erasing the previous cycle. 
 214:      * 
 215:      * @param visible  the flag.
 216:      */
 217:     public void setAdvanceLineVisible(boolean visible) {
 218:         this.advanceLineVisible = visible;
 219:     }
 220:     
 221:     /**
 222:      * The advance line is the line drawn at the limit of the current cycle, 
 223:      * when erasing the previous cycle. 
 224:      * 
 225:      * @return The paint (never <code>null</code>).
 226:      */
 227:     public Paint getAdvanceLinePaint() {
 228:         return this.advanceLinePaint;
 229:     }
 230: 
 231:     /**
 232:      * The advance line is the line drawn at the limit of the current cycle, 
 233:      * when erasing the previous cycle. 
 234:      * 
 235:      * @param paint  the paint (<code>null</code> not permitted).
 236:      */
 237:     public void setAdvanceLinePaint(Paint paint) {
 238:         if (paint == null) {
 239:             throw new IllegalArgumentException("Null 'paint' argument.");
 240:         }
 241:         this.advanceLinePaint = paint;
 242:     }
 243:     
 244:     /**
 245:      * The advance line is the line drawn at the limit of the current cycle, 
 246:      * when erasing the previous cycle. 
 247:      * 
 248:      * @return The stroke (never <code>null</code>).
 249:      */
 250:     public Stroke getAdvanceLineStroke() {
 251:         return this.advanceLineStroke;
 252:     }
 253:     /**
 254:      * The advance line is the line drawn at the limit of the current cycle, 
 255:      * when erasing the previous cycle. 
 256:      * 
 257:      * @param stroke  the stroke (<code>null</code> not permitted).
 258:      */
 259:     public void setAdvanceLineStroke(Stroke stroke) {
 260:         if (stroke == null) {
 261:             throw new IllegalArgumentException("Null 'stroke' argument.");
 262:         }
 263:         this.advanceLineStroke = stroke;
 264:     }
 265:     
 266:     /**
 267:      * The cycle bound can be associated either with the current or with the 
 268:      * last cycle.  It's up to the user's choice to decide which, as this is 
 269:      * just a convention.  By default, the cycle bound is mapped to the current
 270:      * cycle.
 271:      * <br>
 272:      * Note that this has no effect on visual appearance, as the cycle bound is
 273:      * mapped successively for both axis ends. Use this function for correct 
 274:      * results in translateValueToJava2D. 
 275:      *  
 276:      * @return <code>true</code> if the cycle bound is mapped to the last 
 277:      *         cycle, <code>false</code> if it is bound to the current cycle 
 278:      *         (default)
 279:      */
 280:     public boolean isBoundMappedToLastCycle() {
 281:         return this.boundMappedToLastCycle;
 282:     }
 283:     
 284:     /**
 285:      * The cycle bound can be associated either with the current or with the 
 286:      * last cycle.  It's up to the user's choice to decide which, as this is 
 287:      * just a convention. By default, the cycle bound is mapped to the current 
 288:      * cycle. 
 289:      * <br>
 290:      * Note that this has no effect on visual appearance, as the cycle bound is
 291:      * mapped successively for both axis ends. Use this function for correct 
 292:      * results in valueToJava2D.
 293:      *  
 294:      * @param boundMappedToLastCycle Set it to true to map the cycle bound to 
 295:      *        the last cycle.
 296:      */
 297:     public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) {
 298:         this.boundMappedToLastCycle = boundMappedToLastCycle;
 299:     }
 300:     
 301:     /**
 302:      * Selects a tick unit when the axis is displayed horizontally.
 303:      * 
 304:      * @param g2  the graphics device.
 305:      * @param drawArea  the drawing area.
 306:      * @param dataArea  the data area.
 307:      * @param edge  the side of the rectangle on which the axis is displayed.
 308:      */
 309:     protected void selectHorizontalAutoTickUnit(Graphics2D g2,
 310:                                                 Rectangle2D drawArea, 
 311:                                                 Rectangle2D dataArea,
 312:                                                 RectangleEdge edge) {
 313: 
 314:         double tickLabelWidth 
 315:             = estimateMaximumTickLabelWidth(g2, getTickUnit());
 316:         
 317:         // Compute number of labels
 318:         double n = getRange().getLength() 
 319:                    * tickLabelWidth / dataArea.getWidth();
 320: 
 321:         setTickUnit(
 322:             (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 
 323:             false, false
 324:         );
 325:         
 326:      }
 327: 
 328:     /**
 329:      * Selects a tick unit when the axis is displayed vertically.
 330:      * 
 331:      * @param g2  the graphics device.
 332:      * @param drawArea  the drawing area.
 333:      * @param dataArea  the data area.
 334:      * @param edge  the side of the rectangle on which the axis is displayed.
 335:      */
 336:     protected void selectVerticalAutoTickUnit(Graphics2D g2,
 337:                                                 Rectangle2D drawArea, 
 338:                                                 Rectangle2D dataArea,
 339:                                                 RectangleEdge edge) {
 340: 
 341:         double tickLabelWidth 
 342:             = estimateMaximumTickLabelWidth(g2, getTickUnit());
 343: 
 344:         // Compute number of labels
 345:         double n = getRange().getLength() 
 346:                    * tickLabelWidth / dataArea.getHeight();
 347: 
 348:         setTickUnit(
 349:             (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 
 350:             false, false
 351:         );
 352:         
 353:      }
 354: 
 355:     /** 
 356:      * A special Number tick that also hold information about the cycle bound 
 357:      * mapping for this tick.  This is especially useful for having a tick at 
 358:      * each axis end with the cycle bound value.  See also 
 359:      * isBoundMappedToLastCycle()
 360:      */
 361:     protected static class CycleBoundTick extends NumberTick {
 362:         
 363:         /** Map to last cycle. */
 364:         public boolean mapToLastCycle;
 365:         
 366:         /**
 367:          * Creates a new tick.
 368:          * 
 369:          * @param mapToLastCycle  map to last cycle?
 370:          * @param number  the number.
 371:          * @param label  the label.
 372:          * @param textAnchor  the text anchor.
 373:          * @param rotationAnchor  the rotation anchor.
 374:          * @param angle  the rotation angle.
 375:          */
 376:         public CycleBoundTick(boolean mapToLastCycle, Number number, 
 377:                               String label, TextAnchor textAnchor,
 378:                               TextAnchor rotationAnchor, double angle) {
 379:             super(number, label, textAnchor, rotationAnchor, angle);
 380:             this.mapToLastCycle = mapToLastCycle;
 381:         }
 382:     }
 383:     
 384:     /**
 385:      * Calculates the anchor point for a tick.
 386:      * 
 387:      * @param tick  the tick.
 388:      * @param cursor  the cursor.
 389:      * @param dataArea  the data area.
 390:      * @param edge  the side on which the axis is displayed.
 391:      * 
 392:      * @return The anchor point.
 393:      */
 394:     protected float[] calculateAnchorPoint(ValueTick tick, double cursor, 
 395:                                            Rectangle2D dataArea, 
 396:                                            RectangleEdge edge) {
 397:         if (tick instanceof CycleBoundTick) {
 398:             boolean mapsav = this.boundMappedToLastCycle;
 399:             this.boundMappedToLastCycle 
 400:                 = ((CycleBoundTick) tick).mapToLastCycle;
 401:             float[] ret = super.calculateAnchorPoint(
 402:                 tick, cursor, dataArea, edge
 403:             );
 404:             this.boundMappedToLastCycle = mapsav;
 405:             return ret;
 406:         }
 407:         return super.calculateAnchorPoint(tick, cursor, dataArea, edge);
 408:     }
 409:     
 410:     
 411:     
 412:     /**
 413:      * Builds a list of ticks for the axis.  This method is called when the 
 414:      * axis is at the top or bottom of the chart (so the axis is "horizontal").
 415:      * 
 416:      * @param g2  the graphics device.
 417:      * @param dataArea  the data area.
 418:      * @param edge  the edge.
 419:      * 
 420:      * @return A list of ticks.
 421:      */
 422:     protected List refreshTicksHorizontal(Graphics2D g2, 
 423:                                           Rectangle2D dataArea, 
 424:                                           RectangleEdge edge) {
 425: 
 426:         List result = new java.util.ArrayList();
 427: 
 428:         Font tickLabelFont = getTickLabelFont();
 429:         g2.setFont(tickLabelFont);
 430:         
 431:         if (isAutoTickUnitSelection()) {
 432:             selectAutoTickUnit(g2, dataArea, edge);
 433:         }
 434: 
 435:         double unit = getTickUnit().getSize();
 436:         double cycleBound = getCycleBound();
 437:         double currentTickValue = Math.ceil(cycleBound / unit) * unit;
 438:         double upperValue = getRange().getUpperBound();
 439:         boolean cycled = false;
 440: 
 441:         boolean boundMapping = this.boundMappedToLastCycle; 
 442:         this.boundMappedToLastCycle = false; 
 443:         
 444:         CycleBoundTick lastTick = null; 
 445:         float lastX = 0.0f;
 446: 
 447:         if (upperValue == cycleBound) {
 448:             currentTickValue = calculateLowestVisibleTickValue();
 449:             cycled = true;
 450:             this.boundMappedToLastCycle = true;
 451:         }
 452:         
 453:         while (currentTickValue <= upperValue) {
 454:             
 455:             // Cycle when necessary
 456:             boolean cyclenow = false;
 457:             if ((currentTickValue + unit > upperValue) && !cycled) {
 458:                 cyclenow = true;
 459:             }
 460:             
 461:             double xx = valueToJava2D(currentTickValue, dataArea, edge);
 462:             String tickLabel;
 463:             NumberFormat formatter = getNumberFormatOverride();
 464:             if (formatter != null) {
 465:                 tickLabel = formatter.format(currentTickValue);
 466:             }
 467:             else {
 468:                 tickLabel = getTickUnit().valueToString(currentTickValue);
 469:             }
 470:             float x = (float) xx;
 471:             TextAnchor anchor = null;
 472:             TextAnchor rotationAnchor = null;
 473:             double angle = 0.0;
 474:             if (isVerticalTickLabels()) {
 475:                 if (edge == RectangleEdge.TOP) {
 476:                     angle = Math.PI / 2.0;
 477:                 }
 478:                 else {
 479:                     angle = -Math.PI / 2.0;
 480:                 }
 481:                 anchor = TextAnchor.CENTER_RIGHT;
 482:                 // If tick overlap when cycling, update last tick too
 483:                 if ((lastTick != null) && (lastX == x) 
 484:                         && (currentTickValue != cycleBound)) {
 485:                     anchor = isInverted() 
 486:                         ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
 487:                     result.remove(result.size() - 1);
 488:                     result.add(new CycleBoundTick(
 489:                         this.boundMappedToLastCycle, lastTick.getNumber(), 
 490:                         lastTick.getText(), anchor, anchor, 
 491:                         lastTick.getAngle())
 492:                     );
 493:                     this.internalMarkerWhenTicksOverlap = true;
 494:                     anchor = isInverted() 
 495:                         ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
 496:                 }
 497:                 rotationAnchor = anchor;
 498:             }
 499:             else {
 500:                 if (edge == RectangleEdge.TOP) {
 501:                     anchor = TextAnchor.BOTTOM_CENTER; 
 502:                     if ((lastTick != null) && (lastX == x) 
 503:                             && (currentTickValue != cycleBound)) {
 504:                         anchor = isInverted() 
 505:                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
 506:                         result.remove(result.size() - 1);
 507:                         result.add(new CycleBoundTick(
 508:                             this.boundMappedToLastCycle, lastTick.getNumber(),
 509:                             lastTick.getText(), anchor, anchor, 
 510:                             lastTick.getAngle())
 511:                         );
 512:                         this.internalMarkerWhenTicksOverlap = true;
 513:                         anchor = isInverted() 
 514:                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
 515:                     }
 516:                     rotationAnchor = anchor;
 517:                 }
 518:                 else {
 519:                     anchor = TextAnchor.TOP_CENTER; 
 520:                     if ((lastTick != null) && (lastX == x) 
 521:                             && (currentTickValue != cycleBound)) {
 522:                         anchor = isInverted() 
 523:                             ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT;
 524:                         result.remove(result.size() - 1);
 525:                         result.add(new CycleBoundTick(
 526:                             this.boundMappedToLastCycle, lastTick.getNumber(),
 527:                             lastTick.getText(), anchor, anchor, 
 528:                             lastTick.getAngle())
 529:                         );
 530:                         this.internalMarkerWhenTicksOverlap = true;
 531:                         anchor = isInverted() 
 532:                             ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT;
 533:                     }
 534:                     rotationAnchor = anchor;
 535:                 }
 536:             }
 537: 
 538:             CycleBoundTick tick = new CycleBoundTick(
 539:                 this.boundMappedToLastCycle, 
 540:                 new Double(currentTickValue), tickLabel, anchor, 
 541:                 rotationAnchor, angle
 542:             );
 543:             if (currentTickValue == cycleBound) {
 544:                 this.internalMarkerCycleBoundTick = tick; 
 545:             }
 546:             result.add(tick);
 547:             lastTick = tick;
 548:             lastX = x;
 549:             
 550:             currentTickValue += unit;
 551:             
 552:             if (cyclenow) {
 553:                 currentTickValue = calculateLowestVisibleTickValue();
 554:                 upperValue = cycleBound;
 555:                 cycled = true;
 556:                 this.boundMappedToLastCycle = true; 
 557:             }
 558: 
 559:         }
 560:         this.boundMappedToLastCycle = boundMapping; 
 561:         return result;
 562:         
 563:     }
 564: 
 565:     /**
 566:      * Builds a list of ticks for the axis.  This method is called when the 
 567:      * axis is at the left or right of the chart (so the axis is "vertical").
 568:      * 
 569:      * @param g2  the graphics device.
 570:      * @param dataArea  the data area.
 571:      * @param edge  the edge.
 572:      * 
 573:      * @return A list of ticks.
 574:      */
 575:     protected List refreshVerticalTicks(Graphics2D g2, 
 576:                                         Rectangle2D dataArea, 
 577:                                         RectangleEdge edge) {
 578:         
 579:         List result = new java.util.ArrayList();
 580:         result.clear();
 581: 
 582:         Font tickLabelFont = getTickLabelFont();
 583:         g2.setFont(tickLabelFont);
 584:         if (isAutoTickUnitSelection()) {
 585:             selectAutoTickUnit(g2, dataArea, edge);
 586:         }
 587: 
 588:         double unit = getTickUnit().getSize();
 589:         double cycleBound = getCycleBound();
 590:         double currentTickValue = Math.ceil(cycleBound / unit) * unit;
 591:         double upperValue = getRange().getUpperBound();
 592:         boolean cycled = false;
 593: 
 594:         boolean boundMapping = this.boundMappedToLastCycle; 
 595:         this.boundMappedToLastCycle = true; 
 596: 
 597:         NumberTick lastTick = null;
 598:         float lastY = 0.0f;
 599: 
 600:         if (upperValue == cycleBound) {
 601:             currentTickValue = calculateLowestVisibleTickValue();
 602:             cycled = true;
 603:             this.boundMappedToLastCycle = true;
 604:         }
 605:         
 606:         while (currentTickValue <= upperValue) {
 607:             
 608:             // Cycle when necessary
 609:             boolean cyclenow = false;
 610:             if ((currentTickValue + unit > upperValue) && !cycled) {
 611:                 cyclenow = true;
 612:             }
 613: 
 614:             double yy = valueToJava2D(currentTickValue, dataArea, edge);
 615:             String tickLabel;
 616:             NumberFormat formatter = getNumberFormatOverride();
 617:             if (formatter != null) {
 618:                 tickLabel = formatter.format(currentTickValue);
 619:             }
 620:             else {
 621:                 tickLabel = getTickUnit().valueToString(currentTickValue);
 622:             }
 623: 
 624:             float y = (float) yy;
 625:             TextAnchor anchor = null;
 626:             TextAnchor rotationAnchor = null;
 627:             double angle = 0.0;
 628:             if (isVerticalTickLabels()) {
 629: 
 630:                 if (edge == RectangleEdge.LEFT) {
 631:                     anchor = TextAnchor.BOTTOM_CENTER; 
 632:                     if ((lastTick != null) && (lastY == y) 
 633:                             && (currentTickValue != cycleBound)) {
 634:                         anchor = isInverted() 
 635:                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
 636:                         result.remove(result.size() - 1);
 637:                         result.add(new CycleBoundTick(
 638:                             this.boundMappedToLastCycle, lastTick.getNumber(),
 639:                             lastTick.getText(), anchor, anchor, 
 640:                             lastTick.getAngle())
 641:                         );
 642:                         this.internalMarkerWhenTicksOverlap = true;
 643:                         anchor = isInverted() 
 644:                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
 645:                     }
 646:                     rotationAnchor = anchor;
 647:                     angle = -Math.PI / 2.0;
 648:                 }
 649:                 else {
 650:                     anchor = TextAnchor.BOTTOM_CENTER; 
 651:                     if ((lastTick != null) && (lastY == y) 
 652:                             && (currentTickValue != cycleBound)) {
 653:                         anchor = isInverted() 
 654:                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
 655:                         result.remove(result.size() - 1);
 656:                         result.add(new CycleBoundTick(
 657:                             this.boundMappedToLastCycle, lastTick.getNumber(),
 658:                             lastTick.getText(), anchor, anchor, 
 659:                             lastTick.getAngle())
 660:                         );
 661:                         this.internalMarkerWhenTicksOverlap = true;
 662:                         anchor = isInverted() 
 663:                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
 664:                     }
 665:                     rotationAnchor = anchor;
 666:                     angle = Math.PI / 2.0;
 667:                 }
 668:             }
 669:             else {
 670:                 if (edge == RectangleEdge.LEFT) {
 671:                     anchor = TextAnchor.CENTER_RIGHT; 
 672:                     if ((lastTick != null) && (lastY == y) 
 673:                             && (currentTickValue != cycleBound)) {
 674:                         anchor = isInverted() 
 675:                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
 676:                         result.remove(result.size() - 1);
 677:                         result.add(new CycleBoundTick(
 678:                             this.boundMappedToLastCycle, lastTick.getNumber(),
 679:                             lastTick.getText(), anchor, anchor, 
 680:                             lastTick.getAngle())
 681:                         );
 682:                         this.internalMarkerWhenTicksOverlap = true;
 683:                         anchor = isInverted() 
 684:                             ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
 685:                     }
 686:                     rotationAnchor = anchor;
 687:                 }
 688:                 else {
 689:                     anchor = TextAnchor.CENTER_LEFT; 
 690:                     if ((lastTick != null) && (lastY == y) 
 691:                             && (currentTickValue != cycleBound)) {
 692:                         anchor = isInverted() 
 693:                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT;
 694:                         result.remove(result.size() - 1);
 695:                         result.add(new CycleBoundTick(
 696:                             this.boundMappedToLastCycle, lastTick.getNumber(),
 697:                             lastTick.getText(), anchor, anchor, 
 698:                             lastTick.getAngle())
 699:                         );
 700:                         this.internalMarkerWhenTicksOverlap = true;
 701:                         anchor = isInverted() 
 702:                             ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT;
 703:                     }
 704:                     rotationAnchor = anchor;
 705:                 }
 706:             }
 707: 
 708:             CycleBoundTick tick = new CycleBoundTick(
 709:                 this.boundMappedToLastCycle, new Double(currentTickValue), 
 710:                 tickLabel, anchor, rotationAnchor, angle
 711:             );
 712:             if (currentTickValue == cycleBound) {
 713:                 this.internalMarkerCycleBoundTick = tick; 
 714:             }
 715:             result.add(tick);
 716:             lastTick = tick;
 717:             lastY = y;
 718:             
 719:             if (currentTickValue == cycleBound) {
 720:                 this.internalMarkerCycleBoundTick = tick;
 721:             }
 722: 
 723:             currentTickValue += unit;
 724:             
 725:             if (cyclenow) {
 726:                 currentTickValue = calculateLowestVisibleTickValue();
 727:                 upperValue = cycleBound;
 728:                 cycled = true;
 729:                 this.boundMappedToLastCycle = false; 
 730:             }
 731: 
 732:         }
 733:         this.boundMappedToLastCycle = boundMapping; 
 734:         return result;
 735:     }
 736:     
 737:     /**
 738:      * Converts a coordinate from Java 2D space to data space.
 739:      * 
 740:      * @param java2DValue  the coordinate in Java2D space.
 741:      * @param dataArea  the data area.
 742:      * @param edge  the edge.
 743:      * 
 744:      * @return The data value.
 745:      */
 746:     public double java2DToValue(double java2DValue, Rectangle2D dataArea, 
 747:                                 RectangleEdge edge) {
 748:         Range range = getRange();
 749:         
 750:         double vmax = range.getUpperBound();
 751:         double vp = getCycleBound();
 752: 
 753:         double jmin = 0.0;
 754:         double jmax = 0.0;
 755:         if (RectangleEdge.isTopOrBottom(edge)) {
 756:             jmin = dataArea.getMinX();
 757:             jmax = dataArea.getMaxX();
 758:         }
 759:         else if (RectangleEdge.isLeftOrRight(edge)) {
 760:             jmin = dataArea.getMaxY();
 761:             jmax = dataArea.getMinY();
 762:         }
 763:         
 764:         if (isInverted()) {
 765:             double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period;
 766:             if (java2DValue >= jbreak) { 
 767:                 return vp + (jmax - java2DValue) * this.period / (jmax - jmin);
 768:             } 
 769:             else {
 770:                 return vp - (java2DValue - jmin) * this.period / (jmax - jmin);
 771:             }
 772:         }
 773:         else {
 774:             double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin;
 775:             if (java2DValue <= jbreak) { 
 776:                 return vp + (java2DValue - jmin) * this.period / (jmax - jmin);
 777:             } 
 778:             else {
 779:                 return vp - (jmax - java2DValue) * this.period / (jmax - jmin);
 780:             }
 781:         }
 782:     }
 783:     
 784:     /**
 785:      * Translates a value from data space to Java 2D space.
 786:      * 
 787:      * @param value  the data value.
 788:      * @param dataArea  the data area.
 789:      * @param edge  the edge.
 790:      * 
 791:      * @return The Java 2D value.
 792:      */
 793:     public double valueToJava2D(double value, Rectangle2D dataArea, 
 794:                                 RectangleEdge edge) {
 795:         Range range = getRange();
 796:         
 797:         double vmin = range.getLowerBound();
 798:         double vmax = range.getUpperBound();
 799:         double vp = getCycleBound();
 800: 
 801:         if ((value < vmin) || (value > vmax)) {
 802:             return Double.NaN;
 803:         }
 804:         
 805:         
 806:         double jmin = 0.0;
 807:         double jmax = 0.0;
 808:         if (RectangleEdge.isTopOrBottom(edge)) {
 809:             jmin = dataArea.getMinX();
 810:             jmax = dataArea.getMaxX();
 811:         }
 812:         else if (RectangleEdge.isLeftOrRight(edge)) {
 813:             jmax = dataArea.getMinY();
 814:             jmin = dataArea.getMaxY();
 815:         }
 816: 
 817:         if (isInverted()) {
 818:             if (value == vp) {
 819:                 return this.boundMappedToLastCycle ? jmin : jmax; 
 820:             }
 821:             else if (value > vp) {
 822:                 return jmax - (value - vp) * (jmax - jmin) / this.period;
 823:             } 
 824:             else {
 825:                 return jmin + (vp - value) * (jmax - jmin) / this.period;
 826:             }
 827:         }
 828:         else {
 829:             if (value == vp) {
 830:                 return this.boundMappedToLastCycle ? jmax : jmin; 
 831:             }
 832:             else if (value >= vp) {
 833:                 return jmin + (value - vp) * (jmax - jmin) / this.period;
 834:             } 
 835:             else {
 836:                 return jmax - (vp - value) * (jmax - jmin) / this.period;
 837:             }
 838:         }
 839:     }
 840:     
 841:     /**
 842:      * Centers the range about the given value.
 843:      * 
 844:      * @param value  the data value.
 845:      */
 846:     public void centerRange(double value) {
 847:         setRange(value - this.period / 2.0, value + this.period / 2.0);
 848:     }
 849: 
 850:     /** 
 851:      * This function is nearly useless since the auto range is fixed for this 
 852:      * class to the period.  The period is extended if necessary to fit the 
 853:      * minimum size.
 854:      * 
 855:      * @param size  the size.
 856:      * @param notify  notify?
 857:      * 
 858:      * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double, 
 859:      *      boolean)
 860:      */
 861:     public void setAutoRangeMinimumSize(double size, boolean notify) {
 862:         if (size > this.period) {
 863:             this.period = size;
 864:         }
 865:         super.setAutoRangeMinimumSize(size, notify);
 866:     }
 867: 
 868:     /** 
 869:      * The auto range is fixed for this class to the period by default. 
 870:      * This function will thus set a new period.
 871:      * 
 872:      * @param length  the length.
 873:      * 
 874:      * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double)
 875:      */
 876:     public void setFixedAutoRange(double length) {
 877:         this.period = length;
 878:         super.setFixedAutoRange(length);
 879:     }
 880: 
 881:     /** 
 882:      * Sets a new axis range. The period is extended to fit the range size, if 
 883:      * necessary.
 884:      * 
 885:      * @param range  the range.
 886:      * @param turnOffAutoRange  switch off the auto range.
 887:      * @param notify notify?
 888:      * 
 889:      * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean) 
 890:      */
 891:     public void setRange(Range range, boolean turnOffAutoRange, 
 892:                          boolean notify) {
 893:         double size = range.getUpperBound() - range.getLowerBound();
 894:         if (size > this.period) {
 895:             this.period = size;
 896:         }
 897:         super.setRange(range, turnOffAutoRange, notify);
 898:     }
 899:     
 900:     /**
 901:      * The cycle bound is defined as the higest value x such that 
 902:      * "offset + period * i = x", with i and integer and x &lt; 
 903:      * range.getUpperBound() This is the value which is at both ends of the 
 904:      * axis :  x...up|low...x
 905:      * The values from x to up are the valued in the current cycle.
 906:      * The values from low to x are the valued in the previous cycle.
 907:      * 
 908:      * @return The cycle bound.
 909:      */
 910:     public double getCycleBound() {
 911:         return Math.floor(
 912:             (getRange().getUpperBound() - this.offset) / this.period
 913:         ) * this.period + this.offset;
 914:     }
 915:     
 916:     /**
 917:      * The cycle bound is a multiple of the period, plus optionally a start 
 918:      * offset.
 919:      * <P>
 920:      * <pre>cb = n * period + offset</pre><br>
 921:      * 
 922:      * @return The current offset.
 923:      * 
 924:      * @see #getCycleBound()
 925:      */
 926:     public double getOffset() {
 927:         return this.offset;
 928:     }
 929:     
 930:     /**
 931:      * The cycle bound is a multiple of the period, plus optionally a start 
 932:      * offset.
 933:      * <P>
 934:      * <pre>cb = n * period + offset</pre><br>
 935:      * 
 936:      * @param offset The offset to set.
 937:      *
 938:      * @see #getCycleBound() 
 939:      */
 940:     public void setOffset(double offset) {
 941:         this.offset = offset;
 942:     }
 943:     
 944:     /**
 945:      * The cycle bound is a multiple of the period, plus optionally a start 
 946:      * offset.
 947:      * <P>
 948:      * <pre>cb = n * period + offset</pre><br>
 949:      * 
 950:      * @return The current period.
 951:      * 
 952:      * @see #getCycleBound()
 953:      */
 954:     public double getPeriod() {
 955:         return this.period;
 956:     }
 957:     
 958:     /**
 959:      * The cycle bound is a multiple of the period, plus optionally a start 
 960:      * offset.
 961:      * <P>
 962:      * <pre>cb = n * period + offset</pre><br>
 963:      * 
 964:      * @param period The period to set.
 965:      * 
 966:      * @see #getCycleBound()
 967:      */
 968:     public void setPeriod(double period) {
 969:         this.period = period;
 970:     }
 971: 
 972:     /**
 973:      * Draws the tick marks and labels.
 974:      * 
 975:      * @param g2  the graphics device.
 976:      * @param cursor  the cursor.
 977:      * @param plotArea  the plot area.
 978:      * @param dataArea  the area inside the axes.
 979:      * @param edge  the side on which the axis is displayed.
 980:      * 
 981:      * @return The axis state.
 982:      */
 983:     protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor, 
 984:                                                Rectangle2D plotArea, 
 985:                                                Rectangle2D dataArea, 
 986:                                                RectangleEdge edge) {
 987:         this.internalMarkerWhenTicksOverlap = false;
 988:         AxisState ret = super.drawTickMarksAndLabels(
 989:             g2, cursor, plotArea, dataArea, edge
 990:         );
 991:         
 992:         // continue and separate the labels only if necessary
 993:         if (!this.internalMarkerWhenTicksOverlap) {
 994:             return ret;
 995:         }
 996:         
 997:         double ol = getTickMarkOutsideLength();
 998:         FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
 999:         
1000:         if (isVerticalTickLabels()) {
1001:             ol = fm.getMaxAdvance(); 
1002:         }
1003:         else {
1004:             ol = fm.getHeight();
1005:         }
1006:         
1007:         double il = 0;
1008:         if (isTickMarksVisible()) {
1009:             float xx = (float) valueToJava2D(
1010:                 getRange().getUpperBound(), dataArea, edge
1011:             );
1012:             Line2D mark = null;
1013:             g2.setStroke(getTickMarkStroke());
1014:             g2.setPaint(getTickMarkPaint());
1015:             if (edge == RectangleEdge.LEFT) {
1016:                 mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
1017:             }
1018:             else if (edge == RectangleEdge.RIGHT) {
1019:                 mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
1020:             }
1021:             else if (edge == RectangleEdge.TOP) {
1022:                 mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
1023:             }
1024:             else if (edge == RectangleEdge.BOTTOM) {
1025:                 mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
1026:             }
1027:             g2.draw(mark);
1028:         }
1029:         return ret;
1030:     }
1031:     
1032:     /**
1033:      * Draws the axis.
1034:      * 
1035:      * @param g2  the graphics device (<code>null</code> not permitted).
1036:      * @param cursor  the cursor position.
1037:      * @param plotArea  the plot area (<code>null</code> not permitted).
1038:      * @param dataArea  the data area (<code>null</code> not permitted).
1039:      * @param edge  the edge (<code>null</code> not permitted).
1040:      * @param plotState  collects information about the plot 
1041:      *                   (<code>null</code> permitted).
1042:      * 
1043:      * @return The axis state (never <code>null</code>).
1044:      */
1045:     public AxisState draw(Graphics2D g2, 
1046:                           double cursor,
1047:                           Rectangle2D plotArea, 
1048:                           Rectangle2D dataArea, 
1049:                           RectangleEdge edge,
1050:                           PlotRenderingInfo plotState) {
1051:         
1052:         AxisState ret = super.draw(
1053:             g2, cursor, plotArea, dataArea, edge, plotState
1054:         );
1055:         if (isAdvanceLineVisible()) {
1056:             double xx = valueToJava2D(
1057:                 getRange().getUpperBound(), dataArea, edge
1058:             );
1059:             Line2D mark = null;
1060:             g2.setStroke(getAdvanceLineStroke());
1061:             g2.setPaint(getAdvanceLinePaint());
1062:             if (edge == RectangleEdge.LEFT) {
1063:                 mark = new Line2D.Double(
1064:                     cursor, xx, cursor + dataArea.getWidth(), xx
1065:                 );
1066:             }
1067:             else if (edge == RectangleEdge.RIGHT) {
1068:                 mark = new Line2D.Double(
1069:                     cursor - dataArea.getWidth(), xx, cursor, xx
1070:                 );
1071:             }
1072:             else if (edge == RectangleEdge.TOP) {
1073:                 mark = new Line2D.Double(
1074:                     xx, cursor + dataArea.getHeight(), xx, cursor
1075:                 );
1076:             }
1077:             else if (edge == RectangleEdge.BOTTOM) {
1078:                 mark = new Line2D.Double(
1079:                     xx, cursor, xx, cursor - dataArea.getHeight()
1080:                 );
1081:             }
1082:             g2.draw(mark);
1083:         }
1084:         return ret;
1085:     }
1086: 
1087:     /**
1088:      * Reserve some space on each axis side because we draw a centered label at
1089:      * each extremity. 
1090:      * 
1091:      * @param g2  the graphics device.
1092:      * @param plot  the plot.
1093:      * @param plotArea  the plot area.
1094:      * @param edge  the edge.
1095:      * @param space  the space already reserved.
1096:      * 
1097:      * @return The reserved space.
1098:      */
1099:     public AxisSpace reserveSpace(Graphics2D g2, 
1100:                                   Plot plot, 
1101:                                   Rectangle2D plotArea, 
1102:                                   RectangleEdge edge, 
1103:                                   AxisSpace space) {
1104:         
1105:         this.internalMarkerCycleBoundTick = null;
1106:         AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
1107:         if (this.internalMarkerCycleBoundTick == null) {
1108:             return ret;
1109:         }
1110: 
1111:         FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
1112:         Rectangle2D r = TextUtilities.getTextBounds(
1113:             this.internalMarkerCycleBoundTick.getText(), g2, fm
1114:         );
1115: 
1116:         if (RectangleEdge.isTopOrBottom(edge)) {
1117:             if (isVerticalTickLabels()) {
1118:                 space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
1119:             }
1120:             else {
1121:                 space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
1122:             }
1123:         }
1124:         else if (RectangleEdge.isLeftOrRight(edge)) {
1125:             if (isVerticalTickLabels()) {
1126:                 space.add(r.getWidth() / 2, RectangleEdge.TOP);
1127:             }
1128:             else {
1129:                 space.add(r.getHeight() / 2, RectangleEdge.TOP);
1130:             }
1131:         }
1132:         
1133:         return ret;
1134:         
1135:     }
1136: 
1137:     /**
1138:      * Provides serialization support.
1139:      *
1140:      * @param stream  the output stream.
1141:      *
1142:      * @throws IOException  if there is an I/O error.
1143:      */
1144:     private void writeObject(ObjectOutputStream stream) throws IOException {
1145:     
1146:         stream.defaultWriteObject();
1147:         SerialUtilities.writePaint(this.advanceLinePaint, stream);
1148:         SerialUtilities.writeStroke(this.advanceLineStroke, stream);
1149:     
1150:     }
1151:     
1152:     /**
1153:      * Provides serialization support.
1154:      *
1155:      * @param stream  the input stream.
1156:      *
1157:      * @throws IOException  if there is an I/O error.
1158:      * @throws ClassNotFoundException  if there is a classpath problem.
1159:      */
1160:     private void readObject(ObjectInputStream stream) 
1161:         throws IOException, ClassNotFoundException {
1162:     
1163:         stream.defaultReadObject();
1164:         this.advanceLinePaint = SerialUtilities.readPaint(stream);
1165:         this.advanceLineStroke = SerialUtilities.readStroke(stream);
1166:     
1167:     }
1168:      
1169:     
1170:     /**
1171:      * Tests the axis for equality with another object.
1172:      * 
1173:      * @param obj  the object to test against.
1174:      * 
1175:      * @return A boolean.
1176:      */
1177:     public boolean equals(Object obj) {
1178:         if (obj == this) {
1179:             return true;
1180:         }
1181:         if (!(obj instanceof CyclicNumberAxis)) {
1182:             return false;
1183:         }
1184:         if (!super.equals(obj)) {
1185:             return false;
1186:         }
1187:         CyclicNumberAxis that = (CyclicNumberAxis) obj;      
1188:         if (this.period != that.period) {
1189:             return false;
1190:         }
1191:         if (this.offset != that.offset) {
1192:             return false;
1193:         }
1194:         if (!PaintUtilities.equal(this.advanceLinePaint, 
1195:                 that.advanceLinePaint)) {
1196:             return false;
1197:         }
1198:         if (!ObjectUtilities.equal(this.advanceLineStroke, 
1199:                 that.advanceLineStroke)) {
1200:             return false;
1201:         }
1202:         if (this.advanceLineVisible != that.advanceLineVisible) {
1203:             return false;
1204:         }
1205:         if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) {
1206:             return false;
1207:         }
1208:         return true;
1209:     }
1210: }