KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > jfree > chart > axis > CyclicNumberAxis


1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
6  *
7  * Project Info: http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public License
20  * along with this library; if not, write to the Free Software Foundation,
21  * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22  *
23  * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
24  * in the United States and other countries.]
25  *
26  * ---------------------
27  * CyclicNumberAxis.java
28  * ---------------------
29  * (C) Copyright 2003, 2004, by Nicolas Brodu and Contributors.
30  *
31  * Original Author: Nicolas Brodu;
32  * Contributor(s): David Gilbert (for Object Refinery Limited);
33  *
34  * $Id: CyclicNumberAxis.java,v 1.10 2005/05/06 14:55:43 mungady Exp $
35  *
36  * Changes
37  * -------
38  * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
39  * 16-Mar-2004 : Added plotState to draw() method (DG);
40  * 07-Apr-2004 : Modifed text bounds calculation (DG);
41  * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
42  * argument in selectAutoTickUnit() (DG);
43  * 22-Apr-2005 : Renamed refreshHorizontalTicks() --> refreshTicksHorizontal
44  * (for consistency with other classes) and removed unused
45  * parameters (DG);
46  *
47  */

48
49 package org.jfree.chart.axis;
50
51 import java.awt.BasicStroke JavaDoc;
52 import java.awt.Color JavaDoc;
53 import java.awt.Font JavaDoc;
54 import java.awt.FontMetrics JavaDoc;
55 import java.awt.Graphics2D JavaDoc;
56 import java.awt.Paint JavaDoc;
57 import java.awt.Stroke JavaDoc;
58 import java.awt.geom.Line2D JavaDoc;
59 import java.awt.geom.Rectangle2D JavaDoc;
60 import java.io.IOException JavaDoc;
61 import java.io.ObjectInputStream JavaDoc;
62 import java.io.ObjectOutputStream JavaDoc;
63 import java.text.NumberFormat JavaDoc;
64 import java.util.List JavaDoc;
65
66 import org.jfree.chart.plot.Plot;
67 import org.jfree.chart.plot.PlotRenderingInfo;
68 import org.jfree.data.Range;
69 import org.jfree.io.SerialUtilities;
70 import org.jfree.text.TextUtilities;
71 import org.jfree.ui.RectangleEdge;
72 import org.jfree.ui.TextAnchor;
73 import org.jfree.util.ObjectUtilities;
74
75 /**
76 This class extends NumberAxis and handles cycling.
77  
78 Traditional representation of data in the range x0..x1
79 <pre>
80 |-------------------------|
81 x0 x1
82 </pre>
83
84 Here, the range bounds are at the axis extremities.
85 With cyclic axis, however, the time is split in
86 "cycles", or "time frames", or the same duration : the period.
87
88 A cycle axis cannot by definition handle a larger interval
89 than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
90 period can be represented with such an axis.
91
92 The cycle bound is the number between x0 and x1 which marks
93 the beginning of new time frame:
94 <pre>
95 |---------------------|----------------------------|
96 x0 cb x1
97 <---previous cycle---><-------current cycle-------->
98 </pre>
99
100 It is actually a multiple of the period, plus optionally
101 a start offset: <pre>cb = n * period + offset</pre>
102
103 Thus, by definition, two consecutive cycle bounds
104 period apart, which is precisely why it is called a
105 period.
106
107 The visual representation of a cyclic axis is like that:
108 <pre>
109 |----------------------------|---------------------|
110 cb x1|x0 cb
111 <-------current cycle--------><---previous cycle--->
112 </pre>
113
114 The cycle bound is at the axis ends, then current
115 cycle is shown, then the last cycle. When using
116 dynamic data, the visual effect is the current cycle
117 erases the last cycle as x grows. Then, the next cycle
118 bound is reached, and the process starts over, erasing
119 the previous cycle.
120
121 A Cyclic item renderer is provided to do exactly this.
122
123  */

124 public class CyclicNumberAxis extends NumberAxis {
125
126     /** The default axis line stroke. */
127     public static Stroke JavaDoc DEFAULT_ADVANCE_LINE_STROKE = new BasicStroke JavaDoc(1.0f);
128     
129     /** The default axis line paint. */
130     public static final Paint JavaDoc DEFAULT_ADVANCE_LINE_PAINT = Color.gray;
131     
132     /** The offset. */
133     protected double offset;
134     
135     /** The period.*/
136     protected double period;
137     
138     /** ??. */
139     protected boolean boundMappedToLastCycle;
140     
141     /** A flag that controls whether or not the advance line is visible. */
142     protected boolean advanceLineVisible;
143
144     /** The advance line stroke. */
145     protected transient Stroke JavaDoc advanceLineStroke = DEFAULT_ADVANCE_LINE_STROKE;
146     
147     /** The advance line paint. */
148     protected transient Paint JavaDoc advanceLinePaint;
149     
150     private transient boolean internalMarkerWhenTicksOverlap;
151     private transient Tick internalMarkerCycleBoundTick;
152     
153     /**
154      * Creates a CycleNumberAxis with the given period.
155      *
156      * @param period the period.
157      */

158     public CyclicNumberAxis(double period) {
159         this(period, 0.0);
160     }
161
162     /**
163      * Creates a CycleNumberAxis with the given period and offset.
164      *
165      * @param period the period.
166      * @param offset the offset.
167      */

168     public CyclicNumberAxis(double period, double offset) {
169         this(period, offset, null);
170     }
171
172     /**
173      * Creates a named CycleNumberAxis with the given period.
174      *
175      * @param period the period.
176      * @param label the label.
177      */

178     public CyclicNumberAxis(double period, String JavaDoc label) {
179         this(0, period, label);
180     }
181     
182     /**
183      * Creates a named CycleNumberAxis with the given period and offset.
184      *
185      * @param period the period.
186      * @param offset the offset.
187      * @param label the label.
188      */

189     public CyclicNumberAxis(double period, double offset, String JavaDoc label) {
190         super(label);
191         this.period = period;
192         this.offset = offset;
193         setFixedAutoRange(period);
194         this.advanceLineVisible = true;
195         this.advanceLinePaint = DEFAULT_ADVANCE_LINE_PAINT;
196     }
197         
198     /**
199      * The advance line is the line drawn at the limit of the current cycle,
200      * when erasing the previous cycle.
201      *
202      * @return A boolean.
203      */

204     public boolean isAdvanceLineVisible() {
205         return this.advanceLineVisible;
206     }
207     
208     /**
209      * The advance line is the line drawn at the limit of the current cycle,
210      * when erasing the previous cycle.
211      *
212      * @param visible the flag.
213      */

214     public void setAdvanceLineVisible(boolean visible) {
215         this.advanceLineVisible = visible;
216     }
217     
218     /**
219      * The advance line is the line drawn at the limit of the current cycle,
220      * when erasing the previous cycle.
221      *
222      * @return The paint (never <code>null</code>).
223      */

224     public Paint JavaDoc getAdvanceLinePaint() {
225         return this.advanceLinePaint;
226     }
227
228     /**
229      * The advance line is the line drawn at the limit of the current cycle,
230      * when erasing the previous cycle.
231      *
232      * @param paint the paint (<code>null</code> not permitted).
233      */

234     public void setAdvanceLinePaint(Paint JavaDoc paint) {
235         if (paint == null) {
236             throw new IllegalArgumentException JavaDoc("Null 'paint' argument.");
237         }
238         this.advanceLinePaint = paint;
239     }
240     
241     /**
242      * The advance line is the line drawn at the limit of the current cycle,
243      * when erasing the previous cycle.
244      *
245      * @return The stroke (never <code>null</code>).
246      */

247     public Stroke JavaDoc getAdvanceLineStroke() {
248         return this.advanceLineStroke;
249     }
250     /**
251      * The advance line is the line drawn at the limit of the current cycle,
252      * when erasing the previous cycle.
253      *
254      * @param stroke the stroke (<code>null</code> not permitted).
255      */

256     public void setAdvanceLineStroke(Stroke JavaDoc stroke) {
257         if (stroke == null) {
258             throw new IllegalArgumentException JavaDoc("Null 'stroke' argument.");
259         }
260         this.advanceLineStroke = stroke;
261     }
262     
263     /**
264      * The cycle bound can be associated either with the current or with the
265      * last cycle. It's up to the user's choice to decide which, as this is
266      * just a convention. By default, the cycle bound is mapped to the current
267      * cycle.
268      * <br>
269      * Note that this has no effect on visual appearance, as the cycle bound is
270      * mapped successively for both axis ends. Use this function for correct
271      * results in translateValueToJava2D.
272      *
273      * @return <code>true</code> if the cycle bound is mapped to the last
274      * cycle, <code>false</code> if it is bound to the current cycle
275      * (default)
276      */

277     public boolean isBoundMappedToLastCycle() {
278         return this.boundMappedToLastCycle;
279     }
280     
281     /**
282      * The cycle bound can be associated either with the current or with the
283      * last cycle. It's up to the user's choice to decide which, as this is
284      * just a convention. By default, the cycle bound is mapped to the current
285      * cycle.
286      * <br>
287      * Note that this has no effect on visual appearance, as the cycle bound is
288      * mapped successively for both axis ends. Use this function for correct
289      * results in valueToJava2D.
290      *
291      * @param boundMappedToLastCycle Set it to true to map the cycle bound to
292      * the last cycle.
293      */

294     public void setBoundMappedToLastCycle(boolean boundMappedToLastCycle) {
295         this.boundMappedToLastCycle = boundMappedToLastCycle;
296     }
297     
298     /**
299      * Selects a tick unit when the axis is displayed horizontally.
300      *
301      * @param g2 the graphics device.
302      * @param drawArea the drawing area.
303      * @param dataArea the data area.
304      * @param edge the side of the rectangle on which the axis is displayed.
305      */

306     protected void selectHorizontalAutoTickUnit(Graphics2D JavaDoc g2,
307                                                 Rectangle2D JavaDoc drawArea,
308                                                 Rectangle2D JavaDoc dataArea,
309                                                 RectangleEdge edge) {
310
311         double tickLabelWidth
312             = estimateMaximumTickLabelWidth(g2, getTickUnit());
313         
314         // Compute number of labels
315
double n = getRange().getLength()
316                    * tickLabelWidth / dataArea.getWidth();
317
318         setTickUnit(
319             (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n),
320             false, false
321         );
322         
323      }
324
325     /**
326      * Selects a tick unit when the axis is displayed vertically.
327      *
328      * @param g2 the graphics device.
329      * @param drawArea the drawing area.
330      * @param dataArea the data area.
331      * @param edge the side of the rectangle on which the axis is displayed.
332      */

333     protected void selectVerticalAutoTickUnit(Graphics2D JavaDoc g2,
334                                                 Rectangle2D JavaDoc drawArea,
335                                                 Rectangle2D JavaDoc dataArea,
336                                                 RectangleEdge edge) {
337
338         double tickLabelWidth
339             = estimateMaximumTickLabelWidth(g2, getTickUnit());
340
341         // Compute number of labels
342
double n = getRange().getLength()
343                    * tickLabelWidth / dataArea.getHeight();
344
345         setTickUnit(
346             (NumberTickUnit) getStandardTickUnits().getCeilingTickUnit(n),
347             false, false
348         );
349         
350      }
351
352     /**
353      * A special Number tick that also hold information about the cycle bound
354      * mapping for this tick. This is especially useful for having a tick at
355      * each axis end with the cycle bound value. See also
356      * isBoundMappedToLastCycle()
357      */

358     protected static class CycleBoundTick extends NumberTick {
359         
360         /** Map to last cycle. */
361         public boolean mapToLastCycle;
362         
363         /**
364          * Creates a new tick.
365          *
366          * @param mapToLastCycle map to last cycle?
367          * @param number the number.
368          * @param label the label.
369          * @param textAnchor the text anchor.
370          * @param rotationAnchor the rotation anchor.
371          * @param angle the rotation angle.
372          */

373         public CycleBoundTick(boolean mapToLastCycle, Number JavaDoc number,
374                               String JavaDoc label, TextAnchor textAnchor,
375                               TextAnchor rotationAnchor, double angle) {
376             super(number, label, textAnchor, rotationAnchor, angle);
377             this.mapToLastCycle = mapToLastCycle;
378         }
379     }
380     
381     /**
382      * Calculates the anchor point for a tick.
383      *
384      * @param tick the tick.
385      * @param cursor the cursor.
386      * @param dataArea the data area.
387      * @param edge the side on which the axis is displayed.
388      *
389      * @return The anchor point.
390      */

391     protected float[] calculateAnchorPoint(ValueTick tick, double cursor,
392                                            Rectangle2D JavaDoc dataArea,
393                                            RectangleEdge edge) {
394         if (tick instanceof CycleBoundTick) {
395             boolean mapsav = this.boundMappedToLastCycle;
396             this.boundMappedToLastCycle
397                 = ((CycleBoundTick) tick).mapToLastCycle;
398             float[] ret = super.calculateAnchorPoint(
399                 tick, cursor, dataArea, edge
400             );
401             this.boundMappedToLastCycle = mapsav;
402             return ret;
403         }
404         return super.calculateAnchorPoint(tick, cursor, dataArea, edge);
405     }
406     
407     
408     
409     /**
410      * Builds a list of ticks for the axis. This method is called when the
411      * axis is at the top or bottom of the chart (so the axis is "horizontal").
412      *
413      * @param g2 the graphics device.
414      * @param dataArea the data area.
415      * @param edge the edge.
416      *
417      * @return A list of ticks.
418      */

419     protected List JavaDoc refreshTicksHorizontal(Graphics2D JavaDoc g2,
420                                           Rectangle2D JavaDoc dataArea,
421                                           RectangleEdge edge) {
422
423         List JavaDoc result = new java.util.ArrayList JavaDoc();
424
425         Font JavaDoc tickLabelFont = getTickLabelFont();
426         g2.setFont(tickLabelFont);
427         
428         if (isAutoTickUnitSelection()) {
429             selectAutoTickUnit(g2, dataArea, edge);
430         }
431
432         double unit = getTickUnit().getSize();
433         double cycleBound = getCycleBound();
434         double currentTickValue = Math.ceil(cycleBound / unit) * unit;
435         double upperValue = getRange().getUpperBound();
436         boolean cycled = false;
437
438         boolean boundMapping = this.boundMappedToLastCycle;
439         this.boundMappedToLastCycle = false;
440         
441         CycleBoundTick lastTick = null;
442         float lastX = 0.0f;
443
444         if (upperValue == cycleBound) {
445             currentTickValue = calculateLowestVisibleTickValue();
446             cycled = true;
447             this.boundMappedToLastCycle = true;
448         }
449         
450         while (currentTickValue <= upperValue) {
451             
452             // Cycle when necessary
453
boolean cyclenow = false;
454             if ((currentTickValue + unit > upperValue) && !cycled) {
455                 cyclenow = true;
456             }
457             
458             double xx = valueToJava2D(currentTickValue, dataArea, edge);
459             String JavaDoc tickLabel;
460             NumberFormat JavaDoc formatter = getNumberFormatOverride();
461             if (formatter != null) {
462                 tickLabel = formatter.format(currentTickValue);
463             }
464             else {
465                 tickLabel = getTickUnit().valueToString(currentTickValue);
466             }
467             float x = (float) xx;
468             TextAnchor anchor = null;
469             TextAnchor rotationAnchor = null;
470             double angle = 0.0;
471             if (isVerticalTickLabels()) {
472                 if (edge == RectangleEdge.TOP) {
473                     angle = Math.PI / 2.0;
474                 }
475                 else {
476                     angle = -Math.PI / 2.0;
477                 }
478                 anchor = TextAnchor.CENTER_RIGHT;
479                 // If tick overlap when cycling, update last tick too
480
if ((lastTick != null) && (lastX == x)
481                         && (currentTickValue != cycleBound)) {
482                     anchor = isInverted()
483                         ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
484                     result.remove(result.size() - 1);
485                     result.add(new CycleBoundTick(
486                         this.boundMappedToLastCycle, lastTick.getNumber(),
487                         lastTick.getText(), anchor, anchor,
488                         lastTick.getAngle())
489                     );
490                     this.internalMarkerWhenTicksOverlap = true;
491                     anchor = isInverted()
492                         ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
493                 }
494                 rotationAnchor = anchor;
495             }
496             else {
497                 if (edge == RectangleEdge.TOP) {
498                     anchor = TextAnchor.BOTTOM_CENTER;
499                     if ((lastTick != null) && (lastX == x)
500                             && (currentTickValue != cycleBound)) {
501                         anchor = isInverted()
502                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
503                         result.remove(result.size() - 1);
504                         result.add(new CycleBoundTick(
505                             this.boundMappedToLastCycle, lastTick.getNumber(),
506                             lastTick.getText(), anchor, anchor,
507                             lastTick.getAngle())
508                         );
509                         this.internalMarkerWhenTicksOverlap = true;
510                         anchor = isInverted()
511                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
512                     }
513                     rotationAnchor = anchor;
514                 }
515                 else {
516                     anchor = TextAnchor.TOP_CENTER;
517                     if ((lastTick != null) && (lastX == x)
518                             && (currentTickValue != cycleBound)) {
519                         anchor = isInverted()
520                             ? TextAnchor.TOP_LEFT : TextAnchor.TOP_RIGHT;
521                         result.remove(result.size() - 1);
522                         result.add(new CycleBoundTick(
523                             this.boundMappedToLastCycle, lastTick.getNumber(),
524                             lastTick.getText(), anchor, anchor,
525                             lastTick.getAngle())
526                         );
527                         this.internalMarkerWhenTicksOverlap = true;
528                         anchor = isInverted()
529                             ? TextAnchor.TOP_RIGHT : TextAnchor.TOP_LEFT;
530                     }
531                     rotationAnchor = anchor;
532                 }
533             }
534
535             CycleBoundTick tick = new CycleBoundTick(
536                 this.boundMappedToLastCycle,
537                 new Double JavaDoc(currentTickValue), tickLabel, anchor,
538                 rotationAnchor, angle
539             );
540             if (currentTickValue == cycleBound) {
541                 this.internalMarkerCycleBoundTick = tick;
542             }
543             result.add(tick);
544             lastTick = tick;
545             lastX = x;
546             
547             currentTickValue += unit;
548             
549             if (cyclenow) {
550                 currentTickValue = calculateLowestVisibleTickValue();
551                 upperValue = cycleBound;
552                 cycled = true;
553                 this.boundMappedToLastCycle = true;
554             }
555
556         }
557         this.boundMappedToLastCycle = boundMapping;
558         return result;
559         
560     }
561
562     /**
563      * Builds a list of ticks for the axis. This method is called when the
564      * axis is at the left or right of the chart (so the axis is "vertical").
565      *
566      * @param g2 the graphics device.
567      * @param dataArea the data area.
568      * @param edge the edge.
569      *
570      * @return A list of ticks.
571      */

572     protected List JavaDoc refreshVerticalTicks(Graphics2D JavaDoc g2,
573                                         Rectangle2D JavaDoc dataArea,
574                                         RectangleEdge edge) {
575         
576         List JavaDoc result = new java.util.ArrayList JavaDoc();
577         result.clear();
578
579         Font JavaDoc tickLabelFont = getTickLabelFont();
580         g2.setFont(tickLabelFont);
581         if (isAutoTickUnitSelection()) {
582             selectAutoTickUnit(g2, dataArea, edge);
583         }
584
585         double unit = getTickUnit().getSize();
586         double cycleBound = getCycleBound();
587         double currentTickValue = Math.ceil(cycleBound / unit) * unit;
588         double upperValue = getRange().getUpperBound();
589         boolean cycled = false;
590
591         boolean boundMapping = this.boundMappedToLastCycle;
592         this.boundMappedToLastCycle = true;
593
594         NumberTick lastTick = null;
595         float lastY = 0.0f;
596
597         if (upperValue == cycleBound) {
598             currentTickValue = calculateLowestVisibleTickValue();
599             cycled = true;
600             this.boundMappedToLastCycle = true;
601         }
602         
603         while (currentTickValue <= upperValue) {
604             
605             // Cycle when necessary
606
boolean cyclenow = false;
607             if ((currentTickValue + unit > upperValue) && !cycled) {
608                 cyclenow = true;
609             }
610
611             double yy = valueToJava2D(currentTickValue, dataArea, edge);
612             String JavaDoc tickLabel;
613             NumberFormat JavaDoc formatter = getNumberFormatOverride();
614             if (formatter != null) {
615                 tickLabel = formatter.format(currentTickValue);
616             }
617             else {
618                 tickLabel = getTickUnit().valueToString(currentTickValue);
619             }
620
621             float y = (float) yy;
622             TextAnchor anchor = null;
623             TextAnchor rotationAnchor = null;
624             double angle = 0.0;
625             if (isVerticalTickLabels()) {
626
627                 if (edge == RectangleEdge.LEFT) {
628                     anchor = TextAnchor.BOTTOM_CENTER;
629                     if ((lastTick != null) && (lastY == y)
630                             && (currentTickValue != cycleBound)) {
631                         anchor = isInverted()
632                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
633                         result.remove(result.size() - 1);
634                         result.add(new CycleBoundTick(
635                             this.boundMappedToLastCycle, lastTick.getNumber(),
636                             lastTick.getText(), anchor, anchor,
637                             lastTick.getAngle())
638                         );
639                         this.internalMarkerWhenTicksOverlap = true;
640                         anchor = isInverted()
641                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
642                     }
643                     rotationAnchor = anchor;
644                     angle = -Math.PI / 2.0;
645                 }
646                 else {
647                     anchor = TextAnchor.BOTTOM_CENTER;
648                     if ((lastTick != null) && (lastY == y)
649                             && (currentTickValue != cycleBound)) {
650                         anchor = isInverted()
651                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.BOTTOM_LEFT;
652                         result.remove(result.size() - 1);
653                         result.add(new CycleBoundTick(
654                             this.boundMappedToLastCycle, lastTick.getNumber(),
655                             lastTick.getText(), anchor, anchor,
656                             lastTick.getAngle())
657                         );
658                         this.internalMarkerWhenTicksOverlap = true;
659                         anchor = isInverted()
660                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.BOTTOM_RIGHT;
661                     }
662                     rotationAnchor = anchor;
663                     angle = Math.PI / 2.0;
664                 }
665             }
666             else {
667                 if (edge == RectangleEdge.LEFT) {
668                     anchor = TextAnchor.CENTER_RIGHT;
669                     if ((lastTick != null) && (lastY == y)
670                             && (currentTickValue != cycleBound)) {
671                         anchor = isInverted()
672                             ? TextAnchor.BOTTOM_RIGHT : TextAnchor.TOP_RIGHT;
673                         result.remove(result.size() - 1);
674                         result.add(new CycleBoundTick(
675                             this.boundMappedToLastCycle, lastTick.getNumber(),
676                             lastTick.getText(), anchor, anchor,
677                             lastTick.getAngle())
678                         );
679                         this.internalMarkerWhenTicksOverlap = true;
680                         anchor = isInverted()
681                             ? TextAnchor.TOP_RIGHT : TextAnchor.BOTTOM_RIGHT;
682                     }
683                     rotationAnchor = anchor;
684                 }
685                 else {
686                     anchor = TextAnchor.CENTER_LEFT;
687                     if ((lastTick != null) && (lastY == y)
688                             && (currentTickValue != cycleBound)) {
689                         anchor = isInverted()
690                             ? TextAnchor.BOTTOM_LEFT : TextAnchor.TOP_LEFT;
691                         result.remove(result.size() - 1);
692                         result.add(new CycleBoundTick(
693                             this.boundMappedToLastCycle, lastTick.getNumber(),
694                             lastTick.getText(), anchor, anchor,
695                             lastTick.getAngle())
696                         );
697                         this.internalMarkerWhenTicksOverlap = true;
698                         anchor = isInverted()
699                             ? TextAnchor.TOP_LEFT : TextAnchor.BOTTOM_LEFT;
700                     }
701                     rotationAnchor = anchor;
702                 }
703             }
704
705             CycleBoundTick tick = new CycleBoundTick(
706                 this.boundMappedToLastCycle, new Double JavaDoc(currentTickValue),
707                 tickLabel, anchor, rotationAnchor, angle
708             );
709             if (currentTickValue == cycleBound) {
710                 this.internalMarkerCycleBoundTick = tick;
711             }
712             result.add(tick);
713             lastTick = tick;
714             lastY = y;
715             
716             if (currentTickValue == cycleBound) {
717                 this.internalMarkerCycleBoundTick = tick;
718             }
719
720             currentTickValue += unit;
721             
722             if (cyclenow) {
723                 currentTickValue = calculateLowestVisibleTickValue();
724                 upperValue = cycleBound;
725                 cycled = true;
726                 this.boundMappedToLastCycle = false;
727             }
728
729         }
730         this.boundMappedToLastCycle = boundMapping;
731         return result;
732     }
733     
734     /**
735      * Converts a coordinate from Java 2D space to data space.
736      *
737      * @param java2DValue the coordinate in Java2D space.
738      * @param dataArea the data area.
739      * @param edge the edge.
740      *
741      * @return The data value.
742      */

743     public double java2DToValue(double java2DValue, Rectangle2D JavaDoc dataArea,
744                                 RectangleEdge edge) {
745         Range range = getRange();
746         
747         double vmax = range.getUpperBound();
748         double vp = getCycleBound();
749
750         double jmin = 0.0;
751         double jmax = 0.0;
752         if (RectangleEdge.isTopOrBottom(edge)) {
753             jmin = dataArea.getMinX();
754             jmax = dataArea.getMaxX();
755         }
756         else if (RectangleEdge.isLeftOrRight(edge)) {
757             jmin = dataArea.getMaxY();
758             jmax = dataArea.getMinY();
759         }
760         
761         if (isInverted()) {
762             double jbreak = jmax - (vmax - vp) * (jmax - jmin) / this.period;
763             if (java2DValue >= jbreak) {
764                 return vp + (jmax - java2DValue) * this.period / (jmax - jmin);
765             }
766             else {
767                 return vp - (java2DValue - jmin) * this.period / (jmax - jmin);
768             }
769         }
770         else {
771             double jbreak = (vmax - vp) * (jmax - jmin) / this.period + jmin;
772             if (java2DValue <= jbreak) {
773                 return vp + (java2DValue - jmin) * this.period / (jmax - jmin);
774             }
775             else {
776                 return vp - (jmax - java2DValue) * this.period / (jmax - jmin);
777             }
778         }
779     }
780     
781     /**
782      * Translates a value from data space to Java 2D space.
783      *
784      * @param value the data value.
785      * @param dataArea the data area.
786      * @param edge the edge.
787      *
788      * @return The Java 2D value.
789      */

790     public double valueToJava2D(double value, Rectangle2D JavaDoc dataArea,
791                                 RectangleEdge edge) {
792         Range range = getRange();
793         
794         double vmin = range.getLowerBound();
795         double vmax = range.getUpperBound();
796         double vp = getCycleBound();
797
798         if ((value < vmin) || (value > vmax)) {
799             return Double.NaN;
800         }
801         
802         
803         double jmin = 0.0;
804         double jmax = 0.0;
805         if (RectangleEdge.isTopOrBottom(edge)) {
806             jmin = dataArea.getMinX();
807             jmax = dataArea.getMaxX();
808         }
809         else if (RectangleEdge.isLeftOrRight(edge)) {
810             jmax = dataArea.getMinY();
811             jmin = dataArea.getMaxY();
812         }
813
814         if (isInverted()) {
815             if (value == vp) {
816                 return this.boundMappedToLastCycle ? jmin : jmax;
817             }
818             else if (value > vp) {
819                 return jmax - (value - vp) * (jmax - jmin) / this.period;
820             }
821             else {
822                 return jmin + (vp - value) * (jmax - jmin) / this.period;
823             }
824         }
825         else {
826             if (value == vp) {
827                 return this.boundMappedToLastCycle ? jmax : jmin;
828             }
829             else if (value >= vp) {
830                 return jmin + (value - vp) * (jmax - jmin) / this.period;
831             }
832             else {
833                 return jmax - (vp - value) * (jmax - jmin) / this.period;
834             }
835         }
836     }
837     
838     /**
839      * Centers the range about the given value.
840      *
841      * @param value the data value.
842      */

843     public void centerRange(double value) {
844         setRange(value - this.period / 2.0, value + this.period / 2.0);
845     }
846
847     /**
848      * This function is nearly useless since the auto range is fixed for this
849      * class to the period. The period is extended if necessary to fit the
850      * minimum size.
851      *
852      * @param size the size.
853      * @param notify notify?
854      *
855      * @see org.jfree.chart.axis.ValueAxis#setAutoRangeMinimumSize(double,
856      * boolean)
857      */

858     public void setAutoRangeMinimumSize(double size, boolean notify) {
859         if (size > this.period) {
860             this.period = size;
861         }
862         super.setAutoRangeMinimumSize(size, notify);
863     }
864
865     /**
866      * The auto range is fixed for this class to the period by default.
867      * This function will thus set a new period.
868      *
869      * @param length the length.
870      *
871      * @see org.jfree.chart.axis.ValueAxis#setFixedAutoRange(double)
872      */

873     public void setFixedAutoRange(double length) {
874         this.period = length;
875         super.setFixedAutoRange(length);
876     }
877
878     /**
879      * Sets a new axis range. The period is extended to fit the range size, if
880      * necessary.
881      *
882      * @param range the range.
883      * @param turnOffAutoRange switch off the auto range.
884      * @param notify notify?
885      *
886      * @see org.jfree.chart.axis.ValueAxis#setRange(Range, boolean, boolean)
887      */

888     public void setRange(Range range, boolean turnOffAutoRange,
889                          boolean notify) {
890         double size = range.getUpperBound() - range.getLowerBound();
891         if (size > this.period) {
892             this.period = size;
893         }
894         super.setRange(range, turnOffAutoRange, notify);
895     }
896     
897     /**
898      * The cycle bound is defined as the higest value x such that
899      * "offset + period * i = x", with i and integer and x &lt;
900      * range.getUpperBound() This is the value which is at both ends of the
901      * axis : x...up|low...x
902      * The values from x to up are the valued in the current cycle.
903      * The values from low to x are the valued in the previous cycle.
904      *
905      * @return The cycle bound.
906      */

907     public double getCycleBound() {
908         return Math.floor(
909             (getRange().getUpperBound() - this.offset) / this.period
910         ) * this.period + this.offset;
911     }
912     
913     /**
914      * The cycle bound is a multiple of the period, plus optionally a start
915      * offset.
916      * <P>
917      * <pre>cb = n * period + offset</pre><br>
918      *
919      * @return The current offset.
920      *
921      * @see #getCycleBound()
922      */

923     public double getOffset() {
924         return this.offset;
925     }
926     
927     /**
928      * The cycle bound is a multiple of the period, plus optionally a start
929      * offset.
930      * <P>
931      * <pre>cb = n * period + offset</pre><br>
932      *
933      * @param offset The offset to set.
934      *
935      * @see #getCycleBound()
936      */

937     public void setOffset(double offset) {
938         this.offset = offset;
939     }
940     
941     /**
942      * The cycle bound is a multiple of the period, plus optionally a start
943      * offset.
944      * <P>
945      * <pre>cb = n * period + offset</pre><br>
946      *
947      * @return The current period.
948      *
949      * @see #getCycleBound()
950      */

951     public double getPeriod() {
952         return this.period;
953     }
954     
955     /**
956      * The cycle bound is a multiple of the period, plus optionally a start
957      * offset.
958      * <P>
959      * <pre>cb = n * period + offset</pre><br>
960      *
961      * @param period The period to set.
962      *
963      * @see #getCycleBound()
964      */

965     public void setPeriod(double period) {
966         this.period = period;
967     }
968
969     /**
970      * Draws the tick marks and labels.
971      *
972      * @param g2 the graphics device.
973      * @param cursor the cursor.
974      * @param plotArea the plot area.
975      * @param dataArea the area inside the axes.
976      * @param edge the side on which the axis is displayed.
977      *
978      * @return The axis state.
979      */

980     protected AxisState drawTickMarksAndLabels(Graphics2D JavaDoc g2, double cursor,
981                                                Rectangle2D JavaDoc plotArea,
982                                                Rectangle2D JavaDoc dataArea,
983                                                RectangleEdge edge) {
984         this.internalMarkerWhenTicksOverlap = false;
985         AxisState ret = super.drawTickMarksAndLabels(
986             g2, cursor, plotArea, dataArea, edge
987         );
988         
989         // continue and separate the labels only if necessary
990
if (!this.internalMarkerWhenTicksOverlap) {
991             return ret;
992         }
993         
994         double ol = getTickMarkOutsideLength();
995         FontMetrics JavaDoc fm = g2.getFontMetrics(getTickLabelFont());
996         
997         if (isVerticalTickLabels()) {
998             ol = fm.getMaxAdvance();
999         }
1000        else {
1001            ol = fm.getHeight();
1002        }
1003        
1004        double il = 0;
1005        if (isTickMarksVisible()) {
1006            float xx = (float) valueToJava2D(
1007                getRange().getUpperBound(), dataArea, edge
1008            );
1009            Line2D JavaDoc mark = null;
1010            g2.setStroke(getTickMarkStroke());
1011            g2.setPaint(getTickMarkPaint());
1012            if (edge == RectangleEdge.LEFT) {
1013                mark = new Line2D.Double JavaDoc(cursor - ol, xx, cursor + il, xx);
1014            }
1015            else if (edge == RectangleEdge.RIGHT) {
1016                mark = new Line2D.Double JavaDoc(cursor + ol, xx, cursor - il, xx);
1017            }
1018            else if (edge == RectangleEdge.TOP) {
1019                mark = new Line2D.Double JavaDoc(xx, cursor - ol, xx, cursor + il);
1020            }
1021            else if (edge == RectangleEdge.BOTTOM) {
1022                mark = new Line2D.Double JavaDoc(xx, cursor + ol, xx, cursor - il);
1023            }
1024            g2.draw(mark);
1025        }
1026        return ret;
1027    }
1028    
1029    /**
1030     * Draws the axis.
1031     *
1032     * @param g2 the graphics device (<code>null</code> not permitted).
1033     * @param cursor the cursor position.
1034     * @param plotArea the plot area (<code>null</code> not permitted).
1035     * @param dataArea the data area (<code>null</code> not permitted).
1036     * @param edge the edge (<code>null</code> not permitted).
1037     * @param plotState collects information about the plot
1038     * (<code>null</code> permitted).
1039     *
1040     * @return The axis state (never <code>null</code>).
1041     */

1042    public AxisState draw(Graphics2D JavaDoc g2,
1043                          double cursor,
1044                          Rectangle2D JavaDoc plotArea,
1045                          Rectangle2D JavaDoc dataArea,
1046                          RectangleEdge edge,
1047                          PlotRenderingInfo plotState) {
1048        
1049        AxisState ret = super.draw(
1050            g2, cursor, plotArea, dataArea, edge, plotState
1051        );
1052        if (isAdvanceLineVisible()) {
1053            double xx = valueToJava2D(
1054                getRange().getUpperBound(), dataArea, edge
1055            );
1056            Line2D JavaDoc mark = null;
1057            g2.setStroke(getAdvanceLineStroke());
1058            g2.setPaint(getAdvanceLinePaint());
1059            if (edge == RectangleEdge.LEFT) {
1060                mark = new Line2D.Double JavaDoc(
1061                    cursor, xx, cursor + dataArea.getWidth(), xx
1062                );
1063            }
1064            else if (edge == RectangleEdge.RIGHT) {
1065                mark = new Line2D.Double JavaDoc(
1066                    cursor - dataArea.getWidth(), xx, cursor, xx
1067                );
1068            }
1069            else if (edge == RectangleEdge.TOP) {
1070                mark = new Line2D.Double JavaDoc(
1071                    xx, cursor + dataArea.getHeight(), xx, cursor
1072                );
1073            }
1074            else if (edge == RectangleEdge.BOTTOM) {
1075                mark = new Line2D.Double JavaDoc(
1076                    xx, cursor, xx, cursor - dataArea.getHeight()
1077                );
1078            }
1079            g2.draw(mark);
1080        }
1081        return ret;
1082    }
1083
1084    /**
1085     * Reserve some space on each axis side because we draw a centered label at
1086     * each extremity.
1087     *
1088     * @param g2 the graphics device.
1089     * @param plot the plot.
1090     * @param plotArea the plot area.
1091     * @param edge the edge.
1092     * @param space the space already reserved.
1093     *
1094     * @return The reserved space.
1095     */

1096    public AxisSpace reserveSpace(Graphics2D JavaDoc g2,
1097                                  Plot plot,
1098                                  Rectangle2D JavaDoc plotArea,
1099                                  RectangleEdge edge,
1100                                  AxisSpace space) {
1101        
1102        this.internalMarkerCycleBoundTick = null;
1103        AxisSpace ret = super.reserveSpace(g2, plot, plotArea, edge, space);
1104        if (this.internalMarkerCycleBoundTick == null) {
1105            return ret;
1106        }
1107
1108        FontMetrics JavaDoc fm = g2.getFontMetrics(getTickLabelFont());
1109        Rectangle2D JavaDoc r = TextUtilities.getTextBounds(
1110            this.internalMarkerCycleBoundTick.getText(), g2, fm
1111        );
1112
1113        if (RectangleEdge.isTopOrBottom(edge)) {
1114            if (isVerticalTickLabels()) {
1115                space.add(r.getHeight() / 2, RectangleEdge.RIGHT);
1116            }
1117            else {
1118                space.add(r.getWidth() / 2, RectangleEdge.RIGHT);
1119            }
1120        }
1121        else if (RectangleEdge.isLeftOrRight(edge)) {
1122            if (isVerticalTickLabels()) {
1123                space.add(r.getWidth() / 2, RectangleEdge.TOP);
1124            }
1125            else {
1126                space.add(r.getHeight() / 2, RectangleEdge.TOP);
1127            }
1128        }
1129        
1130        return ret;
1131        
1132    }
1133
1134    /**
1135     * Provides serialization support.
1136     *
1137     * @param stream the output stream.
1138     *
1139     * @throws IOException if there is an I/O error.
1140     */

1141    private void writeObject(ObjectOutputStream JavaDoc stream) throws IOException JavaDoc {
1142    
1143        stream.defaultWriteObject();
1144        SerialUtilities.writePaint(this.advanceLinePaint, stream);
1145        SerialUtilities.writeStroke(this.advanceLineStroke, stream);
1146    
1147    }
1148    
1149    /**
1150     * Provides serialization support.
1151     *
1152     * @param stream the input stream.
1153     *
1154     * @throws IOException if there is an I/O error.
1155     * @throws ClassNotFoundException if there is a classpath problem.
1156     */

1157    private void readObject(ObjectInputStream JavaDoc stream)
1158        throws IOException JavaDoc, ClassNotFoundException JavaDoc {
1159    
1160        stream.defaultReadObject();
1161        this.advanceLinePaint = SerialUtilities.readPaint(stream);
1162        this.advanceLineStroke = SerialUtilities.readStroke(stream);
1163    
1164    }
1165     
1166    
1167    /**
1168     * Tests the axis for equality with another object.
1169     *
1170     * @param object the object to test against.
1171     *
1172     * @return A boolean.
1173     */

1174    public boolean equals(Object JavaDoc object) {
1175        if (object == this) {
1176            return true;
1177        }
1178        if (!(object instanceof CyclicNumberAxis)) {
1179            return false;
1180        }
1181        if (!super.equals(object)) {
1182            return false;
1183        }
1184        CyclicNumberAxis axis = (CyclicNumberAxis) object;
1185                
1186        if (this.period != axis.period) {
1187            return false;
1188        }
1189        if (this.offset != axis.offset) {
1190            return false;
1191        }
1192        if (!ObjectUtilities.equal(this.advanceLinePaint,
1193                axis.advanceLinePaint)) {
1194            return false;
1195        }
1196        if (!ObjectUtilities.equal(this.advanceLineStroke,
1197                axis.advanceLineStroke)) {
1198            return false;
1199        }
1200        if (this.advanceLineVisible != axis.advanceLineVisible) {
1201            return false;
1202        }
1203        if (this.boundMappedToLastCycle != axis.boundMappedToLastCycle) {
1204            return false;
1205        }
1206        return true;
1207    }
1208}
1209
Popular Tags