| 1 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 75 124 public class CyclicNumberAxis extends NumberAxis { 125 126 127 public static Stroke DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke (1.0f); 128 129 130 public static final Paint DEFAULT_ADVANCE_LINE_PAINT = Color.gray; 131 132 133 protected double offset; 134 135 136 protected double period; 137 138 139 protected boolean boundMappedToLastCycle; 140 141 142 protected boolean advanceLineVisible; 143 144 145 protected transient Stroke advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE; 146 147 148 protected transient Paint advanceLinePaint; 149 150 private transient boolean internalMarkerWhenTicksOverlap; 151 private transient Tick internalMarkerCycleBoundTick; 152 153 158 public CyclicNumberAxis(double period) { 159 this(period, 0.0); 160 } 161 162 168 public CyclicNumberAxis(double period, double offset) { 169 this(period, offset, null); 170 } 171 172 178 public CyclicNumberAxis(double period, String label) { 179 this(0, period, label); 180 } 181 182 189 public CyclicNumberAxis(double period, double offset, String label) { 190 super(label); 191 this.period = period; 192 this.offset = offset; 193 setFixedAutoRange(period); 194 this.advanceLineVisible = true; 195 this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT; 196 } 197 198 204 public boolean isAdvanceLineVisible() { 205 return this.advanceLineVisible; 206 } 207 208 214 public void setAdvanceLineVisible(boolean visible) { 215 this.advanceLineVisible = visible; 216 } 217 218 224 public Paint getAdvanceLinePaint() { 225 return this.advanceLinePaint; 226 } 227 228 234 public void setAdvanceLinePaint(Paint paint) { 235 if (paint == null) { 236 throw new IllegalArgumentException ("Null 'paint' argument."); 237 } 238 this.advanceLinePaint = paint; 239 } 240 241 247 public Stroke getAdvanceLineStroke() { 248 return this.advanceLineStroke; 249 } 250 256 public void setAdvanceLineStroke(Stroke stroke) { 257 if (stroke == null) { 258 throw new IllegalArgumentException ("Null 'stroke' argument."); 259 } 260 this.advanceLineStroke = stroke; 261 } 262 263 277 public boolean isBoundMappedToLastCycle() { 278 return this.boundMappedToLastCycle; 279 } 280 281 294 public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) { 295 this.boundMappedToLastCycle = boundMappedToLastCycle; 296 } 297 298 306 protected void selectHorizontalAutoTickUnit(Graphics2D g2, 307 Rectangle2D drawArea, 308 Rectangle2D dataArea, 309 RectangleEdge edge) { 310 311 double tickLabelWidth 312 = estimateMaximumTickLabelWidth(g2, getTickUnit()); 313 314 double n = getRange().getLength() 316 * tickLabelWidth / dataArea.getWidth(); 317 318 setTickUnit( 319 (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 320 false, false 321 ); 322 323 } 324 325 333 protected void selectVerticalAutoTickUnit(Graphics2D g2, 334 Rectangle2D drawArea, 335 Rectangle2D dataArea, 336 RectangleEdge edge) { 337 338 double tickLabelWidth 339 = estimateMaximumTickLabelWidth(g2, getTickUnit()); 340 341 double n = getRange().getLength() 343 * tickLabelWidth / dataArea.getHeight(); 344 345 setTickUnit( 346 (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n), 347 false, false 348 ); 349 350 } 351 352 358 protected static class CycleBoundTick extends NumberTick { 359 360 361 public boolean mapToLastCycle; 362 363 373 public CycleBoundTick(boolean mapToLastCycle, Number number, 374 String label, TextAnchor textAnchor, 375 TextAnchor rotationAnchor, double angle) { 376 super(number, label, textAnchor, rotationAnchor, angle); 377 this.mapToLastCycle = mapToLastCycle; 378 } 379 } 380 381 391 protected float[] calculateAnchorPoint(ValueTick tick, double cursor, 392 Rectangle2D dataArea, 393 RectangleEdge edge) { 394 if (tick instanceof CycleBoundTick) { 395 boolean mapsav = this.boundMappedToLastCycle; 396 this.boundMappedToLastCycle 397 = ((CycleBoundTick) tick).mapToLastCycle; 398 float[] ret = super.calculateAnchorPoint( 399 tick, cursor, dataArea, edge 400 ); 401 this.boundMappedToLastCycle = mapsav; 402 return ret; 403 } 404 return super.calculateAnchorPoint(tick, cursor, dataArea, edge); 405 } 406 407 408 409 419 protected List refreshTicksHorizontal(Graphics2D g2, 420 Rectangle2D dataArea, 421 RectangleEdge edge) { 422 423 List result = new java.util.ArrayList (); 424 425 Font tickLabelFont = getTickLabelFont(); 426 g2.setFont(tickLabelFont); 427 428 if (isAutoTickUnitSelection()) { 429 selectAutoTickUnit(g2, dataArea, edge); 430 } 431 432 double unit = getTickUnit().getSize(); 433 double cycleBound = getCycleBound(); 434 double currentTickValue = Math.ceil(cycleBound / unit) * unit; 435 double upperValue = getRange().getUpperBound(); 436 boolean cycled = false; 437 438 boolean boundMapping = this.boundMappedToLastCycle; 439 this.boundMappedToLastCycle = false; 440 441 CycleBoundTick lastTick = null; 442 float lastX = 0.0f; 443 444 if (upperValue == cycleBound) { 445 currentTickValue = calculateLowestVisibleTickValue(); 446 cycled = true; 447 this.boundMappedToLastCycle = true; 448 } 449 450 while (currentTickValue <= upperValue) { 451 452 boolean cyclenow = false; 454 if ((currentTickValue + unit > upperValue) && !cycled) { 455 cyclenow = true; 456 } 457 458 double xx = valueToJava2D(currentTickValue, dataArea, edge); 459 String tickLabel; 460 NumberFormat formatter = getNumberFormatOverride(); 461 if (formatter != null) { 462 tickLabel = formatter.format(currentTickValue); 463 } 464 else { 465 tickLabel = getTickUnit().valueToString(currentTickValue); 466 } 467 float x = (float) xx; 468 TextAnchor anchor = null; 469 TextAnchor rotationAnchor = null; 470 double angle = 0.0; 471 if (isVerticalTickLabels()) { 472 if (edge == RectangleEdge.TOP) { 473 angle = Math.PI / 2.0; 474 } 475 else { 476 angle = -Math.PI / 2.0; 477 } 478 anchor = TextAnchor.CENTER_RIGHT; 479 if ((lastTick != null) && (lastX == x) 481 && (currentTickValue != cycleBound)) { 482 anchor = isInverted() 483 ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT; 484 result.remove(result.size() - 1); 485 result.add(new CycleBoundTick( 486 this.boundMappedToLastCycle, lastTick.getNumber(), 487 lastTick.getText(), anchor, anchor, 488 lastTick.getAngle()) 489 ); 490 this.internalMarkerWhenTicksOverlap = true; 491 anchor = isInverted() 492 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT; 493 } 494 rotationAnchor = anchor; 495 } 496 else { 497 if (edge == RectangleEdge.TOP) { 498 anchor = TextAnchor.BOTTOM_CENTER; 499 if ((lastTick != null) && (lastX == x) 500 && (currentTickValue != cycleBound)) { 501 anchor = isInverted() 502 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 503 result.remove(result.size() - 1); 504 result.add(new CycleBoundTick( 505 this.boundMappedToLastCycle, lastTick.getNumber(), 506 lastTick.getText(), anchor, anchor, 507 lastTick.getAngle()) 508 ); 509 this.internalMarkerWhenTicksOverlap = true; 510 anchor = isInverted() 511 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 512 } 513 rotationAnchor = anchor; 514 } 515 else { 516 anchor = TextAnchor.TOP_CENTER; 517 if ((lastTick != null) && (lastX == x) 518 && (currentTickValue != cycleBound)) { 519 anchor = isInverted() 520 ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT; 521 result.remove(result.size() - 1); 522 result.add(new CycleBoundTick( 523 this.boundMappedToLastCycle, lastTick.getNumber(), 524 lastTick.getText(), anchor, anchor, 525 lastTick.getAngle()) 526 ); 527 this.internalMarkerWhenTicksOverlap = true; 528 anchor = isInverted() 529 ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT; 530 } 531 rotationAnchor = anchor; 532 } 533 } 534 535 CycleBoundTick tick = new CycleBoundTick( 536 this.boundMappedToLastCycle, 537 new Double (currentTickValue), tickLabel, anchor, 538 rotationAnchor, angle 539 ); 540 if (currentTickValue == cycleBound) { 541 this.internalMarkerCycleBoundTick = tick; 542 } 543 result.add(tick); 544 lastTick = tick; 545 lastX = x; 546 547 currentTickValue += unit; 548 549 if (cyclenow) { 550 currentTickValue = calculateLowestVisibleTickValue(); 551 upperValue = cycleBound; 552 cycled = true; 553 this.boundMappedToLastCycle = true; 554 } 555 556 } 557 this.boundMappedToLastCycle = boundMapping; 558 return result; 559 560 } 561 562 572 protected List refreshVerticalTicks(Graphics2D g2, 573 Rectangle2D dataArea, 574 RectangleEdge edge) { 575 576 List result = new java.util.ArrayList (); 577 result.clear(); 578 579 Font tickLabelFont = getTickLabelFont(); 580 g2.setFont(tickLabelFont); 581 if (isAutoTickUnitSelection()) { 582 selectAutoTickUnit(g2, dataArea, edge); 583 } 584 585 double unit = getTickUnit().getSize(); 586 double cycleBound = getCycleBound(); 587 double currentTickValue = Math.ceil(cycleBound / unit) * unit; 588 double upperValue = getRange().getUpperBound(); 589 boolean cycled = false; 590 591 boolean boundMapping = this.boundMappedToLastCycle; 592 this.boundMappedToLastCycle = true; 593 594 NumberTick lastTick = null; 595 float lastY = 0.0f; 596 597 if (upperValue == cycleBound) { 598 currentTickValue = calculateLowestVisibleTickValue(); 599 cycled = true; 600 this.boundMappedToLastCycle = true; 601 } 602 603 while (currentTickValue <= upperValue) { 604 605 boolean cyclenow = false; 607 if ((currentTickValue + unit > upperValue) && !cycled) { 608 cyclenow = true; 609 } 610 611 double yy = valueToJava2D(currentTickValue, dataArea, edge); 612 String tickLabel; 613 NumberFormat formatter = getNumberFormatOverride(); 614 if (formatter != null) { 615 tickLabel = formatter.format(currentTickValue); 616 } 617 else { 618 tickLabel = getTickUnit().valueToString(currentTickValue); 619 } 620 621 float y = (float) yy; 622 TextAnchor anchor = null; 623 TextAnchor rotationAnchor = null; 624 double angle = 0.0; 625 if (isVerticalTickLabels()) { 626 627 if (edge == RectangleEdge.LEFT) { 628 anchor = TextAnchor.BOTTOM_CENTER; 629 if ((lastTick != null) && (lastY == y) 630 && (currentTickValue != cycleBound)) { 631 anchor = isInverted() 632 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 633 result.remove(result.size() - 1); 634 result.add(new CycleBoundTick( 635 this.boundMappedToLastCycle, lastTick.getNumber(), 636 lastTick.getText(), anchor, anchor, 637 lastTick.getAngle()) 638 ); 639 this.internalMarkerWhenTicksOverlap = true; 640 anchor = isInverted() 641 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 642 } 643 rotationAnchor = anchor; 644 angle = -Math.PI / 2.0; 645 } 646 else { 647 anchor = TextAnchor.BOTTOM_CENTER; 648 if ((lastTick != null) && (lastY == y) 649 && (currentTickValue != cycleBound)) { 650 anchor = isInverted() 651 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT; 652 result.remove(result.size() - 1); 653 result.add(new CycleBoundTick( 654 this.boundMappedToLastCycle, lastTick.getNumber(), 655 lastTick.getText(), anchor, anchor, 656 lastTick.getAngle()) 657 ); 658 this.internalMarkerWhenTicksOverlap = true; 659 anchor = isInverted() 660 ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT; 661 } 662 rotationAnchor = anchor; 663 angle = Math.PI / 2.0; 664 } 665 } 666 else { 667 if (edge == RectangleEdge.LEFT) { 668 anchor = TextAnchor.CENTER_RIGHT; 669 if ((lastTick != null) && (lastY == y) 670 && (currentTickValue != cycleBound)) { 671 anchor = isInverted() 672 ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT; 673 result.remove(result.size() - 1); 674 result.add(new CycleBoundTick( 675 this.boundMappedToLastCycle, lastTick.getNumber(), 676 lastTick.getText(), anchor, anchor, 677 lastTick.getAngle()) 678 ); 679 this.internalMarkerWhenTicksOverlap = true; 680 anchor = isInverted() 681 ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT; 682 } 683 rotationAnchor = anchor; 684 } 685 else { 686 anchor = TextAnchor.CENTER_LEFT; 687 if ((lastTick != null) && (lastY == y) 688 && (currentTickValue != cycleBound)) { 689 anchor = isInverted() 690 ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT; 691 result.remove(result.size() - 1); 692 result.add(new CycleBoundTick( 693 this.boundMappedToLastCycle, lastTick.getNumber(), 694 lastTick.getText(), anchor, anchor, 695 lastTick.getAngle()) 696 ); 697 this.internalMarkerWhenTicksOverlap = true; 698 anchor = isInverted() 699 ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT; 700 } 701 rotationAnchor = anchor; 702 } 703 } 704 705 CycleBoundTick tick = new CycleBoundTick( 706 this.boundMappedToLastCycle, new Double (currentTickValue), 707 tickLabel, anchor, rotationAnchor, angle 708 ); 709 if (currentTickValue == cycleBound) { 710 this.internalMarkerCycleBoundTick = tick; 711 } 712 result.add(tick); 713 lastTick = tick; 714 lastY = y; 715 716 if (currentTickValue == cycleBound) { 717 this.internalMarkerCycleBoundTick = tick; 718 } 719 720 currentTickValue += unit; 721 722 if (cyclenow) { 723 currentTickValue = calculateLowestVisibleTickValue(); 724 upperValue = cycleBound; 725 cycled = true; 726 this.boundMappedToLastCycle = false; 727 } 728 729 } 730 this.boundMappedToLastCycle = boundMapping; 731 return result; 732 } 733 734 743 public double java2DToValue(double java2DValue, Rectangle2D dataArea, 744 RectangleEdge edge) { 745 Range range = getRange(); 746 747 double vmax = range.getUpperBound(); 748 double vp = getCycleBound(); 749 750 double jmin = 0.0; 751 double jmax = 0.0; 752 if (RectangleEdge.isTopOrBottom(edge)) { 753 jmin = dataArea.getMinX(); 754 jmax = dataArea.getMaxX(); 755 } 756 else if (RectangleEdge.isLeftOrRight(edge)) { 757 jmin = dataArea.getMaxY(); 758 jmax = dataArea.getMinY(); 759 } 760 761 if (isInverted()) { 762 double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period; 763 if (java2DValue >= jbreak) { 764 return vp + (jmax - java2DValue) * this.period / (jmax - jmin); 765 } 766 else { 767 return vp - (java2DValue - jmin) * this.period / (jmax - jmin); 768 } 769 } 770 else { 771 double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin; 772 if (java2DValue <= jbreak) { 773 return vp + (java2DValue - jmin) * this.period / (jmax - jmin); 774 } 775 else { 776 return vp - (jmax - java2DValue) * this.period / (jmax - jmin); 777 } 778 } 779 } 780 781 790 public double valueToJava2D(double value, Rectangle2D dataArea, 791 RectangleEdge edge) { 792 Range range = getRange(); 793 794 double vmin = range.getLowerBound(); 795 double vmax = range.getUpperBound(); 796 double vp = getCycleBound(); 797 798 if ((value < vmin) || (value > vmax)) { 799 return Double.NaN; 800 } 801 802 803 double jmin = 0.0; 804 double jmax = 0.0; 805 if (RectangleEdge.isTopOrBottom(edge)) { 806 jmin = dataArea.getMinX(); 807 jmax = dataArea.getMaxX(); 808 } 809 else if (RectangleEdge.isLeftOrRight(edge)) { 810 jmax = dataArea.getMinY(); 811 jmin = dataArea.getMaxY(); 812 } 813 814 if (isInverted()) { 815 if (value == vp) { 816 return this.boundMappedToLastCycle ? jmin : jmax; 817 } 818 else |