001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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-2007, 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, 
986                                               Rectangle2D dataArea, 
987                                               RectangleEdge edge) {
988        this.internalMarkerWhenTicksOverlap = false;
989        AxisState ret = super.drawTickMarksAndLabels(
990            g2, cursor, plotArea, dataArea, edge
991        );
992        
993        // continue and separate the labels only if necessary
994        if (!this.internalMarkerWhenTicksOverlap) {
995            return ret;
996        }
997        
998        double ol = getTickMarkOutsideLength();
999        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
1000        
1001        if (isVerticalTickLabels()) {
1002            ol = fm.getMaxAdvance(); 
1003        }
1004        else {
1005            ol = fm.getHeight();
1006        }
1007        
1008        double il = 0;
1009        if (isTickMarksVisible()) {
1010            float xx = (float) valueToJava2D(
1011                getRange().getUpperBound(), dataArea, edge
1012            );
1013            Line2D mark = null;
1014            g2.setStroke(getTickMarkStroke());
1015            g2.setPaint(getTickMarkPaint());
1016            if (edge == RectangleEdge.LEFT) {
1017                mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
1018            }
1019            else if (edge == RectangleEdge.RIGHT) {
1020                mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
1021            }
1022            else if (edge == RectangleEdge.TOP) {
1023                mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
1024            }
1025            else if (edge == RectangleEdge.BOTTOM) {
1026                mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
1027            }
1028            g2.draw(mark);
1029        }
1030        return ret;
1031    }
1032    
1033    /**
1034     * Draws the axis.
1035     * 
1036     * @param g2  the graphics device (<code>null</code> not permitted).
1037     * @param cursor  the cursor position.
1038     * @param plotArea  the plot area (<code>null</code> not permitted).
1039     * @param dataArea  the data area (<code>null</code> not permitted).
1040     * @param edge  the edge (<code>null</code> not permitted).
1041     * @param plotState  collects information about the plot 
1042     *                   (<code>null</code> permitted).
1043     * 
1044     * @return The axis state (never <code>null</code>).
1045     */
1046    public AxisState draw(Graphics2D g2, 
1047                          double cursor,
1048                          Rectangle2D plotArea, 
1049                          Rectangle2D dataArea, 
1050                          RectangleEdge edge,
1051                          PlotRenderingInfo plotState) {
1052        
1053        AxisState ret = super.draw(
1054            g2, cursor, plotArea, dataArea, edge, plotState
1055        );
1056        if (isAdvanceLineVisible()) {
1057            double xx = valueToJava2D(
1058                getRange().getUpperBound(), dataArea, edge
1059            );
1060            Line2D mark = null;
1061            g2.setStroke(getAdvanceLineStroke());
1062            g2.setPaint(getAdvanceLinePaint());
1063            if (edge == RectangleEdge.LEFT) {
1064                mark = new Line2D.Double(
1065                    cursor, xx, cursor + dataArea.getWidth(), xx
1066                );
1067            }
1068            else if (edge == RectangleEdge.RIGHT) {
1069                mark = new Line2D.Double(
1070                    cursor - dataArea.getWidth(), xx, cursor, xx
1071                );
1072            }
1073            else if (edge == RectangleEdge.TOP) {
1074                mark = new Line2D.Double(
1075                    xx, cursor + dataArea.getHeight(), xx, cursor
1076                );
1077            }
1078            else if (edge == RectangleEdge.BOTTOM) {
1079                mark = new Line2D.Double(
1080                    xx, cursor, xx, cursor - dataArea.getHeight()
1081                );
1082            }
1083            g2.draw(mark);
1084        }
1085        return ret;
1086    }
1087
1088    /**
1089     * Reserve some space on each axis side because we draw a centered label at
1090     * each extremity. 
1091     * 
1092     * @param g2  the graphics device.
1093     * @param plot  the plot.
1094     * @param plotArea  the plot area.
1095     * @param edge  the edge.
1096     * @param space  the space already reserved.
1097     * 
1098     * @return The reserved space.
1099     */
1100    public AxisSpace reserveSpace(Graphics2D g2, 
1101                                  Plot plot, 
1102                                  Rectangle2D plotArea, 
1103                                  RectangleEdge edge, 
1104                                  AxisSpace space) {
1105        
1106        this.internalMarkerCycleBoundTick = null;
1107        AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
1108        if (this.internalMarkerCycleBoundTick == null) {
1109            return ret;
1110        }
1111
1112        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
1113        Rectangle2D r = TextUtilities.getTextBounds(
1114            this.internalMarkerCycleBoundTick.getText(), g2, fm
1115        );
1116
1117        if (RectangleEdge.isTopOrBottom(edge)) {
1118            if (isVerticalTickLabels()) {
1119                space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
1120            }
1121            else {
1122                space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
1123            }
1124        }
1125        else if (RectangleEdge.isLeftOrRight(edge)) {
1126            if (isVerticalTickLabels()) {
1127                space.add(r.getWidth() / 2, RectangleEdge.TOP);
1128            }
1129            else {
1130                space.add(r.getHeight() / 2, RectangleEdge.TOP);
1131            }
1132        }
1133        
1134        return ret;
1135        
1136    }
1137
1138    /**
1139     * Provides serialization support.
1140     *
1141     * @param stream  the output stream.
1142     *
1143     * @throws IOException  if there is an I/O error.
1144     */
1145    private void writeObject(ObjectOutputStream stream) throws IOException {
1146    
1147        stream.defaultWriteObject();
1148        SerialUtilities.writePaint(this.advanceLinePaint, stream);
1149        SerialUtilities.writeStroke(this.advanceLineStroke, stream);
1150    
1151    }
1152    
1153    /**
1154     * Provides serialization support.
1155     *
1156     * @param stream  the input stream.
1157     *
1158     * @throws IOException  if there is an I/O error.
1159     * @throws ClassNotFoundException  if there is a classpath problem.
1160     */
1161    private void readObject(ObjectInputStream stream) 
1162        throws IOException, ClassNotFoundException {
1163    
1164        stream.defaultReadObject();
1165        this.advanceLinePaint = SerialUtilities.readPaint(stream);
1166        this.advanceLineStroke = SerialUtilities.readStroke(stream);
1167    
1168    }
1169     
1170    
1171    /**
1172     * Tests the axis for equality with another object.
1173     * 
1174     * @param obj  the object to test against.
1175     * 
1176     * @return A boolean.
1177     */
1178    public boolean equals(Object obj) {
1179        if (obj == this) {
1180            return true;
1181        }
1182        if (!(obj instanceof CyclicNumberAxis)) {
1183            return false;
1184        }
1185        if (!super.equals(obj)) {
1186            return false;
1187        }
1188        CyclicNumberAxis that = (CyclicNumberAxis) obj;      
1189        if (this.period != that.period) {
1190            return false;
1191        }
1192        if (this.offset != that.offset) {
1193            return false;
1194        }
1195        if (!PaintUtilities.equal(this.advanceLinePaint, 
1196                that.advanceLinePaint)) {
1197            return false;
1198        }
1199        if (!ObjectUtilities.equal(this.advanceLineStroke, 
1200                that.advanceLineStroke)) {
1201            return false;
1202        }
1203        if (this.advanceLineVisible != that.advanceLineVisible) {
1204            return false;
1205        }
1206        if (this.boundMappedToLastCycle != that.boundMappedToLastCycle) {
1207            return false;
1208        }
1209        return true;
1210    }
1211}