001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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-2014, 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 &gt;= 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&lt;---previous cycle---&gt;&lt;-------current cycle--------&gt;
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&lt;-------current cycle--------&gt;&lt;---previous cycle---&gt;
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     * <pre>cb = n * period + offset</pre>
917     *
918     * @return The current offset.
919     *
920     * @see #getCycleBound()
921     */
922    public double getOffset() {
923        return this.offset;
924    }
925
926    /**
927     * The cycle bound is a multiple of the period, plus optionally a start
928     * offset.
929     * <pre>cb = n * period + offset</pre>
930     *
931     * @param offset The offset to set.
932     *
933     * @see #getCycleBound()
934     */
935    public void setOffset(double offset) {
936        this.offset = offset;
937    }
938
939    /**
940     * The cycle bound is a multiple of the period, plus optionally a start
941     * offset.
942     * <pre>cb = n * period + offset</pre>
943     *
944     * @return The current period.
945     *
946     * @see #getCycleBound()
947     */
948    public double getPeriod() {
949        return this.period;
950    }
951
952    /**
953     * The cycle bound is a multiple of the period, plus optionally a start
954     * offset.
955     * <pre>cb = n * period + offset</pre>
956     *
957     * @param period The period to set.
958     *
959     * @see #getCycleBound()
960     */
961    public void setPeriod(double period) {
962        this.period = period;
963    }
964
965    /**
966     * Draws the tick marks and labels.
967     *
968     * @param g2  the graphics device.
969     * @param cursor  the cursor.
970     * @param plotArea  the plot area.
971     * @param dataArea  the area inside the axes.
972     * @param edge  the side on which the axis is displayed.
973     *
974     * @return The axis state.
975     */
976    @Override
977    protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor,
978            Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge) {
979        this.internalMarkerWhenTicksOverlap = false;
980        AxisState ret = super.drawTickMarksAndLabels(g2, cursor, plotArea,
981                dataArea, edge);
982
983        // continue and separate the labels only if necessary
984        if (!this.internalMarkerWhenTicksOverlap) {
985            return ret;
986        }
987
988        double ol;
989        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
990        if (isVerticalTickLabels()) {
991            ol = fm.getMaxAdvance();
992        }
993        else {
994            ol = fm.getHeight();
995        }
996
997        double il = 0;
998        if (isTickMarksVisible()) {
999            float xx = (float) valueToJava2D(getRange().getUpperBound(),
1000                    dataArea, edge);
1001            Line2D mark = null;
1002            g2.setStroke(getTickMarkStroke());
1003            g2.setPaint(getTickMarkPaint());
1004            if (edge == RectangleEdge.LEFT) {
1005                mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
1006            }
1007            else if (edge == RectangleEdge.RIGHT) {
1008                mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
1009            }
1010            else if (edge == RectangleEdge.TOP) {
1011                mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
1012            }
1013            else if (edge == RectangleEdge.BOTTOM) {
1014                mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
1015            }
1016            g2.draw(mark);
1017        }
1018        return ret;
1019    }
1020
1021    /**
1022     * Draws the axis.
1023     *
1024     * @param g2  the graphics device (<code>null</code> not permitted).
1025     * @param cursor  the cursor position.
1026     * @param plotArea  the plot area (<code>null</code> not permitted).
1027     * @param dataArea  the data area (<code>null</code> not permitted).
1028     * @param edge  the edge (<code>null</code> not permitted).
1029     * @param plotState  collects information about the plot
1030     *                   (<code>null</code> permitted).
1031     *
1032     * @return The axis state (never <code>null</code>).
1033     */
1034    @Override
1035    public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
1036            Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState) {
1037
1038        AxisState ret = super.draw(g2, cursor, plotArea, dataArea, edge, 
1039                plotState);
1040        if (isAdvanceLineVisible()) {
1041            double xx = valueToJava2D(getRange().getUpperBound(), dataArea, 
1042                    edge);
1043            Line2D mark = null;
1044            g2.setStroke(getAdvanceLineStroke());
1045            g2.setPaint(getAdvanceLinePaint());
1046            if (edge == RectangleEdge.LEFT) {
1047                mark = new Line2D.Double(cursor, xx, cursor 
1048                        + dataArea.getWidth(), xx);
1049            }
1050            else if (edge == RectangleEdge.RIGHT) {
1051                mark = new Line2D.Double(cursor - dataArea.getWidth(), xx, 
1052                        cursor, xx);
1053            }
1054            else if (edge == RectangleEdge.TOP) {
1055                mark = new Line2D.Double(xx, cursor + dataArea.getHeight(), xx, 
1056                        cursor);
1057            }
1058            else if (edge == RectangleEdge.BOTTOM) {
1059                mark = new Line2D.Double(xx, cursor, xx, 
1060                        cursor - dataArea.getHeight());
1061            }
1062            g2.draw(mark);
1063        }
1064        return ret;
1065    }
1066
1067    /**
1068     * Reserve some space on each axis side because we draw a centered label at
1069     * each extremity.
1070     *
1071     * @param g2  the graphics device.
1072     * @param plot  the plot.
1073     * @param plotArea  the plot area.
1074     * @param edge  the edge.
1075     * @param space  the space already reserved.
1076     *
1077     * @return The reserved space.
1078     */
1079    @Override
1080    public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
1081            Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
1082
1083        this.internalMarkerCycleBoundTick = null;
1084        AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
1085        if (this.internalMarkerCycleBoundTick == null) {
1086            return ret;
1087        }
1088
1089        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
1090        Rectangle2D r = TextUtilities.getTextBounds(
1091            this.internalMarkerCycleBoundTick.getText(), g2, fm
1092        );
1093
1094        if (RectangleEdge.isTopOrBottom(edge)) {
1095            if (isVerticalTickLabels()) {
1096                space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
1097            }
1098            else {
1099                space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
1100            }
1101        }
1102        else if (RectangleEdge.isLeftOrRight(edge)) {
1103            if (isVerticalTickLabels()) {
1104                space.add(r.getWidth() / 2, RectangleEdge.TOP);
1105            }
1106            else {
1107                space.add(r.getHeight() / 2, RectangleEdge.TOP);
1108            }
1109        }
1110
1111        return ret;
1112
1113    }
1114
1115    /**
1116     * Provides serialization support.
1117     *
1118     * @param stream  the output stream.
1119     *
1120     * @throws IOException  if there is an I/O error.
1121     */
1122    private void writeObject(ObjectOutputStream stream) throws IOException {
1123        stream.defaultWriteObject();
1124        SerialUtilities.writePaint(this.advanceLinePaint, stream);
1125        SerialUtilities.writeStroke(this.advanceLineStroke, stream);
1126    }
1127
1128    /**
1129     * Provides serialization support.
1130     *
1131     * @param stream  the input stream.
1132     *
1133     * @throws IOException  if there is an I/O error.
1134     * @throws ClassNotFoundException  if there is a classpath problem.
1135     */
1136    private void readObject(ObjectInputStream stream)
1137            throws IOException, ClassNotFoundException {
1138        stream.defaultReadObject();
1139        this.advanceLinePaint = SerialUtilities.readPaint(stream);
1140        this.advanceLineStroke = SerialUtilities.readStroke(stream);
1141    }
1142
1143
1144    /**
1145     * Tests the axis for equality with another object.
1146     *
1147     * @param obj  the object to test against.
1148     *
1149     * @return A boolean.
1150     */
1151    @Override
1152    public boolean equals(Object obj) {
1153        if (obj == this) {
1154            return true;
1155        }
1156        if (!(obj instanceof CyclicNumberAxis)) {
1157            return false;
1158        }
1159        if (!super.equals(obj)) {
1160            return false;
1161        }
1162        CyclicNumberAxis that = (CyclicNumberAxis) obj;
1163        if (this.period != that.period) {
1164            return false;
1165        }
1166        if (this.offset != that.offset) {
1167            return false;
1168        }
1169        if (!PaintUtilities.equal(this.advanceLinePaint,
1170                that.advanceLinePaint)) {
1171            return false;
1172        }
1173        if (!ObjectUtilities.equal(this.advanceLineStroke,
1174                that.advanceLineStroke)) {
1175            return false;
1176        }
1177        if (this.advanceLineVisible != that.advanceLineVisible) {
1178            return false;
1179        }
1180        if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) {
1181            return false;
1182        }
1183        return true;
1184    }
1185}