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