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