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
050package org.jfree.chart.axis;
051
052import java.awt.BasicStroke;
053import java.awt.Color;
054import java.awt.Font;
055import java.awt.FontMetrics;
056import java.awt.Graphics2D;
057import java.awt.Paint;
058import java.awt.Stroke;
059import java.awt.geom.Line2D;
060import java.awt.geom.Rectangle2D;
061import java.io.IOException;
062import java.io.ObjectInputStream;
063import java.io.ObjectOutputStream;
064import java.text.NumberFormat;
065import java.util.List;
066
067import org.jfree.chart.plot.Plot;
068import org.jfree.chart.plot.PlotRenderingInfo;
069import org.jfree.data.Range;
070import org.jfree.io.SerialUtilities;
071import org.jfree.text.TextUtilities;
072import org.jfree.ui.RectangleEdge;
073import org.jfree.ui.TextAnchor;
074import org.jfree.util.ObjectUtilities;
075import org.jfree.util.PaintUtilities;
076
077/**
078This class extends NumberAxis and handles cycling.
079
080Traditional representation of data in the range x0..x1
081<pre>
082|-------------------------|
083x0                       x1
084</pre>
085
086Here, the range bounds are at the axis extremities.
087With cyclic axis, however, the time is split in
088"cycles", or "time frames", or the same duration : the period.
089
090A cycle axis cannot by definition handle a larger interval
091than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
092period can be represented with such an axis.
093
094The cycle bound is the number between x0 and x1 which marks
095the beginning of new time frame:
096<pre>
097|---------------------|----------------------------|
098x0                   cb                           x1
099<---previous cycle---><-------current cycle-------->
100</pre>
101
102It is actually a multiple of the period, plus optionally
103a start offset: <pre>cb = n * period + offset</pre>
104
105Thus, by definition, two consecutive cycle bounds
106period apart, which is precisely why it is called a
107period.
108
109The visual representation of a cyclic axis is like that:
110<pre>
111|----------------------------|---------------------|
112cb                         x1|x0                  cb
113<-------current cycle--------><---previous cycle--->
114</pre>
115
116The cycle bound is at the axis ends, then current
117cycle is shown, then the last cycle. When using
118dynamic data, the visual effect is the current cycle
119erases the last cycle as x grows. Then, the next cycle
120bound is reached, and the process starts over, erasing
121the previous cycle.
122
123A Cyclic item renderer is provided to do exactly this.
124
125 */
126public 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}