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