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