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