Source for org.jfree.chart.axis.DateAxis

   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:  * DateAxis.java
  29:  * -------------
  30:  * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert;
  33:  * Contributor(s):   Jonathan Nash;
  34:  *                   David Li;
  35:  *                   Michael Rauch;
  36:  *                   Bill Kelemen;
  37:  *                   Pawel Pabis;
  38:  *                   Chris Boek;
  39:  *
  40:  * Changes (from 23-Jun-2001)
  41:  * --------------------------
  42:  * 23-Jun-2001 : Modified to work with null data source (DG);
  43:  * 18-Sep-2001 : Updated header (DG);
  44:  * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc
  45:  *               comments (DG);
  46:  * 16-Jan-2002 : Added an optional crosshair, based on the implementation by
  47:  *               Jonathan Nash (DG);
  48:  * 26-Feb-2002 : Updated import statements (DG);
  49:  * 22-Apr-2002 : Added a setRange() method (DG);
  50:  * 25-Jun-2002 : Removed redundant local variable (DG);
  51:  * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
  52:  * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit
  53:  *               selection (fix for bug id 528885) (DG);
  54:  * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis
  55:  *               class (DG);
  56:  * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
  57:  * 25-Sep-2002 : Added new setRange() methods, and deprecated
  58:  *               setAxisRange() (DG);
  59:  * 04-Oct-2002 : Changed auto tick selection to parallel number axis
  60:  *               classes (DG);
  61:  * 24-Oct-2002 : Added a date format override (DG);
  62:  * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
  63:  * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
  64:  *               crosshair settings to the plot (DG);
  65:  * 15-Jan-2003 : Removed anchor date (DG);
  66:  * 20-Jan-2003 : Removed unnecessary constructors (DG);
  67:  * 26-Mar-2003 : Implemented Serializable (DG);
  68:  * 02-May-2003 : Added additional units to createStandardDateTickUnits()
  69:  *               method, as suggested by mhilpert in bug report 723187 (DG);
  70:  * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
  71:  * 24-May-2003 : Added support for underlying timeline for
  72:  *               SegmentedTimeline (BK);
  73:  * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
  74:  * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
  75:  * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
  76:  * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
  77:  * 02-Sep-2003 : Fixes for bug report 790506 (DG);
  78:  * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
  79:  * 10-Sep-2003 : Fixes for segmented timeline (DG);
  80:  * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
  81:  * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
  82:  * 07-Nov-2003 : Modified to use new tick classes (DG);
  83:  * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit
  84:  *               when a calculated tick value is hidden (which can occur in
  85:  *               segmented date axes) (DG);
  86:  * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and
  87:  *               fixed bug 846277 (labels missing for inverted axis) (DG);
  88:  * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit
  89:  *               (ex. 1st of month) was hidden, causing infinite loop (BK);
  90:  * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard
  91:  *               Wardle) (DG);
  92:  * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and
  93:  *               translateValueToJava2D --> valueToJava2D (DG);
  94:  * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical
  95:  *               axis (DG);
  96:  * 16-Mar-2004 : Added plotState to draw() method (DG);
  97:  * 07-Apr-2004 : Changed string width calculation (DG);
  98:  * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id
  99:  *               939148) (DG);
 100:  * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
 101:  *               release (DG);
 102:  * 13-Jan-2005 : Fixed bug (see
 103:  *               http://www.jfree.org/forum/viewtopic.php?t=11330) (DG);
 104:  * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
 105:  *               argument from selectAutoTickUnit() (DG);
 106:  * ------------- JFREECHART 1.0.x ---------------------------------------------
 107:  * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG);
 108:  * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG);
 109:  * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG);
 110:  * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG);
 111:  * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in
 112:  *               previousStandardDate() (DG);
 113:  * 04-Apr-2007 : Use time zone in date calculations (CB);
 114:  * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG);
 115:  * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit
 116:  *               tests (DG);
 117:  * 21-Nov-2007 : Fixed warnings from FindBugs (DG);
 118:  *
 119:  */
 120: 
 121: package org.jfree.chart.axis;
 122: 
 123: import java.awt.Font;
 124: import java.awt.FontMetrics;
 125: import java.awt.Graphics2D;
 126: import java.awt.font.FontRenderContext;
 127: import java.awt.font.LineMetrics;
 128: import java.awt.geom.Rectangle2D;
 129: import java.io.Serializable;
 130: import java.text.DateFormat;
 131: import java.text.SimpleDateFormat;
 132: import java.util.Calendar;
 133: import java.util.Date;
 134: import java.util.List;
 135: import java.util.TimeZone;
 136: 
 137: import org.jfree.chart.event.AxisChangeEvent;
 138: import org.jfree.chart.plot.Plot;
 139: import org.jfree.chart.plot.PlotRenderingInfo;
 140: import org.jfree.chart.plot.ValueAxisPlot;
 141: import org.jfree.data.Range;
 142: import org.jfree.data.time.DateRange;
 143: import org.jfree.data.time.Month;
 144: import org.jfree.data.time.RegularTimePeriod;
 145: import org.jfree.data.time.Year;
 146: import org.jfree.ui.RectangleEdge;
 147: import org.jfree.ui.RectangleInsets;
 148: import org.jfree.ui.TextAnchor;
 149: import org.jfree.util.ObjectUtilities;
 150: 
 151: /**
 152:  * The base class for axes that display dates.  You will find it easier to
 153:  * understand how this axis works if you bear in mind that it really
 154:  * displays/measures integer (or long) data, where the integers are
 155:  * milliseconds since midnight, 1-Jan-1970.  When displaying tick labels, the
 156:  * millisecond values are converted back to dates using a
 157:  * <code>DateFormat</code> instance.
 158:  * <P>
 159:  * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in
 160:  * the constructor to create an axis that only contains certain domain values.
 161:  * For example, this allows you to create a date axis that only contains
 162:  * working days.
 163:  */
 164: public class DateAxis extends ValueAxis implements Cloneable, Serializable {
 165: 
 166:     /** For serialization. */
 167:     private static final long serialVersionUID = -1013460999649007604L;
 168: 
 169:     /** The default axis range. */
 170:     public static final DateRange DEFAULT_DATE_RANGE = new DateRange();
 171: 
 172:     /** The default minimum auto range size. */
 173:     public static final double
 174:             DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0;
 175: 
 176:     /** The default date tick unit. */
 177:     public static final DateTickUnit DEFAULT_DATE_TICK_UNIT
 178:             = new DateTickUnit(DateTickUnit.DAY, 1, new SimpleDateFormat());
 179: 
 180:     /** The default anchor date. */
 181:     public static final Date DEFAULT_ANCHOR_DATE = new Date();
 182: 
 183:     /** The current tick unit. */
 184:     private DateTickUnit tickUnit;
 185: 
 186:     /** The override date format. */
 187:     private DateFormat dateFormatOverride;
 188: 
 189:     /**
 190:      * Tick marks can be displayed at the start or the middle of the time
 191:      * period.
 192:      */
 193:     private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START;
 194: 
 195:     /**
 196:      * A timeline that includes all milliseconds (as defined by
 197:      * <code>java.util.Date</code>) in the real time line.
 198:      */
 199:     private static class DefaultTimeline implements Timeline, Serializable {
 200: 
 201:         /**
 202:          * Converts a millisecond into a timeline value.
 203:          *
 204:          * @param millisecond  the millisecond.
 205:          *
 206:          * @return The timeline value.
 207:          */
 208:         public long toTimelineValue(long millisecond) {
 209:             return millisecond;
 210:         }
 211: 
 212:         /**
 213:          * Converts a date into a timeline value.
 214:          *
 215:          * @param date  the domain value.
 216:          *
 217:          * @return The timeline value.
 218:          */
 219:         public long toTimelineValue(Date date) {
 220:             return date.getTime();
 221:         }
 222: 
 223:         /**
 224:          * Converts a timeline value into a millisecond (as encoded by
 225:          * <code>java.util.Date</code>).
 226:          *
 227:          * @param value  the value.
 228:          *
 229:          * @return The millisecond.
 230:          */
 231:         public long toMillisecond(long value) {
 232:             return value;
 233:         }
 234: 
 235:         /**
 236:          * Returns <code>true</code> if the timeline includes the specified
 237:          * domain value.
 238:          *
 239:          * @param millisecond  the millisecond.
 240:          *
 241:          * @return <code>true</code>.
 242:          */
 243:         public boolean containsDomainValue(long millisecond) {
 244:             return true;
 245:         }
 246: 
 247:         /**
 248:          * Returns <code>true</code> if the timeline includes the specified
 249:          * domain value.
 250:          *
 251:          * @param date  the date.
 252:          *
 253:          * @return <code>true</code>.
 254:          */
 255:         public boolean containsDomainValue(Date date) {
 256:             return true;
 257:         }
 258: 
 259:         /**
 260:          * Returns <code>true</code> if the timeline includes the specified
 261:          * domain value range.
 262:          *
 263:          * @param from  the start value.
 264:          * @param to  the end value.
 265:          *
 266:          * @return <code>true</code>.
 267:          */
 268:         public boolean containsDomainRange(long from, long to) {
 269:             return true;
 270:         }
 271: 
 272:         /**
 273:          * Returns <code>true</code> if the timeline includes the specified
 274:          * domain value range.
 275:          *
 276:          * @param from  the start date.
 277:          * @param to  the end date.
 278:          *
 279:          * @return <code>true</code>.
 280:          */
 281:         public boolean containsDomainRange(Date from, Date to) {
 282:             return true;
 283:         }
 284: 
 285:         /**
 286:          * Tests an object for equality with this instance.
 287:          *
 288:          * @param object  the object.
 289:          *
 290:          * @return A boolean.
 291:          */
 292:         public boolean equals(Object object) {
 293:             if (object == null) {
 294:                 return false;
 295:             }
 296:             if (object == this) {
 297:                 return true;
 298:             }
 299:             if (object instanceof DefaultTimeline) {
 300:                 return true;
 301:             }
 302:             return false;
 303:         }
 304:     }
 305: 
 306:     /** A static default timeline shared by all standard DateAxis */
 307:     private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
 308: 
 309:     /** The time zone for the axis. */
 310:     private TimeZone timeZone;
 311: 
 312:     /** Our underlying timeline. */
 313:     private Timeline timeline;
 314: 
 315:     /**
 316:      * Creates a date axis with no label.
 317:      */
 318:     public DateAxis() {
 319:         this(null);
 320:     }
 321: 
 322:     /**
 323:      * Creates a date axis with the specified label.
 324:      *
 325:      * @param label  the axis label (<code>null</code> permitted).
 326:      */
 327:     public DateAxis(String label) {
 328:         this(label, TimeZone.getDefault());
 329:     }
 330: 
 331:     /**
 332:      * Creates a date axis. A timeline is specified for the axis. This allows
 333:      * special transformations to occur between a domain of values and the
 334:      * values included in the axis.
 335:      *
 336:      * @see org.jfree.chart.axis.SegmentedTimeline
 337:      *
 338:      * @param label  the axis label (<code>null</code> permitted).
 339:      * @param zone  the time zone.
 340:      */
 341:     public DateAxis(String label, TimeZone zone) {
 342:         super(label, DateAxis.createStandardDateTickUnits(zone));
 343:         setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
 344:         setAutoRangeMinimumSize(
 345:                 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
 346:         setRange(DEFAULT_DATE_RANGE, false, false);
 347:         this.dateFormatOverride = null;
 348:         this.timeZone = zone;
 349:         this.timeline = DEFAULT_TIMELINE;
 350:     }
 351: 
 352:     /**
 353:      * Returns the time zone for the axis.
 354:      *
 355:      * @return The time zone.
 356:      *
 357:      * @since 1.0.4
 358:      * @see #setTimeZone(TimeZone)
 359:      */
 360:     public TimeZone getTimeZone() {
 361:         return this.timeZone;
 362:     }
 363: 
 364:     /**
 365:      * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to
 366:      * all registered listeners.
 367:      *
 368:      * @param zone  the time zone (<code>null</code> not permitted).
 369:      *
 370:      * @since 1.0.4
 371:      * @see #getTimeZone()
 372:      */
 373:     public void setTimeZone(TimeZone zone) {
 374:         if (!this.timeZone.equals(zone)) {
 375:             this.timeZone = zone;
 376:             setStandardTickUnits(createStandardDateTickUnits(zone));
 377:             notifyListeners(new AxisChangeEvent(this));
 378:         }
 379:     }
 380: 
 381:     /**
 382:      * Returns the underlying timeline used by this axis.
 383:      *
 384:      * @return The timeline.
 385:      */
 386:     public Timeline getTimeline() {
 387:         return this.timeline;
 388:     }
 389: 
 390:     /**
 391:      * Sets the underlying timeline to use for this axis.
 392:      * <P>
 393:      * If the timeline is changed, an {@link AxisChangeEvent} is sent to all
 394:      * registered listeners.
 395:      *
 396:      * @param timeline  the timeline.
 397:      */
 398:     public void setTimeline(Timeline timeline) {
 399:         if (this.timeline != timeline) {
 400:             this.timeline = timeline;
 401:             notifyListeners(new AxisChangeEvent(this));
 402:         }
 403:     }
 404: 
 405:     /**
 406:      * Returns the tick unit for the axis.
 407:      * <p>
 408:      * Note: if the <code>autoTickUnitSelection</code> flag is
 409:      * <code>true</code> the tick unit may be changed while the axis is being
 410:      * drawn, so in that case the return value from this method may be
 411:      * irrelevant if the method is called before the axis has been drawn.
 412:      *
 413:      * @return The tick unit (possibly <code>null</code>).
 414:      *
 415:      * @see #setTickUnit(DateTickUnit)
 416:      * @see ValueAxis#isAutoTickUnitSelection()
 417:      */
 418:     public DateTickUnit getTickUnit() {
 419:         return this.tickUnit;
 420:     }
 421: 
 422:     /**
 423:      * Sets the tick unit for the axis.  The auto-tick-unit-selection flag is
 424:      * set to <code>false</code>, and registered listeners are notified that
 425:      * the axis has been changed.
 426:      *
 427:      * @param unit  the tick unit.
 428:      *
 429:      * @see #getTickUnit()
 430:      * @see #setTickUnit(DateTickUnit, boolean, boolean)
 431:      */
 432:     public void setTickUnit(DateTickUnit unit) {
 433:         setTickUnit(unit, true, true);
 434:     }
 435: 
 436:     /**
 437:      * Sets the tick unit attribute.
 438:      *
 439:      * @param unit  the new tick unit.
 440:      * @param notify  notify registered listeners?
 441:      * @param turnOffAutoSelection  turn off auto selection?
 442:      *
 443:      * @see #getTickUnit()
 444:      */
 445:     public void setTickUnit(DateTickUnit unit, boolean notify,
 446:                             boolean turnOffAutoSelection) {
 447: 
 448:         this.tickUnit = unit;
 449:         if (turnOffAutoSelection) {
 450:             setAutoTickUnitSelection(false, false);
 451:         }
 452:         if (notify) {
 453:             notifyListeners(new AxisChangeEvent(this));
 454:         }
 455: 
 456:     }
 457: 
 458:     /**
 459:      * Returns the date format override.  If this is non-null, then it will be
 460:      * used to format the dates on the axis.
 461:      *
 462:      * @return The formatter (possibly <code>null</code>).
 463:      */
 464:     public DateFormat getDateFormatOverride() {
 465:         return this.dateFormatOverride;
 466:     }
 467: 
 468:     /**
 469:      * Sets the date format override.  If this is non-null, then it will be
 470:      * used to format the dates on the axis.
 471:      *
 472:      * @param formatter  the date formatter (<code>null</code> permitted).
 473:      */
 474:     public void setDateFormatOverride(DateFormat formatter) {
 475:         this.dateFormatOverride = formatter;
 476:         notifyListeners(new AxisChangeEvent(this));
 477:     }
 478: 
 479:     /**
 480:      * Sets the upper and lower bounds for the axis and sends an
 481:      * {@link AxisChangeEvent} to all registered listeners.  As a side-effect,
 482:      * the auto-range flag is set to false.
 483:      *
 484:      * @param range  the new range (<code>null</code> not permitted).
 485:      */
 486:     public void setRange(Range range) {
 487:         setRange(range, true, true);
 488:     }
 489: 
 490:     /**
 491:      * Sets the range for the axis, if requested, sends an
 492:      * {@link AxisChangeEvent} to all registered listeners.  As a side-effect,
 493:      * the auto-range flag is set to <code>false</code> (optional).
 494:      *
 495:      * @param range  the range (<code>null</code> not permitted).
 496:      * @param turnOffAutoRange  a flag that controls whether or not the auto
 497:      *                          range is turned off.
 498:      * @param notify  a flag that controls whether or not listeners are
 499:      *                notified.
 500:      */
 501:     public void setRange(Range range, boolean turnOffAutoRange,
 502:                          boolean notify) {
 503:         if (range == null) {
 504:             throw new IllegalArgumentException("Null 'range' argument.");
 505:         }
 506:         // usually the range will be a DateRange, but if it isn't do a
 507:         // conversion...
 508:         if (!(range instanceof DateRange)) {
 509:             range = new DateRange(range);
 510:         }
 511:         super.setRange(range, turnOffAutoRange, notify);
 512:     }
 513: 
 514:     /**
 515:      * Sets the axis range and sends an {@link AxisChangeEvent} to all
 516:      * registered listeners.
 517:      *
 518:      * @param lower  the lower bound for the axis.
 519:      * @param upper  the upper bound for the axis.
 520:      */
 521:     public void setRange(Date lower, Date upper) {
 522:         if (lower.getTime() >= upper.getTime()) {
 523:             throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
 524:         }
 525:         setRange(new DateRange(lower, upper));
 526:     }
 527: 
 528:     /**
 529:      * Sets the axis range and sends an {@link AxisChangeEvent} to all
 530:      * registered listeners.
 531:      *
 532:      * @param lower  the lower bound for the axis.
 533:      * @param upper  the upper bound for the axis.
 534:      */
 535:     public void setRange(double lower, double upper) {
 536:         if (lower >= upper) {
 537:             throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
 538:         }
 539:         setRange(new DateRange(lower, upper));
 540:     }
 541: 
 542:     /**
 543:      * Returns the earliest date visible on the axis.
 544:      *
 545:      * @return The date.
 546:      *
 547:      * @see #setMinimumDate(Date)
 548:      * @see #getMaximumDate()
 549:      */
 550:     public Date getMinimumDate() {
 551:         Date result = null;
 552:         Range range = getRange();
 553:         if (range instanceof DateRange) {
 554:             DateRange r = (DateRange) range;
 555:             result = r.getLowerDate();
 556:         }
 557:         else {
 558:             result = new Date((long) range.getLowerBound());
 559:         }
 560:         return result;
 561:     }
 562: 
 563:     /**
 564:      * Sets the minimum date visible on the axis and sends an
 565:      * {@link AxisChangeEvent} to all registered listeners.  If
 566:      * <code>date</code> is on or after the current maximum date for
 567:      * the axis, the maximum date will be shifted to preserve the current
 568:      * length of the axis.
 569:      *
 570:      * @param date  the date (<code>null</code> not permitted).
 571:      *
 572:      * @see #getMinimumDate()
 573:      * @see #setMaximumDate(Date)
 574:      */
 575:     public void setMinimumDate(Date date) {
 576:         if (date == null) {
 577:             throw new IllegalArgumentException("Null 'date' argument.");
 578:         }
 579:         // check the new minimum date relative to the current maximum date
 580:         Date maxDate = getMaximumDate();
 581:         long maxMillis = maxDate.getTime();
 582:         long newMinMillis = date.getTime();
 583:         if (maxMillis <= newMinMillis) {
 584:             Date oldMin = getMinimumDate();
 585:             long length = maxMillis - oldMin.getTime();
 586:             maxDate = new Date(newMinMillis + length);
 587:         }
 588:         setRange(new DateRange(date, maxDate), true, false);
 589:         notifyListeners(new AxisChangeEvent(this));
 590:     }
 591: 
 592:     /**
 593:      * Returns the latest date visible on the axis.
 594:      *
 595:      * @return The date.
 596:      *
 597:      * @see #setMaximumDate(Date)
 598:      * @see #getMinimumDate()
 599:      */
 600:     public Date getMaximumDate() {
 601:         Date result = null;
 602:         Range range = getRange();
 603:         if (range instanceof DateRange) {
 604:             DateRange r = (DateRange) range;
 605:             result = r.getUpperDate();
 606:         }
 607:         else {
 608:             result = new Date((long) range.getUpperBound());
 609:         }
 610:         return result;
 611:     }
 612: 
 613:     /**
 614:      * Sets the maximum date visible on the axis and sends an
 615:      * {@link AxisChangeEvent} to all registered listeners.  If
 616:      * <code>maximumDate</code> is on or before the current minimum date for
 617:      * the axis, the minimum date will be shifted to preserve the current
 618:      * length of the axis.
 619:      *
 620:      * @param maximumDate  the date (<code>null</code> not permitted).
 621:      *
 622:      * @see #getMinimumDate()
 623:      * @see #setMinimumDate(Date)
 624:      */
 625:     public void setMaximumDate(Date maximumDate) {
 626:         if (maximumDate == null) {
 627:             throw new IllegalArgumentException("Null 'maximumDate' argument.");
 628:         }
 629:         // check the new maximum date relative to the current minimum date
 630:         Date minDate = getMinimumDate();
 631:         long minMillis = minDate.getTime();
 632:         long newMaxMillis = maximumDate.getTime();
 633:         if (minMillis >= newMaxMillis) {
 634:             Date oldMax = getMaximumDate();
 635:             long length = oldMax.getTime() - minMillis;
 636:             minDate = new Date(newMaxMillis - length);
 637:         }
 638:         setRange(new DateRange(minDate, maximumDate), true, false);
 639:         notifyListeners(new AxisChangeEvent(this));
 640:     }
 641: 
 642:     /**
 643:      * Returns the tick mark position (start, middle or end of the time period).
 644:      *
 645:      * @return The position (never <code>null</code>).
 646:      */
 647:     public DateTickMarkPosition getTickMarkPosition() {
 648:         return this.tickMarkPosition;
 649:     }
 650: 
 651:     /**
 652:      * Sets the tick mark position (start, middle or end of the time period)
 653:      * and sends an {@link AxisChangeEvent} to all registered listeners.
 654:      *
 655:      * @param position  the position (<code>null</code> not permitted).
 656:      */
 657:     public void setTickMarkPosition(DateTickMarkPosition position) {
 658:         if (position == null) {
 659:             throw new IllegalArgumentException("Null 'position' argument.");
 660:         }
 661:         this.tickMarkPosition = position;
 662:         notifyListeners(new AxisChangeEvent(this));
 663:     }
 664: 
 665:     /**
 666:      * Configures the axis to work with the specified plot.  If the axis has
 667:      * auto-scaling, then sets the maximum and minimum values.
 668:      */
 669:     public void configure() {
 670:         if (isAutoRange()) {
 671:             autoAdjustRange();
 672:         }
 673:     }
 674: 
 675:     /**
 676:      * Returns <code>true</code> if the axis hides this value, and
 677:      * <code>false</code> otherwise.
 678:      *
 679:      * @param millis  the data value.
 680:      *
 681:      * @return A value.
 682:      */
 683:     public boolean isHiddenValue(long millis) {
 684:         return (!this.timeline.containsDomainValue(new Date(millis)));
 685:     }
 686: 
 687:     /**
 688:      * Translates the data value to the display coordinates (Java 2D User Space)
 689:      * of the chart.
 690:      *
 691:      * @param value  the date to be plotted.
 692:      * @param area  the rectangle (in Java2D space) where the data is to be
 693:      *              plotted.
 694:      * @param edge  the axis location.
 695:      *
 696:      * @return The coordinate corresponding to the supplied data value.
 697:      */
 698:     public double valueToJava2D(double value, Rectangle2D area,
 699:                                 RectangleEdge edge) {
 700: 
 701:         value = this.timeline.toTimelineValue((long) value);
 702: 
 703:         DateRange range = (DateRange) getRange();
 704:         double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
 705:         double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
 706:         double result = 0.0;
 707:         if (RectangleEdge.isTopOrBottom(edge)) {
 708:             double minX = area.getX();
 709:             double maxX = area.getMaxX();
 710:             if (isInverted()) {
 711:                 result = maxX + ((value - axisMin) / (axisMax - axisMin))
 712:                          * (minX - maxX);
 713:             }
 714:             else {
 715:                 result = minX + ((value - axisMin) / (axisMax - axisMin))
 716:                          * (maxX - minX);
 717:             }
 718:         }
 719:         else if (RectangleEdge.isLeftOrRight(edge)) {
 720:             double minY = area.getMinY();
 721:             double maxY = area.getMaxY();
 722:             if (isInverted()) {
 723:                 result = minY + (((value - axisMin) / (axisMax - axisMin))
 724:                          * (maxY - minY));
 725:             }
 726:             else {
 727:                 result = maxY - (((value - axisMin) / (axisMax - axisMin))
 728:                          * (maxY - minY));
 729:             }
 730:         }
 731:         return result;
 732: 
 733:     }
 734: 
 735:     /**
 736:      * Translates a date to Java2D coordinates, based on the range displayed by
 737:      * this axis for the specified data area.
 738:      *
 739:      * @param date  the date.
 740:      * @param area  the rectangle (in Java2D space) where the data is to be
 741:      *              plotted.
 742:      * @param edge  the axis location.
 743:      *
 744:      * @return The coordinate corresponding to the supplied date.
 745:      */
 746:     public double dateToJava2D(Date date, Rectangle2D area,
 747:                                RectangleEdge edge) {
 748:         double value = date.getTime();
 749:         return valueToJava2D(value, area, edge);
 750:     }
 751: 
 752:     /**
 753:      * Translates a Java2D coordinate into the corresponding data value.  To
 754:      * perform this translation, you need to know the area used for plotting
 755:      * data, and which edge the axis is located on.
 756:      *
 757:      * @param java2DValue  the coordinate in Java2D space.
 758:      * @param area  the rectangle (in Java2D space) where the data is to be
 759:      *              plotted.
 760:      * @param edge  the axis location.
 761:      *
 762:      * @return A data value.
 763:      */
 764:     public double java2DToValue(double java2DValue, Rectangle2D area,
 765:                                 RectangleEdge edge) {
 766: 
 767:         DateRange range = (DateRange) getRange();
 768:         double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
 769:         double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
 770: 
 771:         double min = 0.0;
 772:         double max = 0.0;
 773:         if (RectangleEdge.isTopOrBottom(edge)) {
 774:             min = area.getX();
 775:             max = area.getMaxX();
 776:         }
 777:         else if (RectangleEdge.isLeftOrRight(edge)) {
 778:             min = area.getMaxY();
 779:             max = area.getY();
 780:         }
 781: 
 782:         double result;
 783:         if (isInverted()) {
 784:              result = axisMax - ((java2DValue - min) / (max - min)
 785:                       * (axisMax - axisMin));
 786:         }
 787:         else {
 788:              result = axisMin + ((java2DValue - min) / (max - min)
 789:                       * (axisMax - axisMin));
 790:         }
 791: 
 792:         return this.timeline.toMillisecond((long) result);
 793:     }
 794: 
 795:     /**
 796:      * Calculates the value of the lowest visible tick on the axis.
 797:      *
 798:      * @param unit  date unit to use.
 799:      *
 800:      * @return The value of the lowest visible tick on the axis.
 801:      */
 802:     public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
 803:         return nextStandardDate(getMinimumDate(), unit);
 804:     }
 805: 
 806:     /**
 807:      * Calculates the value of the highest visible tick on the axis.
 808:      *
 809:      * @param unit  date unit to use.
 810:      *
 811:      * @return The value of the highest visible tick on the axis.
 812:      */
 813:     public Date calculateHighestVisibleTickValue(DateTickUnit unit) {
 814:         return previousStandardDate(getMaximumDate(), unit);
 815:     }
 816: 
 817:     /**
 818:      * Returns the previous "standard" date, for a given date and tick unit.
 819:      *
 820:      * @param date  the reference date.
 821:      * @param unit  the tick unit.
 822:      *
 823:      * @return The previous "standard" date.
 824:      */
 825:     protected Date previousStandardDate(Date date, DateTickUnit unit) {
 826: 
 827:         int milliseconds;
 828:         int seconds;
 829:         int minutes;
 830:         int hours;
 831:         int days;
 832:         int months;
 833:         int years;
 834: 
 835:         Calendar calendar = Calendar.getInstance(this.timeZone);
 836:         calendar.setTime(date);
 837:         int count = unit.getCount();
 838:         int current = calendar.get(unit.getCalendarField());
 839:         int value = count * (current / count);
 840: 
 841:         switch (unit.getUnit()) {
 842: 
 843:             case (DateTickUnit.MILLISECOND) :
 844:                 years = calendar.get(Calendar.YEAR);
 845:                 months = calendar.get(Calendar.MONTH);
 846:                 days = calendar.get(Calendar.DATE);
 847:                 hours = calendar.get(Calendar.HOUR_OF_DAY);
 848:                 minutes = calendar.get(Calendar.MINUTE);
 849:                 seconds = calendar.get(Calendar.SECOND);
 850:                 calendar.set(years, months, days, hours, minutes, seconds);
 851:                 calendar.set(Calendar.MILLISECOND, value);
 852:                 Date mm = calendar.getTime();
 853:                 if (mm.getTime() >= date.getTime()) {
 854:                     calendar.set(Calendar.MILLISECOND, value - 1);
 855:                     mm = calendar.getTime();
 856:                 }
 857:                 return mm;
 858: 
 859:             case (DateTickUnit.SECOND) :
 860:                 years = calendar.get(Calendar.YEAR);
 861:                 months = calendar.get(Calendar.MONTH);
 862:                 days = calendar.get(Calendar.DATE);
 863:                 hours = calendar.get(Calendar.HOUR_OF_DAY);
 864:                 minutes = calendar.get(Calendar.MINUTE);
 865:                 if (this.tickMarkPosition == DateTickMarkPosition.START) {
 866:                     milliseconds = 0;
 867:                 }
 868:                 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
 869:                     milliseconds = 500;
 870:                 }
 871:                 else {
 872:                     milliseconds = 999;
 873:                 }
 874:                 calendar.set(Calendar.MILLISECOND, milliseconds);
 875:                 calendar.set(years, months, days, hours, minutes, value);
 876:                 Date dd = calendar.getTime();
 877:                 if (dd.getTime() >= date.getTime()) {
 878:                     calendar.set(Calendar.SECOND, value - 1);
 879:                     dd = calendar.getTime();
 880:                 }
 881:                 return dd;
 882: 
 883:             case (DateTickUnit.MINUTE) :
 884:                 years = calendar.get(Calendar.YEAR);
 885:                 months = calendar.get(Calendar.MONTH);
 886:                 days = calendar.get(Calendar.DATE);
 887:                 hours = calendar.get(Calendar.HOUR_OF_DAY);
 888:                 if (this.tickMarkPosition == DateTickMarkPosition.START) {
 889:                     seconds = 0;
 890:                 }
 891:                 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
 892:                     seconds = 30;
 893:                 }
 894:                 else {
 895:                     seconds = 59;
 896:                 }
 897:                 calendar.clear(Calendar.MILLISECOND);
 898:                 calendar.set(years, months, days, hours, value, seconds);
 899:                 Date d0 = calendar.getTime();
 900:                 if (d0.getTime() >= date.getTime()) {
 901:                     calendar.set(Calendar.MINUTE, value - 1);
 902:                     d0 = calendar.getTime();
 903:                 }
 904:                 return d0;
 905: 
 906:             case (DateTickUnit.HOUR) :
 907:                 years = calendar.get(Calendar.YEAR);
 908:                 months = calendar.get(Calendar.MONTH);
 909:                 days = calendar.get(Calendar.DATE);
 910:                 if (this.tickMarkPosition == DateTickMarkPosition.START) {
 911:                     minutes = 0;
 912:                     seconds = 0;
 913:                 }
 914:                 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
 915:                     minutes = 30;
 916:                     seconds = 0;
 917:                 }
 918:                 else {
 919:                     minutes = 59;
 920:                     seconds = 59;
 921:                 }
 922:                 calendar.clear(Calendar.MILLISECOND);
 923:                 calendar.set(years, months, days, value, minutes, seconds);
 924:                 Date d1 = calendar.getTime();
 925:                 if (d1.getTime() >= date.getTime()) {
 926:                     calendar.set(Calendar.HOUR_OF_DAY, value - 1);
 927:                     d1 = calendar.getTime();
 928:                 }
 929:                 return d1;
 930: 
 931:             case (DateTickUnit.DAY) :
 932:                 years = calendar.get(Calendar.YEAR);
 933:                 months = calendar.get(Calendar.MONTH);
 934:                 if (this.tickMarkPosition == DateTickMarkPosition.START) {
 935:                     hours = 0;
 936:                     minutes = 0;
 937:                     seconds = 0;
 938:                 }
 939:                 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
 940:                     hours = 12;
 941:                     minutes = 0;
 942:                     seconds = 0;
 943:                 }
 944:                 else {
 945:                     hours = 23;
 946:                     minutes = 59;
 947:                     seconds = 59;
 948:                 }
 949:                 calendar.clear(Calendar.MILLISECOND);
 950:                 calendar.set(years, months, value, hours, 0, 0);
 951:                 // long result = calendar.getTimeInMillis();
 952:                     // won't work with JDK 1.3
 953:                 Date d2 = calendar.getTime();
 954:                 if (d2.getTime() >= date.getTime()) {
 955:                     calendar.set(Calendar.DATE, value - 1);
 956:                     d2 = calendar.getTime();
 957:                 }
 958:                 return d2;
 959: 
 960:             case (DateTickUnit.MONTH) :
 961:                 years = calendar.get(Calendar.YEAR);
 962:                 calendar.clear(Calendar.MILLISECOND);
 963:                 calendar.set(years, value, 1, 0, 0, 0);
 964:                 Month month = new Month(calendar.getTime(), this.timeZone);
 965:                 Date standardDate = calculateDateForPosition(
 966:                         month, this.tickMarkPosition);
 967:                 long millis = standardDate.getTime();
 968:                 if (millis >= date.getTime()) {
 969:                     month = (Month) month.previous();
 970:                     standardDate = calculateDateForPosition(
 971:                             month, this.tickMarkPosition);
 972:                 }
 973:                 return standardDate;
 974: 
 975:             case(DateTickUnit.YEAR) :
 976:                 if (this.tickMarkPosition == DateTickMarkPosition.START) {
 977:                     months = 0;
 978:                     days = 1;
 979:                 }
 980:                 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
 981:                     months = 6;
 982:                     days = 1;
 983:                 }
 984:                 else {
 985:                     months = 11;
 986:                     days = 31;
 987:                 }
 988:                 calendar.clear(Calendar.MILLISECOND);
 989:                 calendar.set(value, months, days, 0, 0, 0);
 990:                 Date d3 = calendar.getTime();
 991:                 if (d3.getTime() >= date.getTime()) {
 992:                     calendar.set(Calendar.YEAR, value - 1);
 993:                     d3 = calendar.getTime();
 994:                 }
 995:                 return d3;
 996: 
 997:             default: return null;
 998: 
 999:         }
1000: 
1001:     }
1002: 
1003:     /**
1004:      * Returns a {@link java.util.Date} corresponding to the specified position
1005:      * within a {@link RegularTimePeriod}.
1006:      *
1007:      * @param period  the period.
1008:      * @param position  the position (<code>null</code> not permitted).
1009:      *
1010:      * @return A date.
1011:      */
1012:     private Date calculateDateForPosition(RegularTimePeriod period,
1013:                                           DateTickMarkPosition position) {
1014: 
1015:         if (position == null) {
1016:             throw new IllegalArgumentException("Null 'position' argument.");
1017:         }
1018:         Date result = null;
1019:         if (position == DateTickMarkPosition.START) {
1020:             result = new Date(period.getFirstMillisecond());
1021:         }
1022:         else if (position == DateTickMarkPosition.MIDDLE) {
1023:             result = new Date(period.getMiddleMillisecond());
1024:         }
1025:         else if (position == DateTickMarkPosition.END) {
1026:             result = new Date(period.getLastMillisecond());
1027:         }
1028:         return result;
1029: 
1030:     }
1031: 
1032:     /**
1033:      * Returns the first "standard" date (based on the specified field and
1034:      * units).
1035:      *
1036:      * @param date  the reference date.
1037:      * @param unit  the date tick unit.
1038:      *
1039:      * @return The next "standard" date.
1040:      */
1041:     protected Date nextStandardDate(Date date, DateTickUnit unit) {
1042:         Date previous = previousStandardDate(date, unit);
1043:         Calendar calendar = Calendar.getInstance(this.timeZone);
1044:         calendar.setTime(previous);
1045:         calendar.add(unit.getCalendarField(), unit.getCount());
1046:         return calendar.getTime();
1047:     }
1048: 
1049:     /**
1050:      * Returns a collection of standard date tick units that uses the default
1051:      * time zone.  This collection will be used by default, but you are free
1052:      * to create your own collection if you want to (see the
1053:      * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1054:      * from the {@link ValueAxis} class).
1055:      *
1056:      * @return A collection of standard date tick units.
1057:      */
1058:     public static TickUnitSource createStandardDateTickUnits() {
1059:         return createStandardDateTickUnits(TimeZone.getDefault());
1060:     }
1061: 
1062:     /**
1063:      * Returns a collection of standard date tick units.  This collection will
1064:      * be used by default, but you are free to create your own collection if
1065:      * you want to (see the
1066:      * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1067:      * from the {@link ValueAxis} class).
1068:      *
1069:      * @param zone  the time zone (<code>null</code> not permitted).
1070:      *
1071:      * @return A collection of standard date tick units.
1072:      */
1073:     public static TickUnitSource createStandardDateTickUnits(TimeZone zone) {
1074: 
1075:         if (zone == null) {
1076:             throw new IllegalArgumentException("Null 'zone' argument.");
1077:         }
1078:         TickUnits units = new TickUnits();
1079: 
1080:         // date formatters
1081:         DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
1082:         DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
1083:         DateFormat f3 = new SimpleDateFormat("HH:mm");
1084:         DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
1085:         DateFormat f5 = new SimpleDateFormat("d-MMM");
1086:         DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
1087:         DateFormat f7 = new SimpleDateFormat("yyyy");
1088: 
1089:         f1.setTimeZone(zone);
1090:         f2.setTimeZone(zone);
1091:         f3.setTimeZone(zone);
1092:         f4.setTimeZone(zone);
1093:         f5.setTimeZone(zone);
1094:         f6.setTimeZone(zone);
1095:         f7.setTimeZone(zone);
1096: 
1097:         // milliseconds
1098:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
1099:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5,
1100:                 DateTickUnit.MILLISECOND, 1, f1));
1101:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10,
1102:                 DateTickUnit.MILLISECOND, 1, f1));
1103:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25,
1104:                 DateTickUnit.MILLISECOND, 5, f1));
1105:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50,
1106:                 DateTickUnit.MILLISECOND, 10, f1));
1107:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100,
1108:                 DateTickUnit.MILLISECOND, 10, f1));
1109:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250,
1110:                 DateTickUnit.MILLISECOND, 10, f1));
1111:         units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500,
1112:                 DateTickUnit.MILLISECOND, 50, f1));
1113: 
1114:         // seconds
1115:         units.add(new DateTickUnit(DateTickUnit.SECOND, 1,
1116:                 DateTickUnit.MILLISECOND, 50, f2));
1117:         units.add(new DateTickUnit(DateTickUnit.SECOND, 5,
1118:                 DateTickUnit.SECOND, 1, f2));
1119:         units.add(new DateTickUnit(DateTickUnit.SECOND, 10,
1120:                 DateTickUnit.SECOND, 1, f2));
1121:         units.add(new DateTickUnit(DateTickUnit.SECOND, 30,
1122:                 DateTickUnit.SECOND, 5, f2));
1123: 
1124:         // minutes
1125:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 1,
1126:                 DateTickUnit.SECOND, 5, f3));
1127:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 2,
1128:                 DateTickUnit.SECOND, 10, f3));
1129:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 5,
1130:                 DateTickUnit.MINUTE, 1, f3));
1131:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 10,
1132:                 DateTickUnit.MINUTE, 1, f3));
1133:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 15,
1134:                 DateTickUnit.MINUTE, 5, f3));
1135:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 20,
1136:                 DateTickUnit.MINUTE, 5, f3));
1137:         units.add(new DateTickUnit(DateTickUnit.MINUTE, 30,
1138:                 DateTickUnit.MINUTE, 5, f3));
1139: 
1140:         // hours
1141:         units.add(new DateTickUnit(DateTickUnit.HOUR, 1,
1142:                 DateTickUnit.MINUTE, 5, f3));
1143:         units.add(new DateTickUnit(DateTickUnit.HOUR, 2,
1144:                 DateTickUnit.MINUTE, 10, f3));
1145:         units.add(new DateTickUnit(DateTickUnit.HOUR, 4,
1146:                 DateTickUnit.MINUTE, 30, f3));
1147:         units.add(new DateTickUnit(DateTickUnit.HOUR, 6,
1148:                 DateTickUnit.HOUR, 1, f3));
1149:         units.add(new DateTickUnit(DateTickUnit.HOUR, 12,
1150:                 DateTickUnit.HOUR, 1, f4));
1151: 
1152:         // days
1153:         units.add(new DateTickUnit(DateTickUnit.DAY, 1,
1154:                 DateTickUnit.HOUR, 1, f5));
1155:         units.add(new DateTickUnit(DateTickUnit.DAY, 2,
1156:                 DateTickUnit.HOUR, 1, f5));
1157:         units.add(new DateTickUnit(DateTickUnit.DAY, 7,
1158:                 DateTickUnit.DAY, 1, f5));
1159:         units.add(new DateTickUnit(DateTickUnit.DAY, 15,
1160:                 DateTickUnit.DAY, 1, f5));
1161: 
1162:         // months
1163:         units.add(new DateTickUnit(DateTickUnit.MONTH, 1,
1164:                 DateTickUnit.DAY, 1, f6));
1165:         units.add(new DateTickUnit(DateTickUnit.MONTH, 2,
1166:                 DateTickUnit.DAY, 1, f6));
1167:         units.add(new DateTickUnit(DateTickUnit.MONTH, 3,
1168:                 DateTickUnit.MONTH, 1, f6));
1169:         units.add(new DateTickUnit(DateTickUnit.MONTH, 4,
1170:                 DateTickUnit.MONTH, 1, f6));
1171:         units.add(new DateTickUnit(DateTickUnit.MONTH, 6,
1172:                 DateTickUnit.MONTH, 1, f6));
1173: 
1174:         // years
1175:         units.add(new DateTickUnit(DateTickUnit.YEAR, 1,
1176:                 DateTickUnit.MONTH, 1, f7));
1177:         units.add(new DateTickUnit(DateTickUnit.YEAR, 2,
1178:                 DateTickUnit.MONTH, 3, f7));
1179:         units.add(new DateTickUnit(DateTickUnit.YEAR, 5,
1180:                 DateTickUnit.YEAR, 1, f7));
1181:         units.add(new DateTickUnit(DateTickUnit.YEAR, 10,
1182:                 DateTickUnit.YEAR, 1, f7));
1183:         units.add(new DateTickUnit(DateTickUnit.YEAR, 25,
1184:                 DateTickUnit.YEAR, 5, f7));
1185:         units.add(new DateTickUnit(DateTickUnit.YEAR, 50,
1186:                 DateTickUnit.YEAR, 10, f7));
1187:         units.add(new DateTickUnit(DateTickUnit.YEAR, 100,
1188:                 DateTickUnit.YEAR, 20, f7));
1189: 
1190:         return units;
1191: 
1192:     }
1193: 
1194:     /**
1195:      * Rescales the axis to ensure that all data is visible.
1196:      */
1197:     protected void autoAdjustRange() {
1198: 
1199:         Plot plot = getPlot();
1200: 
1201:         if (plot == null) {
1202:             return;  // no plot, no data
1203:         }
1204: 
1205:         if (plot instanceof ValueAxisPlot) {
1206:             ValueAxisPlot vap = (ValueAxisPlot) plot;
1207: 
1208:             Range r = vap.getDataRange(this);
1209:             if (r == null) {
1210:                 if (this.timeline instanceof SegmentedTimeline) {
1211:                     //Timeline hasn't method getStartTime()
1212:                     r = new DateRange((
1213:                             (SegmentedTimeline) this.timeline).getStartTime(),
1214:                             ((SegmentedTimeline) this.timeline).getStartTime()
1215:                             + 1);
1216:                 }
1217:                 else {
1218:                     r = new DateRange();
1219:                 }
1220:             }
1221: 
1222:             long upper = this.timeline.toTimelineValue(
1223:                     (long) r.getUpperBound());
1224:             long lower;
1225:             long fixedAutoRange = (long) getFixedAutoRange();
1226:             if (fixedAutoRange > 0.0) {
1227:                 lower = upper - fixedAutoRange;
1228:             }
1229:             else {
1230:                 lower = this.timeline.toTimelineValue((long) r.getLowerBound());
1231:                 double range = upper - lower;
1232:                 long minRange = (long) getAutoRangeMinimumSize();
1233:                 if (range < minRange) {
1234:                     long expand = (long) (minRange - range) / 2;
1235:                     upper = upper + expand;
1236:                     lower = lower - expand;
1237:                 }
1238:                 upper = upper + (long) (range * getUpperMargin());
1239:                 lower = lower - (long) (range * getLowerMargin());
1240:             }
1241: 
1242:             upper = this.timeline.toMillisecond(upper);
1243:             lower = this.timeline.toMillisecond(lower);
1244:             DateRange dr = new DateRange(new Date(lower), new Date(upper));
1245:             setRange(dr, false, false);
1246:         }
1247: 
1248:     }
1249: 
1250:     /**
1251:      * Selects an appropriate tick value for the axis.  The strategy is to
1252:      * display as many ticks as possible (selected from an array of 'standard'
1253:      * tick units) without the labels overlapping.
1254:      *
1255:      * @param g2  the graphics device.
1256:      * @param dataArea  the area defined by the axes.
1257:      * @param edge  the axis location.
1258:      */
1259:     protected void selectAutoTickUnit(Graphics2D g2,
1260:                                       Rectangle2D dataArea,
1261:                                       RectangleEdge edge) {
1262: 
1263:         if (RectangleEdge.isTopOrBottom(edge)) {
1264:             selectHorizontalAutoTickUnit(g2, dataArea, edge);
1265:         }
1266:         else if (RectangleEdge.isLeftOrRight(edge)) {
1267:             selectVerticalAutoTickUnit(g2, dataArea, edge);
1268:         }
1269: 
1270:     }
1271: 
1272:     /**
1273:      * Selects an appropriate tick size for the axis.  The strategy is to
1274:      * display as many ticks as possible (selected from a collection of
1275:      * 'standard' tick units) without the labels overlapping.
1276:      *
1277:      * @param g2  the graphics device.
1278:      * @param dataArea  the area defined by the axes.
1279:      * @param edge  the axis location.
1280:      */
1281:     protected void selectHorizontalAutoTickUnit(Graphics2D g2,
1282:                                                 Rectangle2D dataArea,
1283:                                                 RectangleEdge edge) {
1284: 
1285:         long shift = 0;
1286:         if (this.timeline instanceof SegmentedTimeline) {
1287:             shift = ((SegmentedTimeline) this.timeline).getStartTime();
1288:         }
1289:         double zero = valueToJava2D(shift + 0.0, dataArea, edge);
1290:         double tickLabelWidth
1291:             = estimateMaximumTickLabelWidth(g2, getTickUnit());
1292: 
1293:         // start with the current tick unit...
1294:         TickUnitSource tickUnits = getStandardTickUnits();
1295:         TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
1296:         double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge);
1297:         double unit1Width = Math.abs(x1 - zero);
1298: 
1299:         // then extrapolate...
1300:         double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
1301:         DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess);
1302:         double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge);
1303:         double unit2Width = Math.abs(x2 - zero);
1304:         tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
1305:         if (tickLabelWidth > unit2Width) {
1306:             unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
1307:         }
1308:         setTickUnit(unit2, false, false);
1309:     }
1310: 
1311:     /**
1312:      * Selects an appropriate tick size for the axis.  The strategy is to
1313:      * display as many ticks as possible (selected from a collection of
1314:      * 'standard' tick units) without the labels overlapping.
1315:      *
1316:      * @param g2  the graphics device.
1317:      * @param dataArea  the area in which the plot should be drawn.
1318:      * @param edge  the axis location.
1319:      */
1320:     protected void selectVerticalAutoTickUnit(Graphics2D g2,
1321:                                               Rectangle2D dataArea,
1322:                                               RectangleEdge edge) {
1323: 
1324:         // start with the current tick unit...
1325:         TickUnitSource tickUnits = getStandardTickUnits();
1326:         double zero = valueToJava2D(0.0, dataArea, edge);
1327: 
1328:         // start with a unit that is at least 1/10th of the axis length
1329:         double estimate1 = getRange().getLength() / 10.0;
1330:         DateTickUnit candidate1
1331:             = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1);
1332:         double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1);
1333:         double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge);
1334:         double candidate1UnitHeight = Math.abs(y1 - zero);
1335: 
1336:         // now extrapolate based on label height and unit height...
1337:         double estimate2
1338:             = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize();
1339:         DateTickUnit candidate2
1340:             = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2);
1341:         double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2);
1342:         double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge);
1343:         double unit2Height = Math.abs(y2 - zero);
1344: 
1345:        // make final selection...
1346:        DateTickUnit finalUnit;
1347:        if (labelHeight2 < unit2Height) {
1348:            finalUnit = candidate2;
1349:        }
1350:        else {
1351:            finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2);
1352:        }
1353:        setTickUnit(finalUnit, false, false);
1354: 
1355:     }
1356: 
1357:     /**
1358:      * Estimates the maximum width of the tick labels, assuming the specified
1359:      * tick unit is used.
1360:      * <P>
1361:      * Rather than computing the string bounds of every tick on the axis, we
1362:      * just look at two values: the lower bound and the upper bound for the
1363:      * axis.  These two values will usually be representative.
1364:      *
1365:      * @param g2  the graphics device.
1366:      * @param unit  the tick unit to use for calculation.
1367:      *
1368:      * @return The estimated maximum width of the tick labels.
1369:      */
1370:     private double estimateMaximumTickLabelWidth(Graphics2D g2,
1371:                                                  DateTickUnit unit) {
1372: 
1373:         RectangleInsets tickLabelInsets = getTickLabelInsets();
1374:         double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
1375: 
1376:         Font tickLabelFont = getTickLabelFont();
1377:         FontRenderContext frc = g2.getFontRenderContext();
1378:         LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1379:         if (isVerticalTickLabels()) {
1380:             // all tick labels have the same width (equal to the height of
1381:             // the font)...
1382:             result += lm.getHeight();
1383:         }
1384:         else {
1385:             // look at lower and upper bounds...
1386:             DateRange range = (DateRange) getRange();
1387:             Date lower = range.getLowerDate();
1388:             Date upper = range.getUpperDate();
1389:             String lowerStr = null;
1390:             String upperStr = null;
1391:             DateFormat formatter = getDateFormatOverride();
1392:             if (formatter != null) {
1393:                 lowerStr = formatter.format(lower);
1394:                 upperStr = formatter.format(upper);
1395:             }
1396:             else {
1397:                 lowerStr = unit.dateToString(lower);
1398:                 upperStr = unit.dateToString(upper);
1399:             }
1400:             FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1401:             double w1 = fm.stringWidth(lowerStr);
1402:             double w2 = fm.stringWidth(upperStr);
1403:             result += Math.max(w1, w2);
1404:         }
1405: 
1406:         return result;
1407: 
1408:     }
1409: 
1410:     /**
1411:      * Estimates the maximum width of the tick labels, assuming the specified
1412:      * tick unit is used.
1413:      * <P>
1414:      * Rather than computing the string bounds of every tick on the axis, we
1415:      * just look at two values: the lower bound and the upper bound for the
1416:      * axis.  These two values will usually be representative.
1417:      *
1418:      * @param g2  the graphics device.
1419:      * @param unit  the tick unit to use for calculation.
1420:      *
1421:      * @return The estimated maximum width of the tick labels.
1422:      */
1423:     private double estimateMaximumTickLabelHeight(Graphics2D g2,
1424:                                                   DateTickUnit unit) {
1425: 
1426:         RectangleInsets tickLabelInsets = getTickLabelInsets();
1427:         double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom();
1428: 
1429:         Font tickLabelFont = getTickLabelFont();
1430:         FontRenderContext frc = g2.getFontRenderContext();
1431:         LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1432:         if (!isVerticalTickLabels()) {
1433:             // all tick labels have the same width (equal to the height of
1434:             // the font)...
1435:             result += lm.getHeight();
1436:         }
1437:         else {
1438:             // look at lower and upper bounds...
1439:             DateRange range = (DateRange) getRange();
1440:             Date lower = range.getLowerDate();
1441:             Date upper = range.getUpperDate();
1442:             String lowerStr = null;
1443:             String upperStr = null;
1444:             DateFormat formatter = getDateFormatOverride();
1445:             if (formatter != null) {
1446:                 lowerStr = formatter.format(lower);
1447:                 upperStr = formatter.format(upper);
1448:             }
1449:             else {
1450:                 lowerStr = unit.dateToString(lower);
1451:                 upperStr = unit.dateToString(upper);
1452:             }
1453:             FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1454:             double w1 = fm.stringWidth(lowerStr);
1455:             double w2 = fm.stringWidth(upperStr);
1456:             result += Math.max(w1, w2);
1457:         }
1458: 
1459:         return result;
1460: 
1461:     }
1462: 
1463:     /**
1464:      * Calculates the positions of the tick labels for the axis, storing the
1465:      * results in the tick label list (ready for drawing).
1466:      *
1467:      * @param g2  the graphics device.
1468:      * @param state  the axis state.
1469:      * @param dataArea  the area in which the plot should be drawn.
1470:      * @param edge  the location of the axis.
1471:      *
1472:      * @return A list of ticks.
1473:      */
1474:     public List refreshTicks(Graphics2D g2,
1475:                              AxisState state,
1476:                              Rectangle2D dataArea,
1477:                              RectangleEdge edge) {
1478: 
1479:         List result = null;
1480:         if (RectangleEdge.isTopOrBottom(edge)) {
1481:             result = refreshTicksHorizontal(g2, dataArea, edge);
1482:         }
1483:         else if (RectangleEdge.isLeftOrRight(edge)) {
1484:             result = refreshTicksVertical(g2, dataArea, edge);
1485:         }
1486:         return result;
1487: 
1488:     }
1489: 
1490:     /**
1491:      * Recalculates the ticks for the date axis.
1492:      *
1493:      * @param g2  the graphics device.
1494:      * @param dataArea  the area in which the data is to be drawn.
1495:      * @param edge  the location of the axis.
1496:      *
1497:      * @return A list of ticks.
1498:      */
1499:     protected List refreshTicksHorizontal(Graphics2D g2,
1500:                                           Rectangle2D dataArea,
1501:                                           RectangleEdge edge) {
1502: 
1503:         List result = new java.util.ArrayList();
1504: 
1505:         Font tickLabelFont = getTickLabelFont();
1506:         g2.setFont(tickLabelFont);
1507: 
1508:         if (isAutoTickUnitSelection()) {
1509:             selectAutoTickUnit(g2, dataArea, edge);
1510:         }
1511: 
1512:         DateTickUnit unit = getTickUnit();
1513:         Date tickDate = calculateLowestVisibleTickValue(unit);
1514:         Date upperDate = getMaximumDate();
1515: 
1516:         while (tickDate.before(upperDate)) {
1517: 
1518:             if (!isHiddenValue(tickDate.getTime())) {
1519:                 // work out the value, label and position
1520:                 String tickLabel;
1521:                 DateFormat formatter = getDateFormatOverride();
1522:                 if (formatter != null) {
1523:                     tickLabel = formatter.format(tickDate);
1524:                 }
1525:                 else {
1526:                     tickLabel = this.tickUnit.dateToString(tickDate);
1527:                 }
1528:                 TextAnchor anchor = null;
1529:                 TextAnchor rotationAnchor = null;
1530:                 double angle = 0.0;
1531:                 if (isVerticalTickLabels()) {
1532:                     anchor = TextAnchor.CENTER_RIGHT;
1533:                     rotationAnchor = TextAnchor.CENTER_RIGHT;
1534:                     if (edge == RectangleEdge.TOP) {
1535:                         angle = Math.PI / 2.0;
1536:                     }
1537:                     else {
1538:                         angle = -Math.PI / 2.0;
1539:                     }
1540:                 }
1541:                 else {
1542:                     if (edge == RectangleEdge.TOP) {
1543:                         anchor = TextAnchor.BOTTOM_CENTER;
1544:                         rotationAnchor = TextAnchor.BOTTOM_CENTER;
1545:                     }
1546:                     else {
1547:                         anchor = TextAnchor.TOP_CENTER;
1548:                         rotationAnchor = TextAnchor.TOP_CENTER;
1549:                     }
1550:                 }
1551: 
1552:                 Tick tick = new DateTick(tickDate, tickLabel, anchor,
1553:                         rotationAnchor, angle);
1554:                 result.add(tick);
1555:                 tickDate = unit.addToDate(tickDate, this.timeZone);
1556:             }
1557:             else {
1558:                 tickDate = unit.rollDate(tickDate, this.timeZone);
1559:                 continue;
1560:             }
1561: 
1562:             // could add a flag to make the following correction optional...
1563:             switch (unit.getUnit()) {
1564: 
1565:                 case (DateTickUnit.MILLISECOND) :
1566:                 case (DateTickUnit.SECOND) :
1567:                 case (DateTickUnit.MINUTE) :
1568:                 case (DateTickUnit.HOUR) :
1569:                 case (DateTickUnit.DAY) :
1570:                     break;
1571:                 case (DateTickUnit.MONTH) :
1572:                     tickDate = calculateDateForPosition(new Month(tickDate,
1573:                             this.timeZone), this.tickMarkPosition);
1574:                     break;
1575:                 case(DateTickUnit.YEAR) :
1576:                     tickDate = calculateDateForPosition(new Year(tickDate,
1577:                             this.timeZone), this.tickMarkPosition);
1578:                     break;
1579: 
1580:                 default: break;
1581: 
1582:             }
1583: 
1584:         }
1585:         return result;
1586: 
1587:     }
1588: 
1589:     /**
1590:      * Recalculates the ticks for the date axis.
1591:      *
1592:      * @param g2  the graphics device.
1593:      * @param dataArea  the area in which the plot should be drawn.
1594:      * @param edge  the location of the axis.
1595:      *
1596:      * @return A list of ticks.
1597:      */
1598:     protected List refreshTicksVertical(Graphics2D g2,
1599:                                         Rectangle2D dataArea,
1600:                                         RectangleEdge edge) {
1601: 
1602:         List result = new java.util.ArrayList();
1603: 
1604:         Font tickLabelFont = getTickLabelFont();
1605:         g2.setFont(tickLabelFont);
1606: 
1607:         if (isAutoTickUnitSelection()) {
1608:             selectAutoTickUnit(g2, dataArea, edge);
1609:         }
1610:         DateTickUnit unit = getTickUnit();
1611:         Date tickDate = calculateLowestVisibleTickValue(unit);
1612:         //Date upperDate = calculateHighestVisibleTickValue(unit);
1613:         Date upperDate = getMaximumDate();
1614:         while (tickDate.before(upperDate)) {
1615: 
1616:             if (!isHiddenValue(tickDate.getTime())) {
1617:                 // work out the value, label and position
1618:                 String tickLabel;
1619:                 DateFormat formatter = getDateFormatOverride();
1620:                 if (formatter != null) {
1621:                     tickLabel = formatter.format(tickDate);
1622:                 }
1623:                 else {
1624:                     tickLabel = this.tickUnit.dateToString(tickDate);
1625:                 }
1626:                 TextAnchor anchor = null;
1627:                 TextAnchor rotationAnchor = null;
1628:                 double angle = 0.0;
1629:                 if (isVerticalTickLabels()) {
1630:                     anchor = TextAnchor.BOTTOM_CENTER;
1631:                     rotationAnchor = TextAnchor.BOTTOM_CENTER;
1632:                     if (edge == RectangleEdge.LEFT) {
1633:                         angle = -Math.PI / 2.0;
1634:                     }
1635:                     else {
1636:                         angle = Math.PI / 2.0;
1637:                     }
1638:                 }
1639:                 else {
1640:                     if (edge == RectangleEdge.LEFT) {
1641:                         anchor = TextAnchor.CENTER_RIGHT;
1642:                         rotationAnchor = TextAnchor.CENTER_RIGHT;
1643:                     }
1644:                     else {
1645:                         anchor = TextAnchor.CENTER_LEFT;
1646:                         rotationAnchor = TextAnchor.CENTER_LEFT;
1647:                     }
1648:                 }
1649: 
1650:                 Tick tick = new DateTick(tickDate, tickLabel, anchor,
1651:                         rotationAnchor, angle);
1652:                 result.add(tick);
1653:                 tickDate = unit.addToDate(tickDate, this.timeZone);
1654:             }
1655:             else {
1656:                 tickDate = unit.rollDate(tickDate, this.timeZone);
1657:             }
1658:         }
1659:         return result;
1660:     }
1661: 
1662:     /**
1663:      * Draws the axis on a Java 2D graphics device (such as the screen or a
1664:      * printer).
1665:      *
1666:      * @param g2  the graphics device (<code>null</code> not permitted).
1667:      * @param cursor  the cursor location.
1668:      * @param plotArea  the area within which the axes and data should be
1669:      *                  drawn (<code>null</code> not permitted).
1670:      * @param dataArea  the area within which the data should be drawn
1671:      *                  (<code>null</code> not permitted).
1672:      * @param edge  the location of the axis (<code>null</code> not permitted).
1673:      * @param plotState  collects information about the plot
1674:      *                   (<code>null</code> permitted).
1675:      *
1676:      * @return The axis state (never <code>null</code>).
1677:      */
1678:     public AxisState draw(Graphics2D g2,
1679:                           double cursor,
1680:                           Rectangle2D plotArea,
1681:                           Rectangle2D dataArea,
1682:                           RectangleEdge edge,
1683:                           PlotRenderingInfo plotState) {
1684: 
1685:         // if the axis is not visible, don't draw it...
1686:         if (!isVisible()) {
1687:             AxisState state = new AxisState(cursor);
1688:             // even though the axis is not visible, we need to refresh ticks in
1689:             // case the grid is being drawn...
1690:             List ticks = refreshTicks(g2, state, dataArea, edge);
1691:             state.setTicks(ticks);
1692:             return state;
1693:         }
1694: 
1695:         // draw the tick marks and labels...
1696:         AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea,
1697:                 dataArea, edge);
1698: 
1699:         // draw the axis label (note that 'state' is passed in *and*
1700:         // returned)...
1701:         state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
1702: 
1703:         return state;
1704: 
1705:     }
1706: 
1707:     /**
1708:      * Zooms in on the current range.
1709:      *
1710:      * @param lowerPercent  the new lower bound.
1711:      * @param upperPercent  the new upper bound.
1712:      */
1713:     public void zoomRange(double lowerPercent, double upperPercent) {
1714:         double start = this.timeline.toTimelineValue(
1715:             (long) getRange().getLowerBound()
1716:         );
1717:         double length = (this.timeline.toTimelineValue(
1718:                 (long) getRange().getUpperBound())
1719:                 - this.timeline.toTimelineValue(
1720:                     (long) getRange().getLowerBound()));
1721:         Range adjusted = null;
1722:         if (isInverted()) {
1723:             adjusted = new DateRange(this.timeline.toMillisecond((long) (start
1724:                     + (length * (1 - upperPercent)))),
1725:                     this.timeline.toMillisecond((long) (start + (length
1726:                     * (1 - lowerPercent)))));
1727:         }
1728:         else {
1729:             adjusted = new DateRange(this.timeline.toMillisecond(
1730:                     (long) (start + length * lowerPercent)),
1731:                     this.timeline.toMillisecond((long) (start + length
1732:                     * upperPercent)));
1733:         }
1734:         setRange(adjusted);
1735:     }
1736: 
1737:     /**
1738:      * Tests this axis for equality with an arbitrary object.
1739:      *
1740:      * @param obj  the object (<code>null</code> permitted).
1741:      *
1742:      * @return A boolean.
1743:      */
1744:     public boolean equals(Object obj) {
1745:         if (obj == this) {
1746:             return true;
1747:         }
1748:         if (!(obj instanceof DateAxis)) {
1749:             return false;
1750:         }
1751:         DateAxis that = (DateAxis) obj;
1752:         if (!ObjectUtilities.equal(this.tickUnit, that.tickUnit)) {
1753:             return false;
1754:         }
1755:         if (!ObjectUtilities.equal(this.dateFormatOverride,
1756:                 that.dateFormatOverride)) {
1757:             return false;
1758:         }
1759:         if (!ObjectUtilities.equal(this.tickMarkPosition,
1760:                 that.tickMarkPosition)) {
1761:             return false;
1762:         }
1763:         if (!ObjectUtilities.equal(this.timeline, that.timeline)) {
1764:             return false;
1765:         }
1766:         if (!super.equals(obj)) {
1767:             return false;
1768:         }
1769:         return true;
1770:     }
1771: 
1772:     /**
1773:      * Returns a hash code for this object.
1774:      *
1775:      * @return A hash code.
1776:      */
1777:     public int hashCode() {
1778:         if (getLabel() != null) {
1779:             return getLabel().hashCode();
1780:         }
1781:         else {
1782:             return 0;
1783:         }
1784:     }
1785: 
1786:     /**
1787:      * Returns a clone of the object.
1788:      *
1789:      * @return A clone.
1790:      *
1791:      * @throws CloneNotSupportedException if some component of the axis does
1792:      *         not support cloning.
1793:      */
1794:     public Object clone() throws CloneNotSupportedException {
1795: 
1796:         DateAxis clone = (DateAxis) super.clone();
1797: 
1798:         // 'dateTickUnit' is immutable : no need to clone
1799:         if (this.dateFormatOverride != null) {
1800:             clone.dateFormatOverride
1801:                 = (DateFormat) this.dateFormatOverride.clone();
1802:         }
1803:         // 'tickMarkPosition' is immutable : no need to clone
1804: 
1805:         return clone;
1806: 
1807:     }
1808: 
1809: }