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