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
049 package org.jfree.chart.axis;
050
051 import java.awt.BasicStroke;
052 import java.awt.Color;
053 import java.awt.Font;
054 import java.awt.FontMetrics;
055 import java.awt.Graphics2D;
056 import java.awt.Paint;
057 import java.awt.Stroke;
058 import java.awt.geom.Line2D;
059 import java.awt.geom.Rectangle2D;
060 import java.io.IOException;
061 import java.io.ObjectInputStream;
062 import java.io.ObjectOutputStream;
063 import java.text.NumberFormat;
064 import java.util.List;
065
066 import org.jfree.chart.plot.Plot;
067 import org.jfree.chart.plot.PlotRenderingInfo;
068 import org.jfree.data.Range;
069 import org.jfree.io.SerialUtilities;
070 import org.jfree.text.TextUtilities;
071 import org.jfree.ui.RectangleEdge;
072 import org.jfree.ui.TextAnchor;
073 import org.jfree.util.ObjectUtilities;
074 import org.jfree.util.PaintUtilities;
075
076 /**
077 This class extends NumberAxis and handles cycling.
078
079 Traditional representation of data in the range x0..x1
080 <pre>
081 |-------------------------|
082 x0 x1
083 </pre>
084
085 Here, the range bounds are at the axis extremities.
086 With cyclic axis, however, the time is split in
087 "cycles", or "time frames", or the same duration : the period.
088
089 A cycle axis cannot by definition handle a larger interval
090 than the period : <pre>x1 - x0 >= period</pre>. Thus, at most a full
091 period can be represented with such an axis.
092
093 The cycle bound is the number between x0 and x1 which marks
094 the beginning of new time frame:
095 <pre>
096 |---------------------|----------------------------|
097 x0 cb x1
098 <---previous cycle---><-------current cycle-------->
099 </pre>
100
101 It is actually a multiple of the period, plus optionally
102 a start offset: <pre>cb = n * period + offset</pre>
103
104 Thus, by definition, two consecutive cycle bounds
105 period apart, which is precisely why it is called a
106 period.
107
108 The visual representation of a cyclic axis is like that:
109 <pre>
110 |----------------------------|---------------------|
111 cb x1|x0 cb
112 <-------current cycle--------><---previous cycle--->
113 </pre>
114
115 The cycle bound is at the axis ends, then current
116 cycle is shown, then the last cycle. When using
117 dynamic data, the visual effect is the current cycle
118 erases the last cycle as x grows. Then, the next cycle
119 bound is reached, and the process starts over, erasing
120 the previous cycle.
121
122 A Cyclic item renderer is provided to do exactly this.
123
124 */
125 public 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 <
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 }