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