001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * --------------- 028 * PeriodAxis.java 029 * --------------- 030 * (C) Copyright 2004-2009, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 01-Jun-2004 : Version 1 (DG); 038 * 16-Sep-2004 : Fixed bug in equals() method, added clone() method and 039 * PublicCloneable interface (DG); 040 * 25-Nov-2004 : Updates to support major and minor tick marks (DG); 041 * 25-Feb-2005 : Fixed some tick mark bugs (DG); 042 * 15-Apr-2005 : Fixed some more tick mark bugs (DG); 043 * 26-Apr-2005 : Removed LOGGER (DG); 044 * 16-Jun-2005 : Fixed zooming (DG); 045 * 15-Sep-2005 : Changed configure() method to check autoRange flag, 046 * and added ticks to state (DG); 047 * ------------- JFREECHART 1.0.x --------------------------------------------- 048 * 06-Oct-2006 : Updated for deprecations in RegularTimePeriod and 049 * subclasses (DG); 050 * 22-Mar-2007 : Use new defaultAutoRange attribute (DG); 051 * 31-Jul-2007 : Fix for inverted axis labelling (see bug 1763413) (DG); 052 * 08-Apr-2008 : Notify listeners in setRange(Range, boolean, boolean) - fixes 053 * bug 1932146 (DG); 054 * 16-Jan-2009 : Fixed bug 2490803, a problem in the setRange() method (DG); 055 * 02-Mar-2009 : Added locale - see patch 2569670 by Benjamin Bignell (DG); 056 * 02-Mar-2009 : Fixed draw() method to check tickMarksVisible and 057 * tickLabelsVisible (DG); 058 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG); 059 * 060 */ 061 062 package org.jfree.chart.axis; 063 064 import java.awt.BasicStroke; 065 import java.awt.Color; 066 import java.awt.FontMetrics; 067 import java.awt.Graphics2D; 068 import java.awt.Paint; 069 import java.awt.Stroke; 070 import java.awt.geom.Line2D; 071 import java.awt.geom.Rectangle2D; 072 import java.io.IOException; 073 import java.io.ObjectInputStream; 074 import java.io.ObjectOutputStream; 075 import java.io.Serializable; 076 import java.lang.reflect.Constructor; 077 import java.text.DateFormat; 078 import java.text.SimpleDateFormat; 079 import java.util.ArrayList; 080 import java.util.Arrays; 081 import java.util.Calendar; 082 import java.util.Collections; 083 import java.util.Date; 084 import java.util.List; 085 import java.util.Locale; 086 import java.util.TimeZone; 087 088 import org.jfree.chart.event.AxisChangeEvent; 089 import org.jfree.chart.plot.Plot; 090 import org.jfree.chart.plot.PlotRenderingInfo; 091 import org.jfree.chart.plot.ValueAxisPlot; 092 import org.jfree.data.Range; 093 import org.jfree.data.time.Day; 094 import org.jfree.data.time.Month; 095 import org.jfree.data.time.RegularTimePeriod; 096 import org.jfree.data.time.Year; 097 import org.jfree.io.SerialUtilities; 098 import org.jfree.text.TextUtilities; 099 import org.jfree.ui.RectangleEdge; 100 import org.jfree.ui.TextAnchor; 101 import org.jfree.util.PublicCloneable; 102 103 /** 104 * An axis that displays a date scale based on a 105 * {@link org.jfree.data.time.RegularTimePeriod}. This axis works when 106 * displayed across the bottom or top of a plot, but is broken for display at 107 * the left or right of charts. 108 */ 109 public class PeriodAxis extends ValueAxis 110 implements Cloneable, PublicCloneable, Serializable { 111 112 /** For serialization. */ 113 private static final long serialVersionUID = 8353295532075872069L; 114 115 /** The first time period in the overall range. */ 116 private RegularTimePeriod first; 117 118 /** The last time period in the overall range. */ 119 private RegularTimePeriod last; 120 121 /** 122 * The time zone used to convert 'first' and 'last' to absolute 123 * milliseconds. 124 */ 125 private TimeZone timeZone; 126 127 /** 128 * The locale (never <code>null</code>). 129 * 130 * @since 1.0.13 131 */ 132 private Locale locale; 133 134 /** 135 * A calendar used for date manipulations in the current time zone and 136 * locale. 137 */ 138 private Calendar calendar; 139 140 /** 141 * The {@link RegularTimePeriod} subclass used to automatically determine 142 * the axis range. 143 */ 144 private Class autoRangeTimePeriodClass; 145 146 /** 147 * Indicates the {@link RegularTimePeriod} subclass that is used to 148 * determine the spacing of the major tick marks. 149 */ 150 private Class majorTickTimePeriodClass; 151 152 /** 153 * A flag that indicates whether or not tick marks are visible for the 154 * axis. 155 */ 156 private boolean minorTickMarksVisible; 157 158 /** 159 * Indicates the {@link RegularTimePeriod} subclass that is used to 160 * determine the spacing of the minor tick marks. 161 */ 162 private Class minorTickTimePeriodClass; 163 164 /** The length of the tick mark inside the data area (zero permitted). */ 165 private float minorTickMarkInsideLength = 0.0f; 166 167 /** The length of the tick mark outside the data area (zero permitted). */ 168 private float minorTickMarkOutsideLength = 2.0f; 169 170 /** The stroke used to draw tick marks. */ 171 private transient Stroke minorTickMarkStroke = new BasicStroke(0.5f); 172 173 /** The paint used to draw tick marks. */ 174 private transient Paint minorTickMarkPaint = Color.black; 175 176 /** Info for each labelling band. */ 177 private PeriodAxisLabelInfo[] labelInfo; 178 179 /** 180 * Creates a new axis. 181 * 182 * @param label the axis label. 183 */ 184 public PeriodAxis(String label) { 185 this(label, new Day(), new Day()); 186 } 187 188 /** 189 * Creates a new axis. 190 * 191 * @param label the axis label (<code>null</code> permitted). 192 * @param first the first time period in the axis range 193 * (<code>null</code> not permitted). 194 * @param last the last time period in the axis range 195 * (<code>null</code> not permitted). 196 */ 197 public PeriodAxis(String label, 198 RegularTimePeriod first, RegularTimePeriod last) { 199 this(label, first, last, TimeZone.getDefault(), Locale.getDefault()); 200 } 201 202 /** 203 * Creates a new axis. 204 * 205 * @param label the axis label (<code>null</code> permitted). 206 * @param first the first time period in the axis range 207 * (<code>null</code> not permitted). 208 * @param last the last time period in the axis range 209 * (<code>null</code> not permitted). 210 * @param timeZone the time zone (<code>null</code> not permitted). 211 * 212 * @deprecated As of version 1.0.13, you should use the constructor that 213 * specifies a Locale also. 214 */ 215 public PeriodAxis(String label, 216 RegularTimePeriod first, RegularTimePeriod last, 217 TimeZone timeZone) { 218 this(label, first, last, timeZone, Locale.getDefault()); 219 } 220 221 /** 222 * Creates a new axis. 223 * 224 * @param label the axis label (<code>null</code> permitted). 225 * @param first the first time period in the axis range 226 * (<code>null</code> not permitted). 227 * @param last the last time period in the axis range 228 * (<code>null</code> not permitted). 229 * @param timeZone the time zone (<code>null</code> not permitted). 230 * @param locale the locale (<code>null</code> not permitted). 231 * 232 * @since 1.0.13 233 */ 234 public PeriodAxis(String label, RegularTimePeriod first, 235 RegularTimePeriod last, TimeZone timeZone, Locale locale) { 236 super(label, null); 237 if (timeZone == null) { 238 throw new IllegalArgumentException("Null 'timeZone' argument."); 239 } 240 if (locale == null) { 241 throw new IllegalArgumentException("Null 'locale' argument."); 242 } 243 this.first = first; 244 this.last = last; 245 this.timeZone = timeZone; 246 this.locale = locale; 247 this.calendar = Calendar.getInstance(timeZone, locale); 248 this.first.peg(this.calendar); 249 this.last.peg(this.calendar); 250 this.autoRangeTimePeriodClass = first.getClass(); 251 this.majorTickTimePeriodClass = first.getClass(); 252 this.minorTickMarksVisible = false; 253 this.minorTickTimePeriodClass = RegularTimePeriod.downsize( 254 this.majorTickTimePeriodClass); 255 setAutoRange(true); 256 this.labelInfo = new PeriodAxisLabelInfo[2]; 257 this.labelInfo[0] = new PeriodAxisLabelInfo(Month.class, 258 new SimpleDateFormat("MMM", locale)); 259 this.labelInfo[1] = new PeriodAxisLabelInfo(Year.class, 260 new SimpleDateFormat("yyyy", locale)); 261 } 262 263 /** 264 * Returns the first time period in the axis range. 265 * 266 * @return The first time period (never <code>null</code>). 267 */ 268 public RegularTimePeriod getFirst() { 269 return this.first; 270 } 271 272 /** 273 * Sets the first time period in the axis range and sends an 274 * {@link AxisChangeEvent} to all registered listeners. 275 * 276 * @param first the time period (<code>null</code> not permitted). 277 */ 278 public void setFirst(RegularTimePeriod first) { 279 if (first == null) { 280 throw new IllegalArgumentException("Null 'first' argument."); 281 } 282 this.first = first; 283 this.first.peg(this.calendar); 284 notifyListeners(new AxisChangeEvent(this)); 285 } 286 287 /** 288 * Returns the last time period in the axis range. 289 * 290 * @return The last time period (never <code>null</code>). 291 */ 292 public RegularTimePeriod getLast() { 293 return this.last; 294 } 295 296 /** 297 * Sets the last time period in the axis range and sends an 298 * {@link AxisChangeEvent} to all registered listeners. 299 * 300 * @param last the time period (<code>null</code> not permitted). 301 */ 302 public void setLast(RegularTimePeriod last) { 303 if (last == null) { 304 throw new IllegalArgumentException("Null 'last' argument."); 305 } 306 this.last = last; 307 this.last.peg(this.calendar); 308 notifyListeners(new AxisChangeEvent(this)); 309 } 310 311 /** 312 * Returns the time zone used to convert the periods defining the axis 313 * range into absolute milliseconds. 314 * 315 * @return The time zone (never <code>null</code>). 316 */ 317 public TimeZone getTimeZone() { 318 return this.timeZone; 319 } 320 321 /** 322 * Sets the time zone that is used to convert the time periods into 323 * absolute milliseconds. 324 * 325 * @param zone the time zone (<code>null</code> not permitted). 326 */ 327 public void setTimeZone(TimeZone zone) { 328 if (zone == null) { 329 throw new IllegalArgumentException("Null 'zone' argument."); 330 } 331 this.timeZone = zone; 332 this.calendar = Calendar.getInstance(zone, this.locale); 333 this.first.peg(this.calendar); 334 this.last.peg(this.calendar); 335 notifyListeners(new AxisChangeEvent(this)); 336 } 337 338 /** 339 * Returns the locale for this axis. 340 * 341 * @return The locale (never (<code>null</code>). 342 * 343 * @since 1.0.13 344 */ 345 public Locale getLocale() { 346 return this.locale; 347 } 348 349 /** 350 * Returns the class used to create the first and last time periods for 351 * the axis range when the auto-range flag is set to <code>true</code>. 352 * 353 * @return The class (never <code>null</code>). 354 */ 355 public Class getAutoRangeTimePeriodClass() { 356 return this.autoRangeTimePeriodClass; 357 } 358 359 /** 360 * Sets the class used to create the first and last time periods for the 361 * axis range when the auto-range flag is set to <code>true</code> and 362 * sends an {@link AxisChangeEvent} to all registered listeners. 363 * 364 * @param c the class (<code>null</code> not permitted). 365 */ 366 public void setAutoRangeTimePeriodClass(Class c) { 367 if (c == null) { 368 throw new IllegalArgumentException("Null 'c' argument."); 369 } 370 this.autoRangeTimePeriodClass = c; 371 notifyListeners(new AxisChangeEvent(this)); 372 } 373 374 /** 375 * Returns the class that controls the spacing of the major tick marks. 376 * 377 * @return The class (never <code>null</code>). 378 */ 379 public Class getMajorTickTimePeriodClass() { 380 return this.majorTickTimePeriodClass; 381 } 382 383 /** 384 * Sets the class that controls the spacing of the major tick marks, and 385 * sends an {@link AxisChangeEvent} to all registered listeners. 386 * 387 * @param c the class (a subclass of {@link RegularTimePeriod} is 388 * expected). 389 */ 390 public void setMajorTickTimePeriodClass(Class c) { 391 if (c == null) { 392 throw new IllegalArgumentException("Null 'c' argument."); 393 } 394 this.majorTickTimePeriodClass = c; 395 notifyListeners(new AxisChangeEvent(this)); 396 } 397 398 /** 399 * Returns the flag that controls whether or not minor tick marks 400 * are displayed for the axis. 401 * 402 * @return A boolean. 403 */ 404 public boolean isMinorTickMarksVisible() { 405 return this.minorTickMarksVisible; 406 } 407 408 /** 409 * Sets the flag that controls whether or not minor tick marks 410 * are displayed for the axis, and sends a {@link AxisChangeEvent} 411 * to all registered listeners. 412 * 413 * @param visible the flag. 414 */ 415 public void setMinorTickMarksVisible(boolean visible) { 416 this.minorTickMarksVisible = visible; 417 notifyListeners(new AxisChangeEvent(this)); 418 } 419 420 /** 421 * Returns the class that controls the spacing of the minor tick marks. 422 * 423 * @return The class (never <code>null</code>). 424 */ 425 public Class getMinorTickTimePeriodClass() { 426 return this.minorTickTimePeriodClass; 427 } 428 429 /** 430 * Sets the class that controls the spacing of the minor tick marks, and 431 * sends an {@link AxisChangeEvent} to all registered listeners. 432 * 433 * @param c the class (a subclass of {@link RegularTimePeriod} is 434 * expected). 435 */ 436 public void setMinorTickTimePeriodClass(Class c) { 437 if (c == null) { 438 throw new IllegalArgumentException("Null 'c' argument."); 439 } 440 this.minorTickTimePeriodClass = c; 441 notifyListeners(new AxisChangeEvent(this)); 442 } 443 444 /** 445 * Returns the stroke used to display minor tick marks, if they are 446 * visible. 447 * 448 * @return A stroke (never <code>null</code>). 449 */ 450 public Stroke getMinorTickMarkStroke() { 451 return this.minorTickMarkStroke; 452 } 453 454 /** 455 * Sets the stroke used to display minor tick marks, if they are 456 * visible, and sends a {@link AxisChangeEvent} to all registered 457 * listeners. 458 * 459 * @param stroke the stroke (<code>null</code> not permitted). 460 */ 461 public void setMinorTickMarkStroke(Stroke stroke) { 462 if (stroke == null) { 463 throw new IllegalArgumentException("Null 'stroke' argument."); 464 } 465 this.minorTickMarkStroke = stroke; 466 notifyListeners(new AxisChangeEvent(this)); 467 } 468 469 /** 470 * Returns the paint used to display minor tick marks, if they are 471 * visible. 472 * 473 * @return A paint (never <code>null</code>). 474 */ 475 public Paint getMinorTickMarkPaint() { 476 return this.minorTickMarkPaint; 477 } 478 479 /** 480 * Sets the paint used to display minor tick marks, if they are 481 * visible, and sends a {@link AxisChangeEvent} to all registered 482 * listeners. 483 * 484 * @param paint the paint (<code>null</code> not permitted). 485 */ 486 public void setMinorTickMarkPaint(Paint paint) { 487 if (paint == null) { 488 throw new IllegalArgumentException("Null 'paint' argument."); 489 } 490 this.minorTickMarkPaint = paint; 491 notifyListeners(new AxisChangeEvent(this)); 492 } 493 494 /** 495 * Returns the inside length for the minor tick marks. 496 * 497 * @return The length. 498 */ 499 public float getMinorTickMarkInsideLength() { 500 return this.minorTickMarkInsideLength; 501 } 502 503 /** 504 * Sets the inside length of the minor tick marks and sends an 505 * {@link AxisChangeEvent} to all registered listeners. 506 * 507 * @param length the length. 508 */ 509 public void setMinorTickMarkInsideLength(float length) { 510 this.minorTickMarkInsideLength = length; 511 notifyListeners(new AxisChangeEvent(this)); 512 } 513 514 /** 515 * Returns the outside length for the minor tick marks. 516 * 517 * @return The length. 518 */ 519 public float getMinorTickMarkOutsideLength() { 520 return this.minorTickMarkOutsideLength; 521 } 522 523 /** 524 * Sets the outside length of the minor tick marks and sends an 525 * {@link AxisChangeEvent} to all registered listeners. 526 * 527 * @param length the length. 528 */ 529 public void setMinorTickMarkOutsideLength(float length) { 530 this.minorTickMarkOutsideLength = length; 531 notifyListeners(new AxisChangeEvent(this)); 532 } 533 534 /** 535 * Returns an array of label info records. 536 * 537 * @return An array. 538 */ 539 public PeriodAxisLabelInfo[] getLabelInfo() { 540 return this.labelInfo; 541 } 542 543 /** 544 * Sets the array of label info records and sends an 545 * {@link AxisChangeEvent} to all registered listeners. 546 * 547 * @param info the info. 548 */ 549 public void setLabelInfo(PeriodAxisLabelInfo[] info) { 550 this.labelInfo = info; 551 notifyListeners(new AxisChangeEvent(this)); 552 } 553 554 /** 555 * Sets the range for the axis, if requested, sends an 556 * {@link AxisChangeEvent} to all registered listeners. As a side-effect, 557 * the auto-range flag is set to <code>false</code> (optional). 558 * 559 * @param range the range (<code>null</code> not permitted). 560 * @param turnOffAutoRange a flag that controls whether or not the auto 561 * range is turned off. 562 * @param notify a flag that controls whether or not listeners are 563 * notified. 564 */ 565 public void setRange(Range range, boolean turnOffAutoRange, 566 boolean notify) { 567 long upper = Math.round(range.getUpperBound()); 568 long lower = Math.round(range.getLowerBound()); 569 this.first = createInstance(this.autoRangeTimePeriodClass, 570 new Date(lower), this.timeZone, this.locale); 571 this.last = createInstance(this.autoRangeTimePeriodClass, 572 new Date(upper), this.timeZone, this.locale); 573 super.setRange(new Range(this.first.getFirstMillisecond(), 574 this.last.getLastMillisecond() + 1.0), turnOffAutoRange, 575 notify); 576 } 577 578 /** 579 * Configures the axis to work with the current plot. Override this method 580 * to perform any special processing (such as auto-rescaling). 581 */ 582 public void configure() { 583 if (this.isAutoRange()) { 584 autoAdjustRange(); 585 } 586 } 587 588 /** 589 * Estimates the space (height or width) required to draw the axis. 590 * 591 * @param g2 the graphics device. 592 * @param plot the plot that the axis belongs to. 593 * @param plotArea the area within which the plot (including axes) should 594 * be drawn. 595 * @param edge the axis location. 596 * @param space space already reserved. 597 * 598 * @return The space required to draw the axis (including pre-reserved 599 * space). 600 */ 601 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 602 Rectangle2D plotArea, RectangleEdge edge, 603 AxisSpace space) { 604 // create a new space object if one wasn't supplied... 605 if (space == null) { 606 space = new AxisSpace(); 607 } 608 609 // if the axis is not visible, no additional space is required... 610 if (!isVisible()) { 611 return space; 612 } 613 614 // if the axis has a fixed dimension, return it... 615 double dimension = getFixedDimension(); 616 if (dimension > 0.0) { 617 space.ensureAtLeast(dimension, edge); 618 } 619 620 // get the axis label size and update the space object... 621 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge); 622 double labelHeight = 0.0; 623 double labelWidth = 0.0; 624 double tickLabelBandsDimension = 0.0; 625 626 for (int i = 0; i < this.labelInfo.length; i++) { 627 PeriodAxisLabelInfo info = this.labelInfo[i]; 628 FontMetrics fm = g2.getFontMetrics(info.getLabelFont()); 629 tickLabelBandsDimension 630 += info.getPadding().extendHeight(fm.getHeight()); 631 } 632 633 if (RectangleEdge.isTopOrBottom(edge)) { 634 labelHeight = labelEnclosure.getHeight(); 635 space.add(labelHeight + tickLabelBandsDimension, edge); 636 } 637 else if (RectangleEdge.isLeftOrRight(edge)) { 638 labelWidth = labelEnclosure.getWidth(); 639 space.add(labelWidth + tickLabelBandsDimension, edge); 640 } 641 642 // add space for the outer tick labels, if any... 643 double tickMarkSpace = 0.0; 644 if (isTickMarksVisible()) { 645 tickMarkSpace = getTickMarkOutsideLength(); 646 } 647 if (this.minorTickMarksVisible) { 648 tickMarkSpace = Math.max(tickMarkSpace, 649 this.minorTickMarkOutsideLength); 650 } 651 space.add(tickMarkSpace, edge); 652 return space; 653 } 654 655 /** 656 * Draws the axis on a Java 2D graphics device (such as the screen or a 657 * printer). 658 * 659 * @param g2 the graphics device (<code>null</code> not permitted). 660 * @param cursor the cursor location (determines where to draw the axis). 661 * @param plotArea the area within which the axes and plot should be drawn. 662 * @param dataArea the area within which the data should be drawn. 663 * @param edge the axis location (<code>null</code> not permitted). 664 * @param plotState collects information about the plot 665 * (<code>null</code> permitted). 666 * 667 * @return The axis state (never <code>null</code>). 668 */ 669 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 670 Rectangle2D dataArea, RectangleEdge edge, 671 PlotRenderingInfo plotState) { 672 673 AxisState axisState = new AxisState(cursor); 674 if (isAxisLineVisible()) { 675 drawAxisLine(g2, cursor, dataArea, edge); 676 } 677 if (isTickMarksVisible()) { 678 drawTickMarks(g2, axisState, dataArea, edge); 679 } 680 if (isTickLabelsVisible()) { 681 for (int band = 0; band < this.labelInfo.length; band++) { 682 axisState = drawTickLabels(band, g2, axisState, dataArea, edge); 683 } 684 } 685 686 // draw the axis label (note that 'state' is passed in *and* 687 // returned)... 688 axisState = drawLabel(getLabel(), g2, plotArea, dataArea, edge, 689 axisState); 690 return axisState; 691 692 } 693 694 /** 695 * Draws the tick marks for the axis. 696 * 697 * @param g2 the graphics device. 698 * @param state the axis state. 699 * @param dataArea the data area. 700 * @param edge the edge. 701 */ 702 protected void drawTickMarks(Graphics2D g2, AxisState state, 703 Rectangle2D dataArea, 704 RectangleEdge edge) { 705 if (RectangleEdge.isTopOrBottom(edge)) { 706 drawTickMarksHorizontal(g2, state, dataArea, edge); 707 } 708 else if (RectangleEdge.isLeftOrRight(edge)) { 709 drawTickMarksVertical(g2, state, dataArea, edge); 710 } 711 } 712 713 /** 714 * Draws the major and minor tick marks for an axis that lies at the top or 715 * bottom of the plot. 716 * 717 * @param g2 the graphics device. 718 * @param state the axis state. 719 * @param dataArea the data area. 720 * @param edge the edge. 721 */ 722 protected void drawTickMarksHorizontal(Graphics2D g2, AxisState state, 723 Rectangle2D dataArea, 724 RectangleEdge edge) { 725 List ticks = new ArrayList(); 726 double x0; 727 double y0 = state.getCursor(); 728 double insideLength = getTickMarkInsideLength(); 729 double outsideLength = getTickMarkOutsideLength(); 730 RegularTimePeriod t = createInstance(this.majorTickTimePeriodClass, 731 this.first.getStart(), getTimeZone(), this.locale); 732 long t0 = t.getFirstMillisecond(); 733 Line2D inside = null; 734 Line2D outside = null; 735 long firstOnAxis = getFirst().getFirstMillisecond(); 736 long lastOnAxis = getLast().getLastMillisecond() + 1; 737 while (t0 <= lastOnAxis) { 738 ticks.add(new NumberTick(new Double(t0), "", TextAnchor.CENTER, 739 TextAnchor.CENTER, 0.0)); 740 x0 = valueToJava2D(t0, dataArea, edge); 741 if (edge == RectangleEdge.TOP) { 742 inside = new Line2D.Double(x0, y0, x0, y0 + insideLength); 743 outside = new Line2D.Double(x0, y0, x0, y0 - outsideLength); 744 } 745 else if (edge == RectangleEdge.BOTTOM) { 746 inside = new Line2D.Double(x0, y0, x0, y0 - insideLength); 747 outside = new Line2D.Double(x0, y0, x0, y0 + outsideLength); 748 } 749 if (t0 >= firstOnAxis) { 750 g2.setPaint(getTickMarkPaint()); 751 g2.setStroke(getTickMarkStroke()); 752 g2.draw(inside); 753 g2.draw(outside); 754 } 755 // draw minor tick marks 756 if (this.minorTickMarksVisible) { 757 RegularTimePeriod tminor = createInstance( 758 this.minorTickTimePeriodClass, new Date(t0), 759 getTimeZone(), this.locale); 760 long tt0 = tminor.getFirstMillisecond(); 761 while (tt0 < t.getLastMillisecond() 762 && tt0 < lastOnAxis) { 763 double xx0 = valueToJava2D(tt0, dataArea, edge); 764 if (edge == RectangleEdge.TOP) { 765 inside = new Line2D.Double(xx0, y0, xx0, 766 y0 + this.minorTickMarkInsideLength); 767 outside = new Line2D.Double(xx0, y0, xx0, 768 y0 - this.minorTickMarkOutsideLength); 769 } 770 else if (edge == RectangleEdge.BOTTOM) { 771 inside = new Line2D.Double(xx0, y0, xx0, 772 y0 - this.minorTickMarkInsideLength); 773 outside = new Line2D.Double(xx0, y0, xx0, 774 y0 + this.minorTickMarkOutsideLength); 775 } 776 if (tt0 >= firstOnAxis) { 777 g2.setPaint(this.minorTickMarkPaint); 778 g2.setStroke(this.minorTickMarkStroke); 779 g2.draw(inside); 780 g2.draw(outside); 781 } 782 tminor = tminor.next(); 783 tminor.peg(this.calendar); 784 tt0 = tminor.getFirstMillisecond(); 785 } 786 } 787 t = t.next(); 788 t.peg(this.calendar); 789 t0 = t.getFirstMillisecond(); 790 } 791 if (edge == RectangleEdge.TOP) { 792 state.cursorUp(Math.max(outsideLength, 793 this.minorTickMarkOutsideLength)); 794 } 795 else if (edge == RectangleEdge.BOTTOM) { 796 state.cursorDown(Math.max(outsideLength, 797 this.minorTickMarkOutsideLength)); 798 } 799 state.setTicks(ticks); 800 } 801 802 /** 803 * Draws the tick marks for a vertical axis. 804 * 805 * @param g2 the graphics device. 806 * @param state the axis state. 807 * @param dataArea the data area. 808 * @param edge the edge. 809 */ 810 protected void drawTickMarksVertical(Graphics2D g2, AxisState state, 811 Rectangle2D dataArea, 812 RectangleEdge edge) { 813 // FIXME: implement this... 814 } 815 816 /** 817 * Draws the tick labels for one "band" of time periods. 818 * 819 * @param band the band index (zero-based). 820 * @param g2 the graphics device. 821 * @param state the axis state. 822 * @param dataArea the data area. 823 * @param edge the edge where the axis is located. 824 * 825 * @return The updated axis state. 826 */ 827 protected AxisState drawTickLabels(int band, Graphics2D g2, AxisState state, 828 Rectangle2D dataArea, 829 RectangleEdge edge) { 830 831 // work out the initial gap 832 double delta1 = 0.0; 833 FontMetrics fm = g2.getFontMetrics(this.labelInfo[band].getLabelFont()); 834 if (edge == RectangleEdge.BOTTOM) { 835 delta1 = this.labelInfo[band].getPadding().calculateTopOutset( 836 fm.getHeight()); 837 } 838 else if (edge == RectangleEdge.TOP) { 839 delta1 = this.labelInfo[band].getPadding().calculateBottomOutset( 840 fm.getHeight()); 841 } 842 state.moveCursor(delta1, edge); 843 long axisMin = this.first.getFirstMillisecond(); 844 long axisMax = this.last.getLastMillisecond(); 845 g2.setFont(this.labelInfo[band].getLabelFont()); 846 g2.setPaint(this.labelInfo[band].getLabelPaint()); 847 848 // work out the number of periods to skip for labelling 849 RegularTimePeriod p1 = this.labelInfo[band].createInstance( 850 new Date(axisMin), this.timeZone, this.locale); 851 RegularTimePeriod p2 = this.labelInfo[band].createInstance( 852 new Date(axisMax), this.timeZone, this.locale); 853 String label1 = this.labelInfo[band].getDateFormat().format( 854 new Date(p1.getMiddleMillisecond())); 855 String label2 = this.labelInfo[band].getDateFormat().format( 856 new Date(p2.getMiddleMillisecond())); 857 Rectangle2D b1 = TextUtilities.getTextBounds(label1, g2, 858 g2.getFontMetrics()); 859 Rectangle2D b2 = TextUtilities.getTextBounds(label2, g2, 860 g2.getFontMetrics()); 861 double w = Math.max(b1.getWidth(), b2.getWidth()); 862 long ww = Math.round(java2DToValue(dataArea.getX() + w + 5.0, 863 dataArea, edge)); 864 if (isInverted()) { 865 ww = axisMax - ww; 866 } 867 else { 868 ww = ww - axisMin; 869 } 870 long length = p1.getLastMillisecond() 871 - p1.getFirstMillisecond(); 872 int periods = (int) (ww / length) + 1; 873 874 RegularTimePeriod p = this.labelInfo[band].createInstance( 875 new Date(axisMin), this.timeZone, this.locale); 876 Rectangle2D b = null; 877 long lastXX = 0L; 878 float y = (float) (state.getCursor()); 879 TextAnchor anchor = TextAnchor.TOP_CENTER; 880 float yDelta = (float) b1.getHeight(); 881 if (edge == RectangleEdge.TOP) { 882 anchor = TextAnchor.BOTTOM_CENTER; 883 yDelta = -yDelta; 884 } 885 while (p.getFirstMillisecond() <= axisMax) { 886 float x = (float) valueToJava2D(p.getMiddleMillisecond(), dataArea, 887 edge); 888 DateFormat df = this.labelInfo[band].getDateFormat(); 889 String label = df.format(new Date(p.getMiddleMillisecond())); 890 long first = p.getFirstMillisecond(); 891 long last = p.getLastMillisecond(); 892 if (last > axisMax) { 893 // this is the last period, but it is only partially visible 894 // so check that the label will fit before displaying it... 895 Rectangle2D bb = TextUtilities.getTextBounds(label, g2, 896 g2.getFontMetrics()); 897 if ((x + bb.getWidth() / 2) > dataArea.getMaxX()) { 898 float xstart = (float) valueToJava2D(Math.max(first, 899 axisMin), dataArea, edge); 900 if (bb.getWidth() < (dataArea.getMaxX() - xstart)) { 901 x = ((float) dataArea.getMaxX() + xstart) / 2.0f; 902 } 903 else { 904 label = null; 905 } 906 } 907 } 908 if (first < axisMin) { 909 // this is the first period, but it is only partially visible 910 // so check that the label will fit before displaying it... 911 Rectangle2D bb = TextUtilities.getTextBounds(label, g2, 912 g2.getFontMetrics()); 913 if ((x - bb.getWidth() / 2) < dataArea.getX()) { 914 float xlast = (float) valueToJava2D(Math.min(last, 915 axisMax), dataArea, edge); 916 if (bb.getWidth() < (xlast - dataArea.getX())) { 917 x = (xlast + (float) dataArea.getX()) / 2.0f; 918 } 919 else { 920 label = null; 921 } 922 } 923 924 } 925 if (label != null) { 926 g2.setPaint(this.labelInfo[band].getLabelPaint()); 927 b = TextUtilities.drawAlignedString(label, g2, x, y, anchor); 928 } 929 if (lastXX > 0L) { 930 if (this.labelInfo[band].getDrawDividers()) { 931 long nextXX = p.getFirstMillisecond(); 932 long mid = (lastXX + nextXX) / 2; 933 float mid2d = (float) valueToJava2D(mid, dataArea, edge); 934 g2.setStroke(this.labelInfo[band].getDividerStroke()); 935 g2.setPaint(this.labelInfo[band].getDividerPaint()); 936 g2.draw(new Line2D.Float(mid2d, y, mid2d, y + yDelta)); 937 } 938 } 939 lastXX = last; 940 for (int i = 0; i < periods; i++) { 941 p = p.next(); 942 } 943 p.peg(this.calendar); 944 } 945 double used = 0.0; 946 if (b != null) { 947 used = b.getHeight(); 948 // work out the trailing gap 949 if (edge == RectangleEdge.BOTTOM) { 950 used += this.labelInfo[band].getPadding().calculateBottomOutset( 951 fm.getHeight()); 952 } 953 else if (edge == RectangleEdge.TOP) { 954 used += this.labelInfo[band].getPadding().calculateTopOutset( 955 fm.getHeight()); 956 } 957 } 958 state.moveCursor(used, edge); 959 return state; 960 } 961 962 /** 963 * Calculates the positions of the ticks for the axis, storing the results 964 * in the tick list (ready for drawing). 965 * 966 * @param g2 the graphics device. 967 * @param state the axis state. 968 * @param dataArea the area inside the axes. 969 * @param edge the edge on which the axis is located. 970 * 971 * @return The list of ticks. 972 */ 973 public List refreshTicks(Graphics2D g2, AxisState state, 974 Rectangle2D dataArea, RectangleEdge edge) { 975 return Collections.EMPTY_LIST; 976 } 977 978 /** 979 * Converts a data value to a coordinate in Java2D space, assuming that the 980 * axis runs along one edge of the specified dataArea. 981 * <p> 982 * Note that it is possible for the coordinate to fall outside the area. 983 * 984 * @param value the data value. 985 * @param area the area for plotting the data. 986 * @param edge the edge along which the axis lies. 987 * 988 * @return The Java2D coordinate. 989 */ 990 public double valueToJava2D(double value, Rectangle2D area, 991 RectangleEdge edge) { 992 993 double result = Double.NaN; 994 double axisMin = this.first.getFirstMillisecond(); 995 double axisMax = this.last.getLastMillisecond(); 996 if (RectangleEdge.isTopOrBottom(edge)) { 997 double minX = area.getX(); 998 double maxX = area.getMaxX(); 999 if (isInverted()) { 1000 result = maxX + ((value - axisMin) / (axisMax - axisMin)) 1001 * (minX - maxX); 1002 } 1003 else { 1004 result = minX + ((value - axisMin) / (axisMax - axisMin)) 1005 * (maxX - minX); 1006 } 1007 } 1008 else if (RectangleEdge.isLeftOrRight(edge)) { 1009 double minY = area.getMinY(); 1010 double maxY = area.getMaxY(); 1011 if (isInverted()) { 1012 result = minY + (((value - axisMin) / (axisMax - axisMin)) 1013 * (maxY - minY)); 1014 } 1015 else { 1016 result = maxY - (((value - axisMin) / (axisMax - axisMin)) 1017 * (maxY - minY)); 1018 } 1019 } 1020 return result; 1021 1022 } 1023 1024 /** 1025 * Converts a coordinate in Java2D space to the corresponding data value, 1026 * assuming that the axis runs along one edge of the specified dataArea. 1027 * 1028 * @param java2DValue the coordinate in Java2D space. 1029 * @param area the area in which the data is plotted. 1030 * @param edge the edge along which the axis lies. 1031 * 1032 * @return The data value. 1033 */ 1034 public double java2DToValue(double java2DValue, Rectangle2D area, 1035 RectangleEdge edge) { 1036 1037 double result = Double.NaN; 1038 double min = 0.0; 1039 double max = 0.0; 1040 double axisMin = this.first.getFirstMillisecond(); 1041 double axisMax = this.last.getLastMillisecond(); 1042 if (RectangleEdge.isTopOrBottom(edge)) { 1043 min = area.getX(); 1044 max = area.getMaxX(); 1045 } 1046 else if (RectangleEdge.isLeftOrRight(edge)) { 1047 min = area.getMaxY(); 1048 max = area.getY(); 1049 } 1050 if (isInverted()) { 1051 result = axisMax - ((java2DValue - min) / (max - min) 1052 * (axisMax - axisMin)); 1053 } 1054 else { 1055 result = axisMin + ((java2DValue - min) / (max - min) 1056 * (axisMax - axisMin)); 1057 } 1058 return result; 1059 } 1060 1061 /** 1062 * Rescales the axis to ensure that all data is visible. 1063 */ 1064 protected void autoAdjustRange() { 1065 1066 Plot plot = getPlot(); 1067 if (plot == null) { 1068 return; // no plot, no data 1069 } 1070 1071 if (plot instanceof ValueAxisPlot) { 1072 ValueAxisPlot vap = (ValueAxisPlot) plot; 1073 1074 Range r = vap.getDataRange(this); 1075 if (r == null) { 1076 r = getDefaultAutoRange(); 1077 } 1078 1079 long upper = Math.round(r.getUpperBound()); 1080 long lower = Math.round(r.getLowerBound()); 1081 this.first = createInstance(this.autoRangeTimePeriodClass, 1082 new Date(lower), this.timeZone, this.locale); 1083 this.last = createInstance(this.autoRangeTimePeriodClass, 1084 new Date(upper), this.timeZone, this.locale); 1085 setRange(r, false, false); 1086 } 1087 1088 } 1089 1090 /** 1091 * Tests the axis for equality with an arbitrary object. 1092 * 1093 * @param obj the object (<code>null</code> permitted). 1094 * 1095 * @return A boolean. 1096 */ 1097 public boolean equals(Object obj) { 1098 if (obj == this) { 1099 return true; 1100 } 1101 if (!(obj instanceof PeriodAxis)) { 1102 return false; 1103 } 1104 PeriodAxis that = (PeriodAxis) obj; 1105 if (!this.first.equals(that.first)) { 1106 return false; 1107 } 1108 if (!this.last.equals(that.last)) { 1109 return false; 1110 } 1111 if (!this.timeZone.equals(that.timeZone)) { 1112 return false; 1113 } 1114 if (!this.locale.equals(that.locale)) { 1115 return false; 1116 } 1117 if (!this.autoRangeTimePeriodClass.equals( 1118 that.autoRangeTimePeriodClass)) { 1119 return false; 1120 } 1121 if (!(isMinorTickMarksVisible() == that.isMinorTickMarksVisible())) { 1122 return false; 1123 } 1124 if (!this.majorTickTimePeriodClass.equals( 1125 that.majorTickTimePeriodClass)) { 1126 return false; 1127 } 1128 if (!this.minorTickTimePeriodClass.equals( 1129 that.minorTickTimePeriodClass)) { 1130 return false; 1131 } 1132 if (!this.minorTickMarkPaint.equals(that.minorTickMarkPaint)) { 1133 return false; 1134 } 1135 if (!this.minorTickMarkStroke.equals(that.minorTickMarkStroke)) { 1136 return false; 1137 } 1138 if (!Arrays.equals(this.labelInfo, that.labelInfo)) { 1139 return false; 1140 } 1141 return super.equals(obj); 1142 } 1143 1144 /** 1145 * Returns a hash code for this object. 1146 * 1147 * @return A hash code. 1148 */ 1149 public int hashCode() { 1150 if (getLabel() != null) { 1151 return getLabel().hashCode(); 1152 } 1153 else { 1154 return 0; 1155 } 1156 } 1157 1158 /** 1159 * Returns a clone of the axis. 1160 * 1161 * @return A clone. 1162 * 1163 * @throws CloneNotSupportedException this class is cloneable, but 1164 * subclasses may not be. 1165 */ 1166 public Object clone() throws CloneNotSupportedException { 1167 PeriodAxis clone = (PeriodAxis) super.clone(); 1168 clone.timeZone = (TimeZone) this.timeZone.clone(); 1169 clone.labelInfo = new PeriodAxisLabelInfo[this.labelInfo.length]; 1170 for (int i = 0; i < this.labelInfo.length; i++) { 1171 clone.labelInfo[i] = this.labelInfo[i]; // copy across references 1172 // to immutable objs 1173 } 1174 return clone; 1175 } 1176 1177 /** 1178 * A utility method used to create a particular subclass of the 1179 * {@link RegularTimePeriod} class that includes the specified millisecond, 1180 * assuming the specified time zone. 1181 * 1182 * @param periodClass the class. 1183 * @param millisecond the time. 1184 * @param zone the time zone. 1185 * @param locale the locale. 1186 * 1187 * @return The time period. 1188 */ 1189 private RegularTimePeriod createInstance(Class periodClass, 1190 Date millisecond, TimeZone zone, Locale locale) { 1191 RegularTimePeriod result = null; 1192 try { 1193 Constructor c = periodClass.getDeclaredConstructor(new Class[] { 1194 Date.class, TimeZone.class, Locale.class}); 1195 result = (RegularTimePeriod) c.newInstance(new Object[] { 1196 millisecond, zone, locale}); 1197 } 1198 catch (Exception e) { 1199 try { 1200 Constructor c = periodClass.getDeclaredConstructor(new Class[] { 1201 Date.class}); 1202 result = (RegularTimePeriod) c.newInstance(new Object[] { 1203 millisecond}); 1204 } 1205 catch (Exception e2) { 1206 // do nothing 1207 } 1208 } 1209 return result; 1210 } 1211 1212 /** 1213 * Provides serialization support. 1214 * 1215 * @param stream the output stream. 1216 * 1217 * @throws IOException if there is an I/O error. 1218 */ 1219 private void writeObject(ObjectOutputStream stream) throws IOException { 1220 stream.defaultWriteObject(); 1221 SerialUtilities.writeStroke(this.minorTickMarkStroke, stream); 1222 SerialUtilities.writePaint(this.minorTickMarkPaint, stream); 1223 } 1224 1225 /** 1226 * Provides serialization support. 1227 * 1228 * @param stream the input stream. 1229 * 1230 * @throws IOException if there is an I/O error. 1231 * @throws ClassNotFoundException if there is a classpath problem. 1232 */ 1233 private void readObject(ObjectInputStream stream) 1234 throws IOException, ClassNotFoundException { 1235 stream.defaultReadObject(); 1236 this.minorTickMarkStroke = SerialUtilities.readStroke(stream); 1237 this.minorTickMarkPaint = SerialUtilities.readPaint(stream); 1238 } 1239 1240 }