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