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