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