Source for org.jfree.chart.axis.PeriodAxis

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