    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jfreechart/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * ---------------------
    028 * CyclicNumberAxis.java
    029 * ---------------------
    030 * (C) Copyright 2003-2009, by Nicolas Brodu and Contributors.
    031 *
    032 * Original Author:  Nicolas Brodu;
    033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034 *
    035 * Changes
    036 * -------
    037 * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
    038 * 16-Mar-2004 : Added plotState to draw() method (DG);
    039 * 07-Apr-2004 : Modifed text bounds calculation (DG);
    040 * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
    041 *               argument in selectAutoTickUnit() (DG);
    042 * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal
    043 *               (for consistency with other classes) and removed unused
    044 *               parameters (DG);
    045 * 08-Jun-2005 : Fixed equals() method to handle GradientPaint (DG);
    046 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG);
    047 *
    048 */
    049
    050package org.jfree.chart.axis;
    051
    052import java.awt.BasicStroke;
    053import java.awt.Color;
    054import java.awt.Font;
    055import java.awt.FontMetrics;
    056import java.awt.Graphics2D;
    057import java.awt.Paint;
    058import java.awt.Stroke;
    059import java.awt.geom.Line2D;
    060import java.awt.geom.Rectangle2D;
    061import java.io.IOException;
    062import java.io.ObjectInputStream;
    063import java.io.ObjectOutputStream;
    064import java.text.NumberFormat;
    065import java.util.List;
    066
    067import org.jfree.chart.plot.Plot;
    068import org.jfree.chart.plot.PlotRenderingInfo;
    069import org.jfree.data.Range;
    070import org.jfree.io.SerialUtilities;
    071import org.jfree.text.TextUtilities;
    072import org.jfree.ui.RectangleEdge;
    073import org.jfree.ui.TextAnchor;
    074import org.jfree.util.ObjectUtilities;
    075import org.jfree.util.PaintUtilities;
    076
    077/**
    078This class extends NumberAxis and handles cycling.
    079
    080Traditional representation of data in the range x0..x1
    081<pre>
    082|-------------------------|
    083x0                       x1
    084</pre>
    085
    086Here, the range bounds are at the axis extremities.
    087With cyclic axis, however, the time is split in
    088"cycles", or "time frames", or the same duration : the period.
    089
    090A cycle axis cannot by definition handle a larger interval
    091than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
    092period can be represented with such an axis.
    093
    094The cycle bound is the number between x0 and x1 which marks
    095the beginning of new time frame:
    096<pre>
    097|---------------------|----------------------------|
    098x0                   cb                           x1
    099<---previous cycle---><-------current cycle-------->
    100</pre>
    101
    102It is actually a multiple of the period, plus optionally
    103a start offset: <pre>cb = n * period + offset</pre>
    104
    105Thus, by definition, two consecutive cycle bounds
    106period apart, which is precisely why it is called a
    107period.
    108
    109The visual representation of a cyclic axis is like that:
    110<pre>
    111|----------------------------|---------------------|
    112cb                         x1|x0                  cb
    113<-------current cycle--------><---previous cycle--->
    114</pre>
    115
    116The cycle bound is at the axis ends, then current
    117cycle is shown, then the last cycle. When using
    118dynamic data, the visual effect is the current cycle
    119erases the last cycle as x grows. Then, the next cycle
    120bound is reached, and the process starts over, erasing
    121the previous cycle.
    122
    123A Cyclic item renderer is provided to do exactly this.
    124
    125 */
    126public class CyclicNumberAxis extends NumberAxis {
    127
    128    /** For serialization. */
    129    static final long serialVersionUID = -7514160997164582554L;
    130
    131    /** The default axis line stroke. */
    132    public static Stroke DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke(1.0f);
    133
    134    /** The default axis line paint. */
    135    public static final Paint DEFAULT_ADVANCE_LINE_PAINT = Color.gray;
    136
    137    /** The offset. */
    138    protected double offset;
    139
    140    /** The period.*/
    141    protected double period;
    142
    143    /** ??. */
    144    protected boolean boundMappedToLastCycle;
    145
    146    /** A flag that controls whether or not the advance line is visible. */
    147    protected boolean advanceLineVisible;
    148
    149    /** The advance line stroke. */
    150    protected transient Stroke advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE;
    151
    152    /** The advance line paint. */
    153    protected transient Paint advanceLinePaint;
    154
    155    private transient boolean internalMarkerWhenTicksOverlap;
    156    private transient Tick internalMarkerCycleBoundTick;
    157
    158    /**
    159     * Creates a CycleNumberAxis with the given period.
    160     *
    161     * @param period  the period.
    162     */
    163    public CyclicNumberAxis(double period) {
    164        this(period, 0.0);
    165    }
    166
    167    /**
    168     * Creates a CycleNumberAxis with the given period and offset.
    169     *
    170     * @param period  the period.
    171     * @param offset  the offset.
    172     */
    173    public CyclicNumberAxis(double period, double offset) {
    174        this(period, offset, null);
    175    }
    176
    177    /**
    178     * Creates a named CycleNumberAxis with the given period.
    179     *
    180     * @param period  the period.
    181     * @param label  the label.
    182     */
    183    public CyclicNumberAxis(double period, String label) {
    184        this(0, period, label);
    185    }
    186
    187    /**
    188     * Creates a named CycleNumberAxis with the given period and offset.
    189     *
    190     * @param period  the period.
    191     * @param offset  the offset.
    192     * @param label  the label.
    193     */
    194    public CyclicNumberAxis(double period, double offset, String label) {
    195        super(label);
    196        this.period = period;
    197        this.offset = offset;
    198        setFixedAutoRange(period);
    199        this.advanceLineVisible = true;
    200        this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT;
    201    }
    202
    203    /**
    204     * The advance line is the line drawn at the limit of the current cycle,
    205     * when erasing the previous cycle.
    206     *
    207     * @return A boolean.
    208     */
    209    public boolean isAdvanceLineVisible() {
    210        return this.advanceLineVisible;
    211    }
    212
    213    /**
    214     * The advance line is the line drawn at the limit of the current cycle,
    215     * when erasing the previous cycle.
    216     *
    217     * @param visible  the flag.
    218     */
    219    public void setAdvanceLineVisible(boolean visible) {
    220        this.advanceLineVisible = visible;
    221    }
    222
    223    /**
    224     * The advance line is the line drawn at the limit of the current cycle,
    225     * when erasing the previous cycle.
    226     *
    227     * @return The paint (never <code>null</code>).
    228     */
    229    public Paint getAdvanceLinePaint() {
    230        return this.advanceLinePaint;
    231    }
    232
    233    /**
    234     * The advance line is the line drawn at the limit of the current cycle,
    235     * when erasing the previous cycle.
    236     *
    237     * @param paint  the paint (<code>null</code> not permitted).
    238     */
    239    public void setAdvanceLinePaint(Paint paint) {
    240        if (paint == null) {
    241            throw new IllegalArgumentException("Null 'paint' argument.");
    242        }
    243        this.advanceLinePaint = paint;
    244    }
    245
    246    /**
    247     * The advance line is the line drawn at the limit of the current cycle,
    248     * when erasing the previous cycle.
    249     *
    250     * @return The stroke (never <code>null</code>).
    251     */
    252    public Stroke getAdvanceLineStroke() {
    253        return this.advanceLineStroke;
    254    }
    255    /**
    256     * The advance line is the line drawn at the limit of the current cycle,
    257     * when erasing the previous cycle.
    258     *
    259     * @param stroke  the stroke (<code>null</code> not permitted).
    260     */
    261    public void setAdvanceLineStroke(Stroke stroke) {
    262        if (stroke == null) {
    263            throw new IllegalArgumentException("Null 'stroke' argument.");
    264        }
    265        this.advanceLineStroke = stroke;
    266    }
    267
    268    /**
    269     * The cycle bound can be associated either with the current or with the
    270     * last cycle.  It's up to the user's choice to decide which, as this is
    271     * just a convention.  By default, the cycle bound is mapped to the current
    272     * cycle.
    273     * <br>
    274     * Note that this has no effect on visual appearance, as the cycle bound is
    275     * mapped successively for both axis ends. Use this function for correct
    276     * results in translateValueToJava2D.
    277     *
    278     * @return <code>true</code> if the cycle bound is mapped to the last
    279     *         cycle, <code>false</code> if it is bound to the current cycle
    280     *         (default)
    281     */
    282    public boolean isBoundMappedToLastCycle() {
    283        return this.boundMappedToLastCycle;
    284    }
    285
    286    /**
    287     * The cycle bound can be associated either with the current or with the
    288     * last cycle.  It's up to the user's choice to decide which, as this is
    289     * just a convention. By default, the cycle bound is mapped to the current
    290     * cycle.
    291     * <br>
    292     * Note that this has no effect on visual appearance, as the cycle bound is
    293     * mapped successively for both axis ends. Use this function for correct
    294     * results in valueToJava2D.
    295     *
    296     * @param boundMappedToLastCycle Set it to true to map the cycle bound to
    297     *        the last cycle.
    298     */
    299    public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) {
    300        this.boundMappedToLastCycle = boundMappedToLastCycle;
    301    }
    302
    303    /**
    304     * Selects a tick unit when the axis is displayed horizontally.
    305     *
    306     * @param g2  the graphics device.
    307     * @param drawArea  the drawing area.
    308     * @param dataArea  the data area.
    309     * @param edge  the side of the rectangle on which the axis is displayed.
    310     */
    311    protected void selectHorizontalAutoTickUnit(Graphics2D g2,
    312                                                Rectangle2D drawArea,
    313                                                Rectangle2D dataArea,
    314                                                RectangleEdge edge) {
    315
    316        double tickLabelWidth
    317            = estimateMaximumTickLabelWidth(g2, getTickUnit());
    318
    319        // Compute number of labels
    320        double n = getRange().getLength()
    321                   * tickLabelWidth / dataArea.getWidth();
    322
    323        setTickUnit(
    324            (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n),
    325            false, false
    326        );
    327
    328     }
    329
    330    /**
    331     * Selects a tick unit when the axis is displayed vertically.
    332     *
    333     * @param g2  the graphics device.
    334     * @param drawArea  the drawing area.
    335     * @param dataArea  the data area.
    336     * @param edge  the side of the rectangle on which the axis is displayed.
    337     */
    338    protected void selectVerticalAutoTickUnit(Graphics2D g2,
    339                                                Rectangle2D drawArea,
    340                                                Rectangle2D dataArea,
    341                                                RectangleEdge edge) {
    342
    343        double tickLabelWidth
    344            = estimateMaximumTickLabelWidth(g2, getTickUnit());
    345
    346        // Compute number of labels
    347        double n = getRange().getLength()
    348                   * tickLabelWidth / dataArea.getHeight();
    349
    350        setTickUnit(
    351            (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n),
    352            false, false
    353        );
    354
    355     }
    356
    357    /**
    358     * A special Number tick that also hold information about the cycle bound
    359     * mapping for this tick.  This is especially useful for having a tick at
    360     * each axis end with the cycle bound value.  See also
    361     * isBoundMappedToLastCycle()
    362     */
    363    protected static class CycleBoundTick extends NumberTick {
    364
    365        /** Map to last cycle. */
    366        public boolean mapToLastCycle;
    367
    368        /**
    369         * Creates a new tick.
    370         *
    371         * @param mapToLastCycle  map to last cycle?
    372         * @param number  the number.
    373         * @param label  the label.
    374         * @param textAnchor  the text anchor.
    375         * @param rotationAnchor  the rotation anchor.
    376         * @param angle  the rotation angle.
    377         */
    378        public CycleBoundTick(boolean mapToLastCycle, Number number,
    379                              String label, TextAnchor textAnchor,
    380                              TextAnchor rotationAnchor, double angle) {
    381            super(number, label, textAnchor, rotationAnchor, angle);
    382            this.mapToLastCycle = mapToLastCycle;
    383        }
    384    }
    385
    386    /**
    387     * Calculates the anchor point for a tick.
    388     *
    389     * @param tick  the tick.
    390     * @param cursor  the cursor.
    391     * @param dataArea  the data area.
    392     * @param edge  the side on which the axis is displayed.
    393     *
    394     * @return The anchor point.
    395     */
    396    protected float[] calculateAnchorPoint(ValueTick tick, double cursor,
    397                                           Rectangle2D dataArea,
    398                                           RectangleEdge edge) {
    399        if (tick instanceof CycleBoundTick) {
    400            boolean mapsav = this.boundMappedToLastCycle;
    401            this.boundMappedToLastCycle
    402                = ((CycleBoundTick) tick).mapToLastCycle;
    403            float[] ret = super.calculateAnchorPoint(
    404                tick, cursor, dataArea, edge
    405            );
    406            this.boundMappedToLastCycle = mapsav;
    407            return ret;
    408        }
    409        return super.calculateAnchorPoint(tick, cursor, dataArea, edge);
    410    }
    411
    412
    413
    414    /**
    415     * Builds a list of ticks for the axis.  This method is called when the
    416     * axis is at the top or bottom of the chart (so the axis is "horizontal").
    417     *
    418     * @param g2  the graphics device.
    419     * @param dataArea  the data area.
    420     * @param edge  the edge.
    421     *
    422     * @return A list of ticks.
    423     */
    424    protected List refreshTicksHorizontal(Graphics2D g2,
    425                                          Rectangle2D dataArea,
    426                                          RectangleEdge edge) {
    427
    428        List result = new java.util.ArrayList();
    429
    430        Font tickLabelFont = getTickLabelFont();
    431        g2.setFont(tickLabelFont);
    432
    433        if (isAutoTickUnitSelection()) {
    434            selectAutoTickUnit(g2, dataArea, edge);
    435        }
    436
    437        double unit = getTickUnit().getSize();
    438        double cycleBound = getCycleBound();
    439        double currentTickValue = Math.ceil(cycleBound / unit) * unit;
    440        double upperValue = getRange().getUpperBound();
    441        boolean cycled = false;
    442
    443        boolean boundMapping = this.boundMappedToLastCycle;
    444        this.boundMappedToLastCycle = false;
    445
    446        CycleBoundTick lastTick = null;
    447        float lastX = 0.0f;
    448
    449        if (upperValue == cycleBound) {
    450            currentTickValue = calculateLowestVisibleTickValue();
    451            cycled = true;
    452            this.boundMappedToLastCycle = true;
    453        }
    454
    455        while (currentTickValue <= upperValue) {
    456
    457            // Cycle when necessary
    458            boolean cyclenow = false;
    459            if ((currentTickValue + unit > upperValue) && !cycled) {
    460                cyclenow = true;
    461            }
    462
    463            double xx = valueToJava2D(currentTickValue, dataArea, edge);
    464            String tickLabel;
    465            NumberFormat formatter = getNumberFormatOverride();
    466            if (formatter != null) {
    467                tickLabel = formatter.format(currentTickValue);
    468            }
    469            else {
    470                tickLabel = getTickUnit().valueToString(currentTickValue);
    471            }
    472            float x = (float) xx;
    473            TextAnchor anchor = null;
    474            TextAnchor rotationAnchor = null;
    475            double angle = 0.0;
    476            if (isVerticalTickLabels()) {
    477                if (edge == RectangleEdge.TOP) {
    478                    angle = Math.PI / 2.0;
    479                }
    480                else {
    481                    angle = -Math.PI / 2.0;
    482                }
    483                anchor = TextAnchor.CENTER_RIGHT;
    484                // If tick overlap when cycling, update last tick too
    485                if ((lastTick != null) && (lastX == x)
    486                        && (currentTickValue != cycleBound)) {
    487                    anchor = isInverted()
    488                        ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
    489                    result.remove(result.size() - 1);
    490                    result.add(new CycleBoundTick(
    491                        this.boundMappedToLastCycle, lastTick.getNumber(),
    492                        lastTick.getText(), anchor, anchor,
    493                        lastTick.getAngle())
    494                    );
    495                    this.internalMarkerWhenTicksOverlap = true;
    496                    anchor = isInverted()
    497                        ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
    498                }
    499                rotationAnchor = anchor;
    500            }
    501            else {
    502                if (edge == RectangleEdge.TOP) {
    503                    anchor = TextAnchor.BOTTOM_CENTER;
    504                    if ((lastTick != null) && (lastX == x)
    505                            && (currentTickValue != cycleBound)) {
    506                        anchor = isInverted()
    507                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
    508                        result.remove(result.size() - 1);
    509                        result.add(new CycleBoundTick(
    510                            this.boundMappedToLastCycle, lastTick.getNumber(),
    511                            lastTick.getText(), anchor, anchor,
    512                            lastTick.getAngle())
    513                        );
    514                        this.internalMarkerWhenTicksOverlap = true;
    515                        anchor = isInverted()
    516                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
    517                    }
    518                    rotationAnchor = anchor;
    519                }
    520                else {
    521                    anchor = TextAnchor.TOP_CENTER;
    522                    if ((lastTick != null) && (lastX == x)
    523                            && (currentTickValue != cycleBound)) {
    524                        anchor = isInverted()
    525                            ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT;
    526                        result.remove(result.size() - 1);
    527                        result.add(new CycleBoundTick(
    528                            this.boundMappedToLastCycle, lastTick.getNumber(),
    529                            lastTick.getText(), anchor, anchor,
    530                            lastTick.getAngle())
    531                        );
    532                        this.internalMarkerWhenTicksOverlap = true;
    533                        anchor = isInverted()
    534                            ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT;
    535                    }
    536                    rotationAnchor = anchor;
    537                }
    538            }
    539
    540            CycleBoundTick tick = new CycleBoundTick(
    541                this.boundMappedToLastCycle,
    542                new Double(currentTickValue), tickLabel, anchor,
    543                rotationAnchor, angle
    544            );
    545            if (currentTickValue == cycleBound) {
    546                this.internalMarkerCycleBoundTick = tick;
    547            }
    548            result.add(tick);
    549            lastTick = tick;
    550            lastX = x;
    551
    552            currentTickValue += unit;
    553
    554            if (cyclenow) {
    555                currentTickValue = calculateLowestVisibleTickValue();
    556                upperValue = cycleBound;
    557                cycled = true;
    558                this.boundMappedToLastCycle = true;
    559            }
    560
    561        }
    562        this.boundMappedToLastCycle = boundMapping;
    563        return result;
    564
    565    }
    566
    567    /**
    568     * Builds a list of ticks for the axis.  This method is called when the
    569     * axis is at the left or right of the chart (so the axis is "vertical").
    570     *
    571     * @param g2  the graphics device.
    572     * @param dataArea  the data area.
    573     * @param edge  the edge.
    574     *
    575     * @return A list of ticks.
    576     */
    577    protected List refreshVerticalTicks(Graphics2D g2,
    578                                        Rectangle2D dataArea,
    579                                        RectangleEdge edge) {
    580
    581        List result = new java.util.ArrayList();
    582        result.clear();
    583
    584        Font tickLabelFont = getTickLabelFont();
    585        g2.setFont(tickLabelFont);
    586        if (isAutoTickUnitSelection()) {
    587            selectAutoTickUnit(g2, dataArea, edge);
    588        }
    589
    590        double unit = getTickUnit().getSize();
    591        double cycleBound = getCycleBound();
    592        double currentTickValue = Math.ceil(cycleBound / unit) * unit;
    593        double upperValue = getRange().getUpperBound();
    594        boolean cycled = false;
    595
    596        boolean boundMapping = this.boundMappedToLastCycle;
    597        this.boundMappedToLastCycle = true;
    598
    599        NumberTick lastTick = null;
    600        float lastY = 0.0f;
    601
    602        if (upperValue == cycleBound) {
    603            currentTickValue = calculateLowestVisibleTickValue();
    604            cycled = true;
    605            this.boundMappedToLastCycle = true;
    606        }
    607
    608        while (currentTickValue <= upperValue) {
    609
    610            // Cycle when necessary
    611            boolean cyclenow = false;
    612            if ((currentTickValue + unit > upperValue) && !cycled) {
    613                cyclenow = true;
    614            }
    615
    616            double yy = valueToJava2D(currentTickValue, dataArea, edge);
    617            String tickLabel;
    618            NumberFormat formatter = getNumberFormatOverride();
    619            if (formatter != null) {
    620                tickLabel = formatter.format(currentTickValue);
    621            }
    622            else {
    623                tickLabel = getTickUnit().valueToString(currentTickValue);
    624            }
    625
    626            float y = (float) yy;
    627            TextAnchor anchor = null;
    628            TextAnchor rotationAnchor = null;
    629            double angle = 0.0;
    630            if (isVerticalTickLabels()) {
    631
    632                if (edge == RectangleEdge.LEFT) {
    633                    anchor = TextAnchor.BOTTOM_CENTER;
    634                    if ((lastTick != null) && (lastY == y)
    635                            && (currentTickValue != cycleBound)) {
    636                        anchor = isInverted()
    637                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
    638                        result.remove(result.size() - 1);
    639                        result.add(new CycleBoundTick(
    640                            this.boundMappedToLastCycle, lastTick.getNumber(),
    641                            lastTick.getText(), anchor, anchor,
    642                            lastTick.getAngle())
    643                        );
    644                        this.internalMarkerWhenTicksOverlap = true;
    645                        anchor = isInverted()
    646                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
    647                    }
    648                    rotationAnchor = anchor;
    649                    angle = -Math.PI / 2.0;
    650                }
    651                else {
    652                    anchor = TextAnchor.BOTTOM_CENTER;
    653                    if ((lastTick != null) && (lastY == y)
    654                            && (currentTickValue != cycleBound)) {
    655                        anchor = isInverted()
    656                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
    657                        result.remove(result.size() - 1);
    658                        result.add(new CycleBoundTick(
    659                            this.boundMappedToLastCycle, lastTick.getNumber(),
    660                            lastTick.getText(), anchor, anchor,
    661                            lastTick.getAngle())
    662                        );
    663                        this.internalMarkerWhenTicksOverlap = true;
    664                        anchor = isInverted()
    665                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
    666                    }
    667                    rotationAnchor = anchor;
    668                    angle = Math.PI / 2.0;
    669                }
    670            }
    671            else {
    672                if (edge == RectangleEdge.LEFT) {
    673                    anchor = TextAnchor.CENTER_RIGHT;
    674                    if ((lastTick != null) && (lastY == y)
    675                            && (currentTickValue != cycleBound)) {
    676                        anchor = isInverted()
    677                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
    678                        result.remove(result.size() - 1);
    679                        result.add(new CycleBoundTick(
    680                            this.boundMappedToLastCycle, lastTick.getNumber(),
    681                            lastTick.getText(), anchor, anchor,
    682                            lastTick.getAngle())
    683                        );
    684                        this.internalMarkerWhenTicksOverlap = true;
    685                        anchor = isInverted()
    686                            ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
    687                    }
    688                    rotationAnchor = anchor;
    689                }
    690                else {
    691                    anchor = TextAnchor.CENTER_LEFT;
    692                    if ((lastTick != null) && (lastY == y)
    693                            && (currentTickValue != cycleBound)) {
    694                        anchor = isInverted()
    695                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT;
    696                        result.remove(result.size() - 1);
    697                        result.add(new CycleBoundTick(
    698                            this.boundMappedToLastCycle, lastTick.getNumber(),
    699                            lastTick.getText(), anchor, anchor,
    700                            lastTick.getAngle())
    701                        );
    702                        this.internalMarkerWhenTicksOverlap = true;
    703                        anchor = isInverted()
    704                            ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT;
    705                    }
    706                    rotationAnchor = anchor;
    707                }
    708            }
    709
    710            CycleBoundTick tick = new CycleBoundTick(
    711                this.boundMappedToLastCycle, new Double(currentTickValue),
    712                tickLabel, anchor, rotationAnchor, angle
    713            );
    714            if (currentTickValue == cycleBound) {
    715                this.internalMarkerCycleBoundTick = tick;
    716            }
    717            result.add(tick);
    718            lastTick = tick;
    719            lastY = y;
    720
    721            if (currentTickValue == cycleBound) {
    722                this.internalMarkerCycleBoundTick = tick;
    723            }
    724
    725            currentTickValue += unit;
    726
    727            if (cyclenow) {
    728                currentTickValue = calculateLowestVisibleTickValue();
    729                upperValue = cycleBound;
    730                cycled = true;
    731                this.boundMappedToLastCycle = false;
    732            }
    733
    734        }
    735        this.boundMappedToLastCycle = boundMapping;
    736        return result;
    737    }
    738
    739    /**
    740     * Converts a coordinate from Java 2D space to data space.
    741     *
    742     * @param java2DValue  the coordinate in Java2D space.
    743     * @param dataArea  the data area.
    744     * @param edge  the edge.
    745     *
    746     * @return The data value.
    747     */
    748    public double java2DToValue(double java2DValue, Rectangle2D dataArea,
    749                                RectangleEdge edge) {
    750        Range range = getRange();
    751
    752        double vmax = range.getUpperBound();
    753        double vp = getCycleBound();
    754
    755        double jmin = 0.0;
    756        double jmax = 0.0;
    757        if (RectangleEdge.isTopOrBottom(edge)) {
    758            jmin = dataArea.getMinX();
    759            jmax = dataArea.getMaxX();
    760        }
    761        else if (RectangleEdge.isLeftOrRight(edge)) {
    762            jmin = dataArea.getMaxY();
    763            jmax = dataArea.getMinY();
    764        }
    765
    766        if (isInverted()) {
    767            double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period;
    768            if (java2DValue >= jbreak) {
    769                return vp + (jmax - java2DValue) * this.period / (jmax - jmin);
    770            }
    771            else {
    772                return vp - (java2DValue - jmin) * this.period / (jmax - jmin);
    773            }
    774        }
    775        else {
    776            double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin;
    777            if (java2DValue <= jbreak) {
    778                return vp + (java2DValue - jmin) * this.period / (jmax - jmin);
    779            }
    780            else {
    781                return vp - (jmax - java2DValue) * this.period / (jmax - jmin);
    782            }
    783        }
    784    }
    785
    786    /**
    787     * Translates a value from data space to Java 2D space.
    788     *
    789     * @param value  the data value.
    790     * @param dataArea  the data area.
    791     * @param edge  the edge.
    792     *
    793     * @return The Java 2D value.
    794     */
    795    public double valueToJava2D(double value, Rectangle2D dataArea,
    796                                RectangleEdge edge) {
    797        Range range = getRange();
    798
    799        double vmin = range.getLowerBound();
    800        double vmax = range.getUpperBound();
    801        double vp = getCycleBound();
    802
    803        if ((value < vmin) || (value > vmax)) {
    804            return Double.NaN;
    805        }
    806
    807
    808        double jmin = 0.0;
    809        double jmax = 0.0;
    810        if (RectangleEdge.isTopOrBottom(edge)) {
    811            jmin = dataArea.getMinX();
    812            jmax = dataArea.getMaxX();
    813        }
    814        else if (RectangleEdge.isLeftOrRight(edge)) {
    815            jmax = dataArea.getMinY();
    816            jmin = dataArea.getMaxY();
    817        }
    818
    819        if (isInverted()) {
    820            if (value == vp) {
    821                return this.boundMappedToLastCycle ? jmin : jmax;
    822            }
    823            else if (value > vp) {
    824                return jmax - (value - vp) * (jmax - jmin) / this.period;
    825            }
    826            else {
    827                return jmin + (vp - value) * (jmax - jmin) / this.period;
    828            }
    829        }
    830        else {
    831            if (value == vp) {
    832                return this.boundMappedToLastCycle ? jmax : jmin;
    833            }
    834            else if (value >= vp) {
    835                return jmin + (value - vp) * (jmax - jmin) / this.period;
    836            }
    837            else {
    838                return jmax - (vp - value) * (jmax - jmin) / this.period;
    839            }
    840        }
    841    }
    842
    843    /**
    844     * Centers the range about the given value.
    845     *
    846     * @param value  the data value.
    847     */
    848    public void centerRange(double value) {
    849        setRange(value - this.period / 2.0, value + this.period / 2.0);
    850    }
    851
    852    /**
    853     * This function is nearly useless since the auto range is fixed for this
    854     * class to the period.  The period is extended if necessary to fit the
    855     * minimum size.
    856     *
    857     * @param size  the size.
    858     * @param notify  notify?
    859     *
    860     * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double,
    861     *      boolean)
    862     */
    863    public void setAutoRangeMinimumSize(double size, boolean notify) {
    864        if (size > this.period) {
    865            this.period = size;
    866        }
    867        super.setAutoRangeMinimumSize(size, notify);
    868    }
    869
    870    /**
    871     * The auto range is fixed for this class to the period by default.
    872     * This function will thus set a new period.
    873     *
    874     * @param length  the length.
    875     *
    876     * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double)
    877     */
    878    public void setFixedAutoRange(double length) {
    879        this.period = length;
    880        super.setFixedAutoRange(length);
    881    }
    882
    883    /**
    884     * Sets a new axis range. The period is extended to fit the range size, if
    885     * necessary.
    886     *
    887     * @param range  the range.
    888     * @param turnOffAutoRange  switch off the auto range.
    889     * @param notify notify?
    890     *
    891     * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean)
    892     */
    893    public void setRange(Range range, boolean turnOffAutoRange,
    894                         boolean notify) {
    895        double size = range.getUpperBound() - range.getLowerBound();
    896        if (size > this.period) {
    897            this.period = size;
    898        }
    899        super.setRange(range, turnOffAutoRange, notify);
    900    }
    901
    902    /**
    903     * The cycle bound is defined as the higest value x such that
    904     * "offset + period * i = x", with i and integer and x &lt;
    905     * range.getUpperBound() This is the value which is at both ends of the
    906     * axis :  x...up|low...x
    907     * The values from x to up are the valued in the current cycle.
    908     * The values from low to x are the valued in the previous cycle.
    909     *
    910     * @return The cycle bound.
    911     */
    912    public double getCycleBound() {
    913        return Math.floor(
    914            (getRange().getUpperBound() - this.offset) / this.period
    915        ) * this.period + this.offset;
    916    }
    917
    918    /**
    919     * The cycle bound is a multiple of the period, plus optionally a start
    920     * offset.
    921     * <P>
    922     * <pre>cb = n * period + offset</pre><br>
    923     *
    924     * @return The current offset.
    925     *
    926     * @see #getCycleBound()
    927     */
    928    public double getOffset() {
    929        return this.offset;
    930    }
    931
    932    /**
    933     * The cycle bound is a multiple of the period, plus optionally a start
    934     * offset.
    935     * <P>
    936     * <pre>cb = n * period + offset</pre><br>
    937     *
    938     * @param offset The offset to set.
    939     *
    940     * @see #getCycleBound()
    941     */
    942    public void setOffset(double offset) {
    943        this.offset = offset;
    944    }
    945
    946    /**
    947     * The cycle bound is a multiple of the period, plus optionally a start
    948     * offset.
    949     * <P>
    950     * <pre>cb = n * period + offset</pre><br>
    951     *
    952     * @return The current period.
    953     *
    954     * @see #getCycleBound()
    955     */
    956    public double getPeriod() {
    957        return this.period;
    958    }
    959
    960    /**
    961     * The cycle bound is a multiple of the period, plus optionally a start
    962     * offset.
    963     * <P>
    964     * <pre>cb = n * period + offset</pre><br>
    965     *
    966     * @param period The period to set.
    967     *
    968     * @see #getCycleBound()
    969     */
    970    public void setPeriod(double period) {
    971        this.period = period;
    972    }
    973
    974    /**
    975     * Draws the tick marks and labels.
    976     *
    977     * @param g2  the graphics device.
    978     * @param cursor  the cursor.
    979     * @param plotArea  the plot area.
    980     * @param dataArea  the area inside the axes.
    981     * @param edge  the side on which the axis is displayed.
    982     *
    983     * @return The axis state.
    984     */
    985    protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor,
    986            Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge) {
    987        this.internalMarkerWhenTicksOverlap = false;
    988        AxisState ret = super.drawTickMarksAndLabels(g2, cursor, plotArea,
    989                dataArea, edge);
    990
    991        // continue and separate the labels only if necessary
    992        if (!this.internalMarkerWhenTicksOverlap) {
    993            return ret;
    994        }
    995
    996        double ol;
    997        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
    998        if (isVerticalTickLabels()) {
    999            ol = fm.getMaxAdvance();
    1000        }
    1001        else {
    1002            ol = fm.getHeight();
    1003        }
    1004
    1005        double il = 0;
    1006        if (isTickMarksVisible()) {
    1007            float xx = (float) valueToJava2D(getRange().getUpperBound(),
    1008                    dataArea, edge);
    1009            Line2D mark = null;
    1010            g2.setStroke(getTickMarkStroke());
    1011            g2.setPaint(getTickMarkPaint());
    1012            if (edge == RectangleEdge.LEFT) {
    1013                mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
    1014            }
    1015            else if (edge == RectangleEdge.RIGHT) {
    1016                mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
    1017            }
    1018            else if (edge == RectangleEdge.TOP) {
    1019                mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
    1020            }
    1021            else if (edge == RectangleEdge.BOTTOM) {
    1022                mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
    1023            }
    1024            g2.draw(mark);
    1025        }
    1026        return ret;
    1027    }
    1028
    1029    /**
    1030     * Draws the axis.
    1031     *
    1032     * @param g2  the graphics device (<code>null</code> not permitted).
    1033     * @param cursor  the cursor position.
    1034     * @param plotArea  the plot area (<code>null</code> not permitted).
    1035     * @param dataArea  the data area (<code>null</code> not permitted).
    1036     * @param edge  the edge (<code>null</code> not permitted).
    1037     * @param plotState  collects information about the plot
    1038     *                   (<code>null</code> permitted).
    1039     *
    1040     * @return The axis state (never <code>null</code>).
    1041     */
    1042    public AxisState draw(Graphics2D g2,
    1043                          double cursor,
    1044                          Rectangle2D plotArea,
    1045                          Rectangle2D dataArea,
    1046                          RectangleEdge edge,
    1047                          PlotRenderingInfo plotState) {
    1048
    1049        AxisState ret = super.draw(
    1050            g2, cursor, plotArea, dataArea, edge, plotState
    1051        );
    1052        if (isAdvanceLineVisible()) {
    1053            double xx = valueToJava2D(
    1054                getRange().getUpperBound(), dataArea, edge
    1055            );
    1056            Line2D mark = null;
    1057            g2.setStroke(getAdvanceLineStroke());
    1058            g2.setPaint(getAdvanceLinePaint());
    1059            if (edge == RectangleEdge.LEFT) {
    1060                mark = new Line2D.Double(
    1061                    cursor, xx, cursor + dataArea.getWidth(), xx
    1062                );
    1063            }
    1064            else if (edge == RectangleEdge.RIGHT) {
    1065                mark = new Line2D.Double(
    1066                    cursor - dataArea.getWidth(), xx, cursor, xx
    1067                );
    1068            }
    1069            else if (edge == RectangleEdge.TOP) {
    1070                mark = new Line2D.Double(
    1071                    xx, cursor + dataArea.getHeight(), xx, cursor
    1072                );
    1073            }
    1074            else if (edge == RectangleEdge.BOTTOM) {
    1075                mark = new Line2D.Double(
    1076                    xx, cursor, xx, cursor - dataArea.getHeight()
    1077                );
    1078            }
    1079            g2.draw(mark);
    1080        }
    1081        return ret;
    1082    }
    1083
    1084    /**
    1085     * Reserve some space on each axis side because we draw a centered label at
    1086     * each extremity.
    1087     *
    1088     * @param g2  the graphics device.
    1089     * @param plot  the plot.
    1090     * @param plotArea  the plot area.
    1091     * @param edge  the edge.
    1092     * @param space  the space already reserved.
    1093     *
    1094     * @return The reserved space.
    1095     */
    1096    public AxisSpace reserveSpace(Graphics2D g2,
    1097                                  Plot plot,
    1098                                  Rectangle2D plotArea,
    1099                                  RectangleEdge edge,
    1100                                  AxisSpace space) {
    1101
    1102        this.internalMarkerCycleBoundTick = null;
    1103        AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
    1104        if (this.internalMarkerCycleBoundTick == null) {
    1105            return ret;
    1106        }
    1107
    1108        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
    1109        Rectangle2D r = TextUtilities.getTextBounds(
    1110            this.internalMarkerCycleBoundTick.getText(), g2, fm
    1111        );
    1112
    1113        if (RectangleEdge.isTopOrBottom(edge)) {
    1114            if (isVerticalTickLabels()) {
    1115                space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
    1116            }
    1117            else {
    1118                space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
    1119            }
    1120        }
    1121        else if (RectangleEdge.isLeftOrRight(edge)) {
    1122            if (isVerticalTickLabels()) {
    1123                space.add(r.getWidth() / 2, RectangleEdge.TOP);
    1124            }
    1125            else {
    1126                space.add(r.getHeight() / 2, RectangleEdge.TOP);
    1127            }
    1128        }
    1129
    1130        return ret;
    1131
    1132    }
    1133
    1134    /**
    1135     * Provides serialization support.
    1136     *
    1137     * @param stream  the output stream.
    1138     *
    1139     * @throws IOException  if there is an I/O error.
    1140     */
    1141    private void writeObject(ObjectOutputStream stream) throws IOException {
    1142
    1143        stream.defaultWriteObject();
    1144        SerialUtilities.writePaint(this.advanceLinePaint, stream);
    1145        SerialUtilities.writeStroke(this.advanceLineStroke, stream);
    1146
    1147    }
    1148
    1149    /**
    1150     * Provides serialization support.
    1151     *
    1152     * @param stream  the input stream.
    1153     *
    1154     * @throws IOException  if there is an I/O error.
    1155     * @throws ClassNotFoundException  if there is a classpath problem.
    1156     */
    1157    private void readObject(ObjectInputStream stream)
    1158        throws IOException, ClassNotFoundException {
    1159
    1160        stream.defaultReadObject();
    1161        this.advanceLinePaint = SerialUtilities.readPaint(stream);
    1162        this.advanceLineStroke = SerialUtilities.readStroke(stream);
    1163
    1164    }
    1165
    1166
    1167    /**
    1168     * Tests the axis for equality with another object.
    1169     *
    1170     * @param obj  the object to test against.
    1171     *
    1172     * @return A boolean.
    1173     */
    1174    public boolean equals(Object obj) {
    1175        if (obj == this) {
    1176            return true;
    1177        }
    1178        if (!(obj instanceof CyclicNumberAxis)) {
    1179            return false;
    1180        }
    1181        if (!super.equals(obj)) {
    1182            return false;
    1183        }
    1184        CyclicNumberAxis that = (CyclicNumberAxis) obj;
    1185        if (this.period != that.period) {
    1186            return false;
    1187        }
    1188        if (this.offset != that.offset) {
    1189            return false;
    1190        }
    1191        if (!PaintUtilities.equal(this.advanceLinePaint,
    1192                that.advanceLinePaint)) {
    1193            return false;
    1194        }
    1195        if (!ObjectUtilities.equal(this.advanceLineStroke,
    1196                that.advanceLineStroke)) {
    1197            return false;
    1198        }
    1199        if (this.advanceLineVisible != that.advanceLineVisible) {
    1200            return false;
    1201        }
    1202        if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) {
    1203            return false;
    1204        }
    1205        return true;
    1206    }
    1207}