    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
    050    package org.jfree.chart.axis;
    051
    052    import java.awt.BasicStroke;
    053    import java.awt.Color;
    054    import java.awt.Font;
    055    import java.awt.FontMetrics;
    056    import java.awt.Graphics2D;
    057    import java.awt.Paint;
    058    import java.awt.Stroke;
    059    import java.awt.geom.Line2D;
    060    import java.awt.geom.Rectangle2D;
    061    import java.io.IOException;
    062    import java.io.ObjectInputStream;
    063    import java.io.ObjectOutputStream;
    064    import java.text.NumberFormat;
    065    import java.util.List;
    066
    067    import org.jfree.chart.plot.Plot;
    068    import org.jfree.chart.plot.PlotRenderingInfo;
    069    import org.jfree.data.Range;
    070    import org.jfree.io.SerialUtilities;
    071    import org.jfree.text.TextUtilities;
    072    import org.jfree.ui.RectangleEdge;
    073    import org.jfree.ui.TextAnchor;
    074    import org.jfree.util.ObjectUtilities;
    075    import org.jfree.util.PaintUtilities;
    076
    077    /**
    078    This class extends NumberAxis and handles cycling.
    079
    080    Traditional representation of data in the range x0..x1
    081    <pre>
    082    |-------------------------|
    083    x0                       x1
    084    </pre>
    085
    086    Here, the range bounds are at the axis extremities.
    087    With cyclic axis, however, the time is split in
    088    "cycles", or "time frames", or the same duration : the period.
    089
    090    A cycle axis cannot by definition handle a larger interval
    091    than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
    092    period can be represented with such an axis.
    093
    094    The cycle bound is the number between x0 and x1 which marks
    095    the beginning of new time frame:
    096    <pre>
    097    |---------------------|----------------------------|
    098    x0                   cb                           x1
    099    <---previous cycle---><-------current cycle-------->
    100    </pre>
    101
    102    It is actually a multiple of the period, plus optionally
    103    a start offset: <pre>cb = n * period + offset</pre>
    104
    105    Thus, by definition, two consecutive cycle bounds
    106    period apart, which is precisely why it is called a
    107    period.
    108
    109    The visual representation of a cyclic axis is like that:
    110    <pre>
    111    |----------------------------|---------------------|
    112    cb                         x1|x0                  cb
    113    <-------current cycle--------><---previous cycle--->
    114    </pre>
    115
    116    The cycle bound is at the axis ends, then current
    117    cycle is shown, then the last cycle. When using
    118    dynamic data, the visual effect is the current cycle
    119    erases the last cycle as x grows. Then, the next cycle
    120    bound is reached, and the process starts over, erasing
    121    the previous cycle.
    122
    123    A Cyclic item renderer is provided to do exactly this.
    124
    125     */
    126    public 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    }