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