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