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