001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2021, 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-2020, by Nicolas Brodu and Contributors.
031 *
032 * Original Author:  Nicolas Brodu;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
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.ui.RectangleEdge;
059import org.jfree.chart.ui.TextAnchor;
060import org.jfree.chart.util.PaintUtils;
061import org.jfree.chart.util.Args;
062import org.jfree.chart.util.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(
517                this.boundMappedToLastCycle, currentTickValue, tickLabel, anchor,
518                rotationAnchor, angle
519            );
520            if (currentTickValue == cycleBound) {
521                this.internalMarkerCycleBoundTick = tick;
522            }
523            result.add(tick);
524            lastTick = tick;
525            lastX = x;
526
527            currentTickValue += unit;
528
529            if (cyclenow) {
530                currentTickValue = calculateLowestVisibleTickValue();
531                upperValue = cycleBound;
532                cycled = true;
533                this.boundMappedToLastCycle = true;
534            }
535
536        }
537        this.boundMappedToLastCycle = boundMapping;
538        return result;
539
540    }
541
542    /**
543     * Builds a list of ticks for the axis.  This method is called when the
544     * axis is at the left or right of the chart (so the axis is "vertical").
545     *
546     * @param g2  the graphics device.
547     * @param dataArea  the data area.
548     * @param edge  the edge.
549     *
550     * @return A list of ticks.
551     */
552    protected List refreshVerticalTicks(Graphics2D g2, Rectangle2D dataArea,
553            RectangleEdge edge) {
554
555        List result = new java.util.ArrayList();
556        result.clear();
557
558        Font tickLabelFont = getTickLabelFont();
559        g2.setFont(tickLabelFont);
560        if (isAutoTickUnitSelection()) {
561            selectAutoTickUnit(g2, dataArea, edge);
562        }
563
564        double unit = getTickUnit().getSize();
565        double cycleBound = getCycleBound();
566        double currentTickValue = Math.ceil(cycleBound / unit) * unit;
567        double upperValue = getRange().getUpperBound();
568        boolean cycled = false;
569
570        boolean boundMapping = this.boundMappedToLastCycle;
571        this.boundMappedToLastCycle = true;
572
573        NumberTick lastTick = null;
574        float lastY = 0.0f;
575
576        if (upperValue == cycleBound) {
577            currentTickValue = calculateLowestVisibleTickValue();
578            cycled = true;
579            this.boundMappedToLastCycle = true;
580        }
581
582        while (currentTickValue <= upperValue) {
583
584            // Cycle when necessary
585            boolean cyclenow = false;
586            if ((currentTickValue + unit > upperValue) && !cycled) {
587                cyclenow = true;
588            }
589
590            double yy = valueToJava2D(currentTickValue, dataArea, edge);
591            String tickLabel;
592            NumberFormat formatter = getNumberFormatOverride();
593            if (formatter != null) {
594                tickLabel = formatter.format(currentTickValue);
595            }
596            else {
597                tickLabel = getTickUnit().valueToString(currentTickValue);
598            }
599
600            float y = (float) yy;
601            TextAnchor anchor;
602            TextAnchor rotationAnchor;
603            double angle = 0.0;
604            if (isVerticalTickLabels()) {
605
606                if (edge == RectangleEdge.LEFT) {
607                    anchor = TextAnchor.BOTTOM_CENTER;
608                    if ((lastTick != null) && (lastY == y)
609                            && (currentTickValue != cycleBound)) {
610                        anchor = isInverted()
611                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
612                        result.remove(result.size() - 1);
613                        result.add(new CycleBoundTick(
614                            this.boundMappedToLastCycle, lastTick.getNumber(),
615                            lastTick.getText(), anchor, anchor,
616                            lastTick.getAngle())
617                        );
618                        this.internalMarkerWhenTicksOverlap = true;
619                        anchor = isInverted()
620                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
621                    }
622                    rotationAnchor = anchor;
623                    angle = -Math.PI / 2.0;
624                }
625                else {
626                    anchor = TextAnchor.BOTTOM_CENTER;
627                    if ((lastTick != null) && (lastY == y)
628                            && (currentTickValue != cycleBound)) {
629                        anchor = isInverted()
630                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
631                        result.remove(result.size() - 1);
632                        result.add(new CycleBoundTick(
633                            this.boundMappedToLastCycle, lastTick.getNumber(),
634                            lastTick.getText(), anchor, anchor,
635                            lastTick.getAngle())
636                        );
637                        this.internalMarkerWhenTicksOverlap = true;
638                        anchor = isInverted()
639                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
640                    }
641                    rotationAnchor = anchor;
642                    angle = Math.PI / 2.0;
643                }
644            }
645            else {
646                if (edge == RectangleEdge.LEFT) {
647                    anchor = TextAnchor.CENTER_RIGHT;
648                    if ((lastTick != null) && (lastY == y)
649                            && (currentTickValue != cycleBound)) {
650                        anchor = isInverted()
651                            ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
652                        result.remove(result.size() - 1);
653                        result.add(new CycleBoundTick(
654                            this.boundMappedToLastCycle, lastTick.getNumber(),
655                            lastTick.getText(), anchor, anchor,
656                            lastTick.getAngle())
657                        );
658                        this.internalMarkerWhenTicksOverlap = true;
659                        anchor = isInverted()
660                            ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
661                    }
662                    rotationAnchor = anchor;
663                }
664                else {
665                    anchor = TextAnchor.CENTER_LEFT;
666                    if ((lastTick != null) && (lastY == y)
667                            && (currentTickValue != cycleBound)) {
668                        anchor = isInverted()
669                            ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT;
670                        result.remove(result.size() - 1);
671                        result.add(new CycleBoundTick(
672                            this.boundMappedToLastCycle, lastTick.getNumber(),
673                            lastTick.getText(), anchor, anchor,
674                            lastTick.getAngle())
675                        );
676                        this.internalMarkerWhenTicksOverlap = true;
677                        anchor = isInverted()
678                            ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT;
679                    }
680                    rotationAnchor = anchor;
681                }
682            }
683
684            CycleBoundTick tick = new CycleBoundTick(
685                this.boundMappedToLastCycle, currentTickValue,
686                tickLabel, anchor, rotationAnchor, angle);
687            if (currentTickValue == cycleBound) {
688                this.internalMarkerCycleBoundTick = tick;
689            }
690            result.add(tick);
691            lastTick = tick;
692            lastY = y;
693
694            if (currentTickValue == cycleBound) {
695                this.internalMarkerCycleBoundTick = tick;
696            }
697
698            currentTickValue += unit;
699
700            if (cyclenow) {
701                currentTickValue = calculateLowestVisibleTickValue();
702                upperValue = cycleBound;
703                cycled = true;
704                this.boundMappedToLastCycle = false;
705            }
706
707        }
708        this.boundMappedToLastCycle = boundMapping;
709        return result;
710    }
711
712    /**
713     * Converts a coordinate from Java 2D space to data space.
714     *
715     * @param java2DValue  the coordinate in Java2D space.
716     * @param dataArea  the data area.
717     * @param edge  the edge.
718     *
719     * @return The data value.
720     */
721    @Override
722    public double java2DToValue(double java2DValue, Rectangle2D dataArea,
723            RectangleEdge edge) {
724        Range range = getRange();
725
726        double vmax = range.getUpperBound();
727        double vp = getCycleBound();
728
729        double jmin = 0.0;
730        double jmax = 0.0;
731        if (RectangleEdge.isTopOrBottom(edge)) {
732            jmin = dataArea.getMinX();
733            jmax = dataArea.getMaxX();
734        }
735        else if (RectangleEdge.isLeftOrRight(edge)) {
736            jmin = dataArea.getMaxY();
737            jmax = dataArea.getMinY();
738        }
739
740        if (isInverted()) {
741            double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period;
742            if (java2DValue >= jbreak) {
743                return vp + (jmax - java2DValue) * this.period / (jmax - jmin);
744            }
745            else {
746                return vp - (java2DValue - jmin) * this.period / (jmax - jmin);
747            }
748        }
749        else {
750            double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin;
751            if (java2DValue <= jbreak) {
752                return vp + (java2DValue - jmin) * this.period / (jmax - jmin);
753            }
754            else {
755                return vp - (jmax - java2DValue) * this.period / (jmax - jmin);
756            }
757        }
758    }
759
760    /**
761     * Translates a value from data space to Java 2D space.
762     *
763     * @param value  the data value.
764     * @param dataArea  the data area.
765     * @param edge  the edge.
766     *
767     * @return The Java 2D value.
768     */
769    @Override
770    public double valueToJava2D(double value, Rectangle2D dataArea,
771            RectangleEdge edge) {
772        Range range = getRange();
773
774        double vmin = range.getLowerBound();
775        double vmax = range.getUpperBound();
776        double vp = getCycleBound();
777
778        if ((value < vmin) || (value > vmax)) {
779            return Double.NaN;
780        }
781
782
783        double jmin = 0.0;
784        double jmax = 0.0;
785        if (RectangleEdge.isTopOrBottom(edge)) {
786            jmin = dataArea.getMinX();
787            jmax = dataArea.getMaxX();
788        }
789        else if (RectangleEdge.isLeftOrRight(edge)) {
790            jmax = dataArea.getMinY();
791            jmin = dataArea.getMaxY();
792        }
793
794        if (isInverted()) {
795            if (value == vp) {
796                return this.boundMappedToLastCycle ? jmin : jmax;
797            }
798            else if (value > vp) {
799                return jmax - (value - vp) * (jmax - jmin) / this.period;
800            }
801            else {
802                return jmin + (vp - value) * (jmax - jmin) / this.period;
803            }
804        }
805        else {
806            if (value == vp) {
807                return this.boundMappedToLastCycle ? jmax : jmin;
808            }
809            else if (value >= vp) {
810                return jmin + (value - vp) * (jmax - jmin) / this.period;
811            }
812            else {
813                return jmax - (vp - value) * (jmax - jmin) / this.period;
814            }
815        }
816    }
817
818    /**
819     * Centers the range about the given value.
820     *
821     * @param value  the data value.
822     */
823    @Override
824    public void centerRange(double value) {
825        setRange(value - this.period / 2.0, value + this.period / 2.0);
826    }
827
828    /**
829     * This function is nearly useless since the auto range is fixed for this
830     * class to the period.  The period is extended if necessary to fit the
831     * minimum size.
832     *
833     * @param size  the size.
834     * @param notify  notify?
835     *
836     * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double,
837     *      boolean)
838     */
839    @Override
840    public void setAutoRangeMinimumSize(double size, boolean notify) {
841        if (size > this.period) {
842            this.period = size;
843        }
844        super.setAutoRangeMinimumSize(size, notify);
845    }
846
847    /**
848     * The auto range is fixed for this class to the period by default.
849     * This function will thus set a new period.
850     *
851     * @param length  the length.
852     *
853     * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double)
854     */
855    @Override
856    public void setFixedAutoRange(double length) {
857        this.period = length;
858        super.setFixedAutoRange(length);
859    }
860
861    /**
862     * Sets a new axis range. The period is extended to fit the range size, if
863     * necessary.
864     *
865     * @param range  the range.
866     * @param turnOffAutoRange  switch off the auto range.
867     * @param notify notify?
868     *
869     * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean)
870     */
871    @Override
872    public void setRange(Range range, boolean turnOffAutoRange,
873            boolean notify) {
874        double size = range.getUpperBound() - range.getLowerBound();
875        if (size > this.period) {
876            this.period = size;
877        }
878        super.setRange(range, turnOffAutoRange, notify);
879    }
880
881    /**
882     * The cycle bound is defined as the higest value x such that
883     * "offset + period * i = x", with i and integer and x &lt;
884     * range.getUpperBound() This is the value which is at both ends of the
885     * axis :  x...up|low...x
886     * The values from x to up are the valued in the current cycle.
887     * The values from low to x are the valued in the previous cycle.
888     *
889     * @return The cycle bound.
890     */
891    public double getCycleBound() {
892        return Math.floor(
893            (getRange().getUpperBound() - this.offset) / this.period
894        ) * this.period + this.offset;
895    }
896
897    /**
898     * The cycle bound is a multiple of the period, plus optionally a start
899     * offset.
900     * <pre>cb = n * period + offset</pre>
901     *
902     * @return The current offset.
903     *
904     * @see #getCycleBound()
905     */
906    public double getOffset() {
907        return this.offset;
908    }
909
910    /**
911     * The cycle bound is a multiple of the period, plus optionally a start
912     * offset.
913     * <pre>cb = n * period + offset</pre>
914     *
915     * @param offset The offset to set.
916     *
917     * @see #getCycleBound()
918     */
919    public void setOffset(double offset) {
920        this.offset = offset;
921    }
922
923    /**
924     * The cycle bound is a multiple of the period, plus optionally a start
925     * offset.
926     * <pre>cb = n * period + offset</pre>
927     *
928     * @return The current period.
929     *
930     * @see #getCycleBound()
931     */
932    public double getPeriod() {
933        return this.period;
934    }
935
936    /**
937     * The cycle bound is a multiple of the period, plus optionally a start
938     * offset.
939     * <pre>cb = n * period + offset</pre>
940     *
941     * @param period The period to set.
942     *
943     * @see #getCycleBound()
944     */
945    public void setPeriod(double period) {
946        this.period = period;
947    }
948
949    /**
950     * Draws the tick marks and labels.
951     *
952     * @param g2  the graphics device.
953     * @param cursor  the cursor.
954     * @param plotArea  the plot area.
955     * @param dataArea  the area inside the axes.
956     * @param edge  the side on which the axis is displayed.
957     *
958     * @return The axis state.
959     */
960    @Override
961    protected AxisState drawTickMarksAndLabels(Graphics2D g2, double cursor,
962            Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge) {
963        this.internalMarkerWhenTicksOverlap = false;
964        AxisState ret = super.drawTickMarksAndLabels(g2, cursor, plotArea,
965                dataArea, edge);
966
967        // continue and separate the labels only if necessary
968        if (!this.internalMarkerWhenTicksOverlap) {
969            return ret;
970        }
971
972        double ol;
973        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
974        if (isVerticalTickLabels()) {
975            ol = fm.getMaxAdvance();
976        }
977        else {
978            ol = fm.getHeight();
979        }
980
981        double il = 0;
982        if (isTickMarksVisible()) {
983            float xx = (float) valueToJava2D(getRange().getUpperBound(),
984                    dataArea, edge);
985            Line2D mark = null;
986            g2.setStroke(getTickMarkStroke());
987            g2.setPaint(getTickMarkPaint());
988            if (edge == RectangleEdge.LEFT) {
989                mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
990            }
991            else if (edge == RectangleEdge.RIGHT) {
992                mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
993            }
994            else if (edge == RectangleEdge.TOP) {
995                mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
996            }
997            else if (edge == RectangleEdge.BOTTOM) {
998                mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
999            }
1000            g2.draw(mark);
1001        }
1002        return ret;
1003    }
1004
1005    /**
1006     * Draws the axis.
1007     *
1008     * @param g2  the graphics device ({@code null} not permitted).
1009     * @param cursor  the cursor position.
1010     * @param plotArea  the plot area ({@code null} not permitted).
1011     * @param dataArea  the data area ({@code null} not permitted).
1012     * @param edge  the edge ({@code null} not permitted).
1013     * @param plotState  collects information about the plot
1014     *                   ({@code null} permitted).
1015     *
1016     * @return The axis state (never {@code null}).
1017     */
1018    @Override
1019    public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
1020            Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState) {
1021
1022        AxisState ret = super.draw(g2, cursor, plotArea, dataArea, edge, 
1023                plotState);
1024        if (isAdvanceLineVisible()) {
1025            double xx = valueToJava2D(getRange().getUpperBound(), dataArea, 
1026                    edge);
1027            Line2D mark = null;
1028            g2.setStroke(getAdvanceLineStroke());
1029            g2.setPaint(getAdvanceLinePaint());
1030            if (edge == RectangleEdge.LEFT) {
1031                mark = new Line2D.Double(cursor, xx, cursor 
1032                        + dataArea.getWidth(), xx);
1033            }
1034            else if (edge == RectangleEdge.RIGHT) {
1035                mark = new Line2D.Double(cursor - dataArea.getWidth(), xx, 
1036                        cursor, xx);
1037            }
1038            else if (edge == RectangleEdge.TOP) {
1039                mark = new Line2D.Double(xx, cursor + dataArea.getHeight(), xx, 
1040                        cursor);
1041            }
1042            else if (edge == RectangleEdge.BOTTOM) {
1043                mark = new Line2D.Double(xx, cursor, xx, 
1044                        cursor - dataArea.getHeight());
1045            }
1046            g2.draw(mark);
1047        }
1048        return ret;
1049    }
1050
1051    /**
1052     * Reserve some space on each axis side because we draw a centered label at
1053     * each extremity.
1054     *
1055     * @param g2  the graphics device.
1056     * @param plot  the plot.
1057     * @param plotArea  the plot area.
1058     * @param edge  the edge.
1059     * @param space  the space already reserved.
1060     *
1061     * @return The reserved space.
1062     */
1063    @Override
1064    public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
1065            Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
1066
1067        this.internalMarkerCycleBoundTick = null;
1068        AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
1069        if (this.internalMarkerCycleBoundTick == null) {
1070            return ret;
1071        }
1072
1073        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
1074        Rectangle2D r = TextUtils.getTextBounds(
1075            this.internalMarkerCycleBoundTick.getText(), g2, fm
1076        );
1077
1078        if (RectangleEdge.isTopOrBottom(edge)) {
1079            if (isVerticalTickLabels()) {
1080                space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
1081            }
1082            else {
1083                space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
1084            }
1085        }
1086        else if (RectangleEdge.isLeftOrRight(edge)) {
1087            if (isVerticalTickLabels()) {
1088                space.add(r.getWidth() / 2, RectangleEdge.TOP);
1089            }
1090            else {
1091                space.add(r.getHeight() / 2, RectangleEdge.TOP);
1092            }
1093        }
1094
1095        return ret;
1096
1097    }
1098
1099    /**
1100     * Provides serialization support.
1101     *
1102     * @param stream  the output stream.
1103     *
1104     * @throws IOException  if there is an I/O error.
1105     */
1106    private void writeObject(ObjectOutputStream stream) throws IOException {
1107        stream.defaultWriteObject();
1108        SerialUtils.writePaint(this.advanceLinePaint, stream);
1109        SerialUtils.writeStroke(this.advanceLineStroke, stream);
1110    }
1111
1112    /**
1113     * Provides serialization support.
1114     *
1115     * @param stream  the input stream.
1116     *
1117     * @throws IOException  if there is an I/O error.
1118     * @throws ClassNotFoundException  if there is a classpath problem.
1119     */
1120    private void readObject(ObjectInputStream stream)
1121            throws IOException, ClassNotFoundException {
1122        stream.defaultReadObject();
1123        this.advanceLinePaint = SerialUtils.readPaint(stream);
1124        this.advanceLineStroke = SerialUtils.readStroke(stream);
1125    }
1126
1127
1128    /**
1129     * Tests the axis for equality with another object.
1130     *
1131     * @param obj  the object to test against.
1132     *
1133     * @return A boolean.
1134     */
1135    @Override
1136    public boolean equals(Object obj) {
1137        if (obj == this) {
1138            return true;
1139        }
1140        if (!(obj instanceof CyclicNumberAxis)) {
1141            return false;
1142        }
1143        if (!super.equals(obj)) {
1144            return false;
1145        }
1146        CyclicNumberAxis that = (CyclicNumberAxis) obj;
1147        if (this.period != that.period) {
1148            return false;
1149        }
1150        if (this.offset != that.offset) {
1151            return false;
1152        }
1153        if (!PaintUtils.equal(this.advanceLinePaint,
1154                that.advanceLinePaint)) {
1155            return false;
1156        }
1157        if (!Objects.equals(this.advanceLineStroke, that.advanceLineStroke)) {
1158            return false;
1159        }
1160        if (this.advanceLineVisible != that.advanceLineVisible) {
1161            return false;
1162        }
1163        if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) {
1164            return false;
1165        }
1166        return true;
1167    }
1168}