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