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