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