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