001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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 * PolarPlot.java
029 * --------------
030 * (C) Copyright 2004-2008, by Solution Engineering, Inc. and Contributors.
031 *
032 * Original Author: Daniel Bridenbecker, Solution Engineering, Inc.;
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 * Martin Hoeller (patch 1871902);
035 *
036 * Changes
037 * -------
038 * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
039 * 07-Apr-2004 : Changed text bounds calculation (DG);
040 * 05-May-2005 : Updated draw() method parameters (DG);
041 * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG);
042 * 25-Oct-2005 : Implemented Zoomable (DG);
043 * ------------- JFREECHART 1.0.x ---------------------------------------------
044 * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG);
045 * 21-Mar-2007 : Fixed serialization bug (DG);
046 * 24-Sep-2007 : Implemented new zooming methods (DG);
047 * 17-Feb-2007 : Added angle tick unit attribute (see patch 1871902 by
048 * Martin Hoeller) (DG);
049 * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by
050 * Jess Thrysoee (DG);
051 *
052 */
053
054 package org.jfree.chart.plot;
055
056 import java.awt.AlphaComposite;
057 import java.awt.BasicStroke;
058 import java.awt.Color;
059 import java.awt.Composite;
060 import java.awt.Font;
061 import java.awt.FontMetrics;
062 import java.awt.Graphics2D;
063 import java.awt.Paint;
064 import java.awt.Point;
065 import java.awt.Shape;
066 import java.awt.Stroke;
067 import java.awt.geom.Point2D;
068 import java.awt.geom.Rectangle2D;
069 import java.io.IOException;
070 import java.io.ObjectInputStream;
071 import java.io.ObjectOutputStream;
072 import java.io.Serializable;
073 import java.util.ArrayList;
074 import java.util.Iterator;
075 import java.util.List;
076 import java.util.ResourceBundle;
077
078 import org.jfree.chart.LegendItem;
079 import org.jfree.chart.LegendItemCollection;
080 import org.jfree.chart.axis.AxisState;
081 import org.jfree.chart.axis.NumberTick;
082 import org.jfree.chart.axis.NumberTickUnit;
083 import org.jfree.chart.axis.TickUnit;
084 import org.jfree.chart.axis.ValueAxis;
085 import org.jfree.chart.event.PlotChangeEvent;
086 import org.jfree.chart.event.RendererChangeEvent;
087 import org.jfree.chart.event.RendererChangeListener;
088 import org.jfree.chart.renderer.PolarItemRenderer;
089 import org.jfree.chart.util.ResourceBundleWrapper;
090 import org.jfree.data.Range;
091 import org.jfree.data.general.DatasetChangeEvent;
092 import org.jfree.data.general.DatasetUtilities;
093 import org.jfree.data.xy.XYDataset;
094 import org.jfree.io.SerialUtilities;
095 import org.jfree.text.TextUtilities;
096 import org.jfree.ui.RectangleEdge;
097 import org.jfree.ui.RectangleInsets;
098 import org.jfree.ui.TextAnchor;
099 import org.jfree.util.ObjectUtilities;
100 import org.jfree.util.PaintUtilities;
101
102 /**
103 * Plots data that is in (theta, radius) pairs where
104 * theta equal to zero is due north and increases clockwise.
105 */
106 public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable,
107 RendererChangeListener, Cloneable, Serializable {
108
109 /** For serialization. */
110 private static final long serialVersionUID = 3794383185924179525L;
111
112 /** The default margin. */
113 private static final int MARGIN = 20;
114
115 /** The annotation margin. */
116 private static final double ANNOTATION_MARGIN = 7.0;
117
118 /**
119 * The default angle tick unit size.
120 *
121 * @since 1.0.10
122 */
123 public static final double DEFAULT_ANGLE_TICK_UNIT_SIZE = 45.0;
124
125 /** The default grid line stroke. */
126 public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
127 0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
128 0.0f, new float[]{2.0f, 2.0f}, 0.0f);
129
130 /** The default grid line paint. */
131 public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;
132
133 /** The resourceBundle for the localization. */
134 protected static ResourceBundle localizationResources
135 = ResourceBundleWrapper.getBundle(
136 "org.jfree.chart.plot.LocalizationBundle");
137
138 /** The angles that are marked with gridlines. */
139 private List angleTicks;
140
141 /** The axis (used for the y-values). */
142 private ValueAxis axis;
143
144 /** The dataset. */
145 private XYDataset dataset;
146
147 /**
148 * Object responsible for drawing the visual representation of each point
149 * on the plot.
150 */
151 private PolarItemRenderer renderer;
152
153 /**
154 * The tick unit that controls the spacing between the angular grid lines.
155 *
156 * @since 1.0.10
157 */
158 private TickUnit angleTickUnit;
159
160 /** A flag that controls whether or not the angle labels are visible. */
161 private boolean angleLabelsVisible = true;
162
163 /** The font used to display the angle labels - never null. */
164 private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
165
166 /** The paint used to display the angle labels. */
167 private transient Paint angleLabelPaint = Color.black;
168
169 /** A flag that controls whether the angular grid-lines are visible. */
170 private boolean angleGridlinesVisible;
171
172 /** The stroke used to draw the angular grid-lines. */
173 private transient Stroke angleGridlineStroke;
174
175 /** The paint used to draw the angular grid-lines. */
176 private transient Paint angleGridlinePaint;
177
178 /** A flag that controls whether the radius grid-lines are visible. */
179 private boolean radiusGridlinesVisible;
180
181 /** The stroke used to draw the radius grid-lines. */
182 private transient Stroke radiusGridlineStroke;
183
184 /** The paint used to draw the radius grid-lines. */
185 private transient Paint radiusGridlinePaint;
186
187 /** The annotations for the plot. */
188 private List cornerTextItems = new ArrayList();
189
190 /**
191 * Default constructor.
192 */
193 public PolarPlot() {
194 this(null, null, null);
195 }
196
197 /**
198 * Creates a new plot.
199 *
200 * @param dataset the dataset (<code>null</code> permitted).
201 * @param radiusAxis the radius axis (<code>null</code> permitted).
202 * @param renderer the renderer (<code>null</code> permitted).
203 */
204 public PolarPlot(XYDataset dataset,
205 ValueAxis radiusAxis,
206 PolarItemRenderer renderer) {
207
208 super();
209
210 this.dataset = dataset;
211 if (this.dataset != null) {
212 this.dataset.addChangeListener(this);
213 }
214 this.angleTickUnit = new NumberTickUnit(DEFAULT_ANGLE_TICK_UNIT_SIZE);
215
216 this.axis = radiusAxis;
217 if (this.axis != null) {
218 this.axis.setPlot(this);
219 this.axis.addChangeListener(this);
220 }
221
222 this.renderer = renderer;
223 if (this.renderer != null) {
224 this.renderer.setPlot(this);
225 this.renderer.addChangeListener(this);
226 }
227
228 this.angleGridlinesVisible = true;
229 this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE;
230 this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT;
231
232 this.radiusGridlinesVisible = true;
233 this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE;
234 this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT;
235 }
236
237 /**
238 * Add text to be displayed in the lower right hand corner and sends a
239 * {@link PlotChangeEvent} to all registered listeners.
240 *
241 * @param text the text to display (<code>null</code> not permitted).
242 *
243 * @see #removeCornerTextItem(String)
244 */
245 public void addCornerTextItem(String text) {
246 if (text == null) {
247 throw new IllegalArgumentException("Null 'text' argument.");
248 }
249 this.cornerTextItems.add(text);
250 fireChangeEvent();
251 }
252
253 /**
254 * Remove the given text from the list of corner text items and
255 * sends a {@link PlotChangeEvent} to all registered listeners.
256 *
257 * @param text the text to remove (<code>null</code> ignored).
258 *
259 * @see #addCornerTextItem(String)
260 */
261 public void removeCornerTextItem(String text) {
262 boolean removed = this.cornerTextItems.remove(text);
263 if (removed) {
264 fireChangeEvent();
265 }
266 }
267
268 /**
269 * Clear the list of corner text items and sends a {@link PlotChangeEvent}
270 * to all registered listeners.
271 *
272 * @see #addCornerTextItem(String)
273 * @see #removeCornerTextItem(String)
274 */
275 public void clearCornerTextItems() {
276 if (this.cornerTextItems.size() > 0) {
277 this.cornerTextItems.clear();
278 fireChangeEvent();
279 }
280 }
281
282 /**
283 * Returns the plot type as a string.
284 *
285 * @return A short string describing the type of plot.
286 */
287 public String getPlotType() {
288 return PolarPlot.localizationResources.getString("Polar_Plot");
289 }
290
291 /**
292 * Returns the axis for the plot.
293 *
294 * @return The radius axis (possibly <code>null</code>).
295 *
296 * @see #setAxis(ValueAxis)
297 */
298 public ValueAxis getAxis() {
299 return this.axis;
300 }
301
302 /**
303 * Sets the axis for the plot and sends a {@link PlotChangeEvent} to all
304 * registered listeners.
305 *
306 * @param axis the new axis (<code>null</code> permitted).
307 */
308 public void setAxis(ValueAxis axis) {
309 if (axis != null) {
310 axis.setPlot(this);
311 }
312
313 // plot is likely registered as a listener with the existing axis...
314 if (this.axis != null) {
315 this.axis.removeChangeListener(this);
316 }
317
318 this.axis = axis;
319 if (this.axis != null) {
320 this.axis.configure();
321 this.axis.addChangeListener(this);
322 }
323 fireChangeEvent();
324 }
325
326 /**
327 * Returns the primary dataset for the plot.
328 *
329 * @return The primary dataset (possibly <code>null</code>).
330 *
331 * @see #setDataset(XYDataset)
332 */
333 public XYDataset getDataset() {
334 return this.dataset;
335 }
336
337 /**
338 * Sets the dataset for the plot, replacing the existing dataset if there
339 * is one.
340 *
341 * @param dataset the dataset (<code>null</code> permitted).
342 *
343 * @see #getDataset()
344 */
345 public void setDataset(XYDataset dataset) {
346 // if there is an existing dataset, remove the plot from the list of
347 // change listeners...
348 XYDataset existing = this.dataset;
349 if (existing != null) {
350 existing.removeChangeListener(this);
351 }
352
353 // set the new m_Dataset, and register the chart as a change listener...
354 this.dataset = dataset;
355 if (this.dataset != null) {
356 setDatasetGroup(this.dataset.getGroup());
357 this.dataset.addChangeListener(this);
358 }
359
360 // send a m_Dataset change event to self...
361 DatasetChangeEvent event = new DatasetChangeEvent(this, this.dataset);
362 datasetChanged(event);
363 }
364
365 /**
366 * Returns the item renderer.
367 *
368 * @return The renderer (possibly <code>null</code>).
369 *
370 * @see #setRenderer(PolarItemRenderer)
371 */
372 public PolarItemRenderer getRenderer() {
373 return this.renderer;
374 }
375
376 /**
377 * Sets the item renderer, and notifies all listeners of a change to the
378 * plot.
379 * <P>
380 * If the renderer is set to <code>null</code>, no chart will be drawn.
381 *
382 * @param renderer the new renderer (<code>null</code> permitted).
383 *
384 * @see #getRenderer()
385 */
386 public void setRenderer(PolarItemRenderer renderer) {
387 if (this.renderer != null) {
388 this.renderer.removeChangeListener(this);
389 }
390
391 this.renderer = renderer;
392 if (this.renderer != null) {
393 this.renderer.setPlot(this);
394 }
395 fireChangeEvent();
396 }
397
398 /**
399 * Returns the tick unit that controls the spacing of the angular grid
400 * lines.
401 *
402 * @return The tick unit (never <code>null</code>).
403 *
404 * @since 1.0.10
405 */
406 public TickUnit getAngleTickUnit() {
407 return this.angleTickUnit;
408 }
409
410 /**
411 * Sets the tick unit that controls the spacing of the angular grid
412 * lines, and sends a {@link PlotChangeEvent} to all registered listeners.
413 *
414 * @param unit the tick unit (<code>null</code> not permitted).
415 *
416 * @since 1.0.10
417 */
418 public void setAngleTickUnit(TickUnit unit) {
419 if (unit == null) {
420 throw new IllegalArgumentException("Null 'unit' argument.");
421 }
422 this.angleTickUnit = unit;
423 fireChangeEvent();
424 }
425
426 /**
427 * Returns a flag that controls whether or not the angle labels are visible.
428 *
429 * @return A boolean.
430 *
431 * @see #setAngleLabelsVisible(boolean)
432 */
433 public boolean isAngleLabelsVisible() {
434 return this.angleLabelsVisible;
435 }
436
437 /**
438 * Sets the flag that controls whether or not the angle labels are visible,
439 * and sends a {@link PlotChangeEvent} to all registered listeners.
440 *
441 * @param visible the flag.
442 *
443 * @see #isAngleLabelsVisible()
444 */
445 public void setAngleLabelsVisible(boolean visible) {
446 if (this.angleLabelsVisible != visible) {
447 this.angleLabelsVisible = visible;
448 fireChangeEvent();
449 }
450 }
451
452 /**
453 * Returns the font used to display the angle labels.
454 *
455 * @return A font (never <code>null</code>).
456 *
457 * @see #setAngleLabelFont(Font)
458 */
459 public Font getAngleLabelFont() {
460 return this.angleLabelFont;
461 }
462
463 /**
464 * Sets the font used to display the angle labels and sends a
465 * {@link PlotChangeEvent} to all registered listeners.
466 *
467 * @param font the font (<code>null</code> not permitted).
468 *
469 * @see #getAngleLabelFont()
470 */
471 public void setAngleLabelFont(Font font) {
472 if (font == null) {
473 throw new IllegalArgumentException("Null 'font' argument.");
474 }
475 this.angleLabelFont = font;
476 fireChangeEvent();
477 }
478
479 /**
480 * Returns the paint used to display the angle labels.
481 *
482 * @return A paint (never <code>null</code>).
483 *
484 * @see #setAngleLabelPaint(Paint)
485 */
486 public Paint getAngleLabelPaint() {
487 return this.angleLabelPaint;
488 }
489
490 /**
491 * Sets the paint used to display the angle labels and sends a
492 * {@link PlotChangeEvent} to all registered listeners.
493 *
494 * @param paint the paint (<code>null</code> not permitted).
495 */
496 public void setAngleLabelPaint(Paint paint) {
497 if (paint == null) {
498 throw new IllegalArgumentException("Null 'paint' argument.");
499 }
500 this.angleLabelPaint = paint;
501 fireChangeEvent();
502 }
503
504 /**
505 * Returns <code>true</code> if the angular gridlines are visible, and
506 * <code>false<code> otherwise.
507 *
508 * @return <code>true</code> or <code>false</code>.
509 *
510 * @see #setAngleGridlinesVisible(boolean)
511 */
512 public boolean isAngleGridlinesVisible() {
513 return this.angleGridlinesVisible;
514 }
515
516 /**
517 * Sets the flag that controls whether or not the angular grid-lines are
518 * visible.
519 * <p>
520 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
521 * registered listeners.
522 *
523 * @param visible the new value of the flag.
524 *
525 * @see #isAngleGridlinesVisible()
526 */
527 public void setAngleGridlinesVisible(boolean visible) {
528 if (this.angleGridlinesVisible != visible) {
529 this.angleGridlinesVisible = visible;
530 fireChangeEvent();
531 }
532 }
533
534 /**
535 * Returns the stroke for the grid-lines (if any) plotted against the
536 * angular axis.
537 *
538 * @return The stroke (possibly <code>null</code>).
539 *
540 * @see #setAngleGridlineStroke(Stroke)
541 */
542 public Stroke getAngleGridlineStroke() {
543 return this.angleGridlineStroke;
544 }
545
546 /**
547 * Sets the stroke for the grid lines plotted against the angular axis and
548 * sends a {@link PlotChangeEvent} to all registered listeners.
549 * <p>
550 * If you set this to <code>null</code>, no grid lines will be drawn.
551 *
552 * @param stroke the stroke (<code>null</code> permitted).
553 *
554 * @see #getAngleGridlineStroke()
555 */
556 public void setAngleGridlineStroke(Stroke stroke) {
557 this.angleGridlineStroke = stroke;
558 fireChangeEvent();
559 }
560
561 /**
562 * Returns the paint for the grid lines (if any) plotted against the
563 * angular axis.
564 *
565 * @return The paint (possibly <code>null</code>).
566 *
567 * @see #setAngleGridlinePaint(Paint)
568 */
569 public Paint getAngleGridlinePaint() {
570 return this.angleGridlinePaint;
571 }
572
573 /**
574 * Sets the paint for the grid lines plotted against the angular axis.
575 * <p>
576 * If you set this to <code>null</code>, no grid lines will be drawn.
577 *
578 * @param paint the paint (<code>null</code> permitted).
579 *
580 * @see #getAngleGridlinePaint()
581 */
582 public void setAngleGridlinePaint(Paint paint) {
583 this.angleGridlinePaint = paint;
584 fireChangeEvent();
585 }
586
587 /**
588 * Returns <code>true</code> if the radius axis grid is visible, and
589 * <code>false<code> otherwise.
590 *
591 * @return <code>true</code> or <code>false</code>.
592 *
593 * @see #setRadiusGridlinesVisible(boolean)
594 */
595 public boolean isRadiusGridlinesVisible() {
596 return this.radiusGridlinesVisible;
597 }
598
599 /**
600 * Sets the flag that controls whether or not the radius axis grid lines
601 * are visible.
602 * <p>
603 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
604 * registered listeners.
605 *
606 * @param visible the new value of the flag.
607 *
608 * @see #isRadiusGridlinesVisible()
609 */
610 public void setRadiusGridlinesVisible(boolean visible) {
611 if (this.radiusGridlinesVisible != visible) {
612 this.radiusGridlinesVisible = visible;
613 fireChangeEvent();
614 }
615 }
616
617 /**
618 * Returns the stroke for the grid lines (if any) plotted against the
619 * radius axis.
620 *
621 * @return The stroke (possibly <code>null</code>).
622 *
623 * @see #setRadiusGridlineStroke(Stroke)
624 */
625 public Stroke getRadiusGridlineStroke() {
626 return this.radiusGridlineStroke;
627 }
628
629 /**
630 * Sets the stroke for the grid lines plotted against the radius axis and
631 * sends a {@link PlotChangeEvent} to all registered listeners.
632 * <p>
633 * If you set this to <code>null</code>, no grid lines will be drawn.
634 *
635 * @param stroke the stroke (<code>null</code> permitted).
636 *
637 * @see #getRadiusGridlineStroke()
638 */
639 public void setRadiusGridlineStroke(Stroke stroke) {
640 this.radiusGridlineStroke = stroke;
641 fireChangeEvent();
642 }
643
644 /**
645 * Returns the paint for the grid lines (if any) plotted against the radius
646 * axis.
647 *
648 * @return The paint (possibly <code>null</code>).
649 *
650 * @see #setRadiusGridlinePaint(Paint)
651 */
652 public Paint getRadiusGridlinePaint() {
653 return this.radiusGridlinePaint;
654 }
655
656 /**
657 * Sets the paint for the grid lines plotted against the radius axis and
658 * sends a {@link PlotChangeEvent} to all registered listeners.
659 * <p>
660 * If you set this to <code>null</code>, no grid lines will be drawn.
661 *
662 * @param paint the paint (<code>null</code> permitted).
663 *
664 * @see #getRadiusGridlinePaint()
665 */
666 public void setRadiusGridlinePaint(Paint paint) {
667 this.radiusGridlinePaint = paint;
668 fireChangeEvent();
669 }
670
671 /**
672 * Generates a list of tick values for the angular tick marks.
673 *
674 * @return A list of {@link NumberTick} instances.
675 *
676 * @since 1.0.10
677 */
678 protected List refreshAngleTicks() {
679 List ticks = new ArrayList();
680 for (double currentTickVal = 0.0; currentTickVal < 360.0;
681 currentTickVal += this.angleTickUnit.getSize()) {
682 NumberTick tick = new NumberTick(new Double(currentTickVal),
683 this.angleTickUnit.valueToString(currentTickVal),
684 TextAnchor.CENTER, TextAnchor.CENTER, 0.0);
685 ticks.add(tick);
686 }
687 return ticks;
688 }
689
690 /**
691 * Draws the plot on a Java 2D graphics device (such as the screen or a
692 * printer).
693 * <P>
694 * This plot relies on a {@link PolarItemRenderer} to draw each
695 * item in the plot. This allows the visual representation of the data to
696 * be changed easily.
697 * <P>
698 * The optional info argument collects information about the rendering of
699 * the plot (dimensions, tooltip information etc). Just pass in
700 * <code>null</code> if you do not need this information.
701 *
702 * @param g2 the graphics device.
703 * @param area the area within which the plot (including axes and
704 * labels) should be drawn.
705 * @param anchor the anchor point (<code>null</code> permitted).
706 * @param parentState ignored.
707 * @param info collects chart drawing information (<code>null</code>
708 * permitted).
709 */
710 public void draw(Graphics2D g2,
711 Rectangle2D area,
712 Point2D anchor,
713 PlotState parentState,
714 PlotRenderingInfo info) {
715
716 // if the plot area is too small, just return...
717 boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
718 boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
719 if (b1 || b2) {
720 return;
721 }
722
723 // record the plot area...
724 if (info != null) {
725 info.setPlotArea(area);
726 }
727
728 // adjust the drawing area for the plot insets (if any)...
729 RectangleInsets insets = getInsets();
730 insets.trim(area);
731
732 Rectangle2D dataArea = area;
733 if (info != null) {
734 info.setDataArea(dataArea);
735 }
736
737 // draw the plot background and axes...
738 drawBackground(g2, dataArea);
739 double h = Math.min(dataArea.getWidth() / 2.0,
740 dataArea.getHeight() / 2.0) - MARGIN;
741 Rectangle2D quadrant = new Rectangle2D.Double(dataArea.getCenterX(),
742 dataArea.getCenterY(), h, h);
743 AxisState state = drawAxis(g2, area, quadrant);
744 if (this.renderer != null) {
745 Shape originalClip = g2.getClip();
746 Composite originalComposite = g2.getComposite();
747
748 g2.clip(dataArea);
749 g2.setComposite(AlphaComposite.getInstance(
750 AlphaComposite.SRC_OVER, getForegroundAlpha()));
751
752 this.angleTicks = refreshAngleTicks();
753 drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());
754
755 // draw...
756 render(g2, dataArea, info);
757
758 g2.setClip(originalClip);
759 g2.setComposite(originalComposite);
760 }
761 drawOutline(g2, dataArea);
762 drawCornerTextItems(g2, dataArea);
763 }
764
765 /**
766 * Draws the corner text items.
767 *
768 * @param g2 the drawing surface.
769 * @param area the area.
770 */
771 protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
772 if (this.cornerTextItems.isEmpty()) {
773 return;
774 }
775
776 g2.setColor(Color.black);
777 double width = 0.0;
778 double height = 0.0;
779 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
780 String msg = (String) it.next();
781 FontMetrics fm = g2.getFontMetrics();
782 Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm);
783 width = Math.max(width, bounds.getWidth());
784 height += bounds.getHeight();
785 }
786
787 double xadj = ANNOTATION_MARGIN * 2.0;
788 double yadj = ANNOTATION_MARGIN;
789 width += xadj;
790 height += yadj;
791
792 double x = area.getMaxX() - width;
793 double y = area.getMaxY() - height;
794 g2.drawRect((int) x, (int) y, (int) width, (int) height);
795 x += ANNOTATION_MARGIN;
796 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
797 String msg = (String) it.next();
798 Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2,
799 g2.getFontMetrics());
800 y += bounds.getHeight();
801 g2.drawString(msg, (int) x, (int) y);
802 }
803 }
804
805 /**
806 * A utility method for drawing the axes.
807 *
808 * @param g2 the graphics device.
809 * @param plotArea the plot area.
810 * @param dataArea the data area.
811 *
812 * @return A map containing the axis states.
813 */
814 protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea,
815 Rectangle2D dataArea) {
816 return this.axis.draw(g2, dataArea.getMinY(), plotArea, dataArea,
817 RectangleEdge.TOP, null);
818 }
819
820 /**
821 * Draws a representation of the data within the dataArea region, using the
822 * current m_Renderer.
823 *
824 * @param g2 the graphics device.
825 * @param dataArea the region in which the data is to be drawn.
826 * @param info an optional object for collection dimension
827 * information (<code>null</code> permitted).
828 */
829 protected void render(Graphics2D g2,
830 Rectangle2D dataArea,
831 PlotRenderingInfo info) {
832
833 // now get the data and plot it (the visual representation will depend
834 // on the m_Renderer that has been set)...
835 if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
836 int seriesCount = this.dataset.getSeriesCount();
837 for (int series = 0; series < seriesCount; series++) {
838 this.renderer.drawSeries(g2, dataArea, info, this,
839 this.dataset, series);
840 }
841 }
842 else {
843 drawNoDataMessage(g2, dataArea);
844 }
845 }
846
847 /**
848 * Draws the gridlines for the plot, if they are visible.
849 *
850 * @param g2 the graphics device.
851 * @param dataArea the data area.
852 * @param angularTicks the ticks for the angular axis.
853 * @param radialTicks the ticks for the radial axis.
854 */
855 protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea,
856 List angularTicks, List radialTicks) {
857
858 // no renderer, no gridlines...
859 if (this.renderer == null) {
860 return;
861 }
862
863 // draw the domain grid lines, if any...
864 if (isAngleGridlinesVisible()) {
865 Stroke gridStroke = getAngleGridlineStroke();
866 Paint gridPaint = getAngleGridlinePaint();
867 if ((gridStroke != null) && (gridPaint != null)) {
868 this.renderer.drawAngularGridLines(g2, this, angularTicks,
869 dataArea);
870 }
871 }
872
873 // draw the radius grid lines, if any...
874 if (isRadiusGridlinesVisible()) {
875 Stroke gridStroke = getRadiusGridlineStroke();
876 Paint gridPaint = getRadiusGridlinePaint();
877 if ((gridStroke != null) && (gridPaint != null)) {
878 this.renderer.drawRadialGridLines(g2, this, this.axis,
879 radialTicks, dataArea);
880 }
881 }
882 }
883
884 /**
885 * Zooms the axis ranges by the specified percentage about the anchor point.
886 *
887 * @param percent the amount of the zoom.
888 */
889 public void zoom(double percent) {
890 if (percent > 0.0) {
891 double radius = getMaxRadius();
892 double scaledRadius = radius * percent;
893 this.axis.setUpperBound(scaledRadius);
894 getAxis().setAutoRange(false);
895 }
896 else {
897 getAxis().setAutoRange(true);
898 }
899 }
900
901 /**
902 * Returns the range for the specified axis.
903 *
904 * @param axis the axis.
905 *
906 * @return The range.
907 */
908 public Range getDataRange(ValueAxis axis) {
909 Range result = null;
910 if (this.dataset != null) {
911 result = Range.combine(result,
912 DatasetUtilities.findRangeBounds(this.dataset));
913 }
914 return result;
915 }
916
917 /**
918 * Receives notification of a change to the plot's m_Dataset.
919 * <P>
920 * The axis ranges are updated if necessary.
921 *
922 * @param event information about the event (not used here).
923 */
924 public void datasetChanged(DatasetChangeEvent event) {
925
926 if (this.axis != null) {
927 this.axis.configure();
928 }
929
930 if (getParent() != null) {
931 getParent().datasetChanged(event);
932 }
933 else {
934 super.datasetChanged(event);
935 }
936 }
937
938 /**
939 * Notifies all registered listeners of a property change.
940 * <P>
941 * One source of property change events is the plot's m_Renderer.
942 *
943 * @param event information about the property change.
944 */
945 public void rendererChanged(RendererChangeEvent event) {
946 fireChangeEvent();
947 }
948
949 /**
950 * Returns the number of series in the dataset for this plot. If the
951 * dataset is <code>null</code>, the method returns 0.
952 *
953 * @return The series count.
954 */
955 public int getSeriesCount() {
956 int result = 0;
957
958 if (this.dataset != null) {
959 result = this.dataset.getSeriesCount();
960 }
961 return result;
962 }
963
964 /**
965 * Returns the legend items for the plot. Each legend item is generated by
966 * the plot's m_Renderer, since the m_Renderer is responsible for the visual
967 * representation of the data.
968 *
969 * @return The legend items.
970 */
971 public LegendItemCollection getLegendItems() {
972 LegendItemCollection result = new LegendItemCollection();
973
974 // get the legend items for the main m_Dataset...
975 if (this.dataset != null) {
976 if (this.renderer != null) {
977 int seriesCount = this.dataset.getSeriesCount();
978 for (int i = 0; i < seriesCount; i++) {
979 LegendItem item = this.renderer.getLegendItem(i);
980 result.add(item);
981 }
982 }
983 }
984 return result;
985 }
986
987 /**
988 * Tests this plot for equality with another object.
989 *
990 * @param obj the object (<code>null</code> permitted).
991 *
992 * @return <code>true</code> or <code>false</code>.
993 */
994 public boolean equals(Object obj) {
995 if (obj == this) {
996 return true;
997 }
998 if (!(obj instanceof PolarPlot)) {
999 return false;
1000 }
1001 PolarPlot that = (PolarPlot) obj;
1002 if (!ObjectUtilities.equal(this.axis, that.axis)) {
1003 return false;
1004 }
1005 if (!ObjectUtilities.equal(this.renderer, that.renderer)) {
1006 return false;
1007 }
1008 if (!this.angleTickUnit.equals(that.angleTickUnit)) {
1009 return false;
1010 }
1011 if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
1012 return false;
1013 }
1014 if (this.angleLabelsVisible != that.angleLabelsVisible) {
1015 return false;
1016 }
1017 if (!this.angleLabelFont.equals(that.angleLabelFont)) {
1018 return false;
1019 }
1020 if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) {
1021 return false;
1022 }
1023 if (!ObjectUtilities.equal(this.angleGridlineStroke,
1024 that.angleGridlineStroke)) {
1025 return false;
1026 }
1027 if (!PaintUtilities.equal(
1028 this.angleGridlinePaint, that.angleGridlinePaint
1029 )) {
1030 return false;
1031 }
1032 if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
1033 return false;
1034 }
1035 if (!ObjectUtilities.equal(this.radiusGridlineStroke,
1036 that.radiusGridlineStroke)) {
1037 return false;
1038 }
1039 if (!PaintUtilities.equal(this.radiusGridlinePaint,
1040 that.radiusGridlinePaint)) {
1041 return false;
1042 }
1043 if (!this.cornerTextItems.equals(that.cornerTextItems)) {
1044 return false;
1045 }
1046 return super.equals(obj);
1047 }
1048
1049 /**
1050 * Returns a clone of the plot.
1051 *
1052 * @return A clone.
1053 *
1054 * @throws CloneNotSupportedException this can occur if some component of
1055 * the plot cannot be cloned.
1056 */
1057 public Object clone() throws CloneNotSupportedException {
1058
1059 PolarPlot clone = (PolarPlot) super.clone();
1060 if (this.axis != null) {
1061 clone.axis = (ValueAxis) ObjectUtilities.clone(this.axis);
1062 clone.axis.setPlot(clone);
1063 clone.axis.addChangeListener(clone);
1064 }
1065
1066 if (clone.dataset != null) {
1067 clone.dataset.addChangeListener(clone);
1068 }
1069
1070 if (this.renderer != null) {
1071 clone.renderer
1072 = (PolarItemRenderer) ObjectUtilities.clone(this.renderer);
1073 }
1074
1075 clone.cornerTextItems = new ArrayList(this.cornerTextItems);
1076
1077 return clone;
1078 }
1079
1080 /**
1081 * Provides serialization support.
1082 *
1083 * @param stream the output stream.
1084 *
1085 * @throws IOException if there is an I/O error.
1086 */
1087 private void writeObject(ObjectOutputStream stream) throws IOException {
1088 stream.defaultWriteObject();
1089 SerialUtilities.writeStroke(this.angleGridlineStroke, stream);
1090 SerialUtilities.writePaint(this.angleGridlinePaint, stream);
1091 SerialUtilities.writeStroke(this.radiusGridlineStroke, stream);
1092 SerialUtilities.writePaint(this.radiusGridlinePaint, stream);
1093 SerialUtilities.writePaint(this.angleLabelPaint, stream);
1094 }
1095
1096 /**
1097 * Provides serialization support.
1098 *
1099 * @param stream the input stream.
1100 *
1101 * @throws IOException if there is an I/O error.
1102 * @throws ClassNotFoundException if there is a classpath problem.
1103 */
1104 private void readObject(ObjectInputStream stream)
1105 throws IOException, ClassNotFoundException {
1106
1107 stream.defaultReadObject();
1108 this.angleGridlineStroke = SerialUtilities.readStroke(stream);
1109 this.angleGridlinePaint = SerialUtilities.readPaint(stream);
1110 this.radiusGridlineStroke = SerialUtilities.readStroke(stream);
1111 this.radiusGridlinePaint = SerialUtilities.readPaint(stream);
1112 this.angleLabelPaint = SerialUtilities.readPaint(stream);
1113
1114 if (this.axis != null) {
1115 this.axis.setPlot(this);
1116 this.axis.addChangeListener(this);
1117 }
1118
1119 if (this.dataset != null) {
1120 this.dataset.addChangeListener(this);
1121 }
1122 }
1123
1124 /**
1125 * This method is required by the {@link Zoomable} interface, but since
1126 * the plot does not have any domain axes, it does nothing.
1127 *
1128 * @param factor the zoom factor.
1129 * @param state the plot state.
1130 * @param source the source point (in Java2D coordinates).
1131 */
1132 public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1133 Point2D source) {
1134 // do nothing
1135 }
1136
1137 /**
1138 * This method is required by the {@link Zoomable} interface, but since
1139 * the plot does not have any domain axes, it does nothing.
1140 *
1141 * @param factor the zoom factor.
1142 * @param state the plot state.
1143 * @param source the source point (in Java2D coordinates).
1144 * @param useAnchor use source point as zoom anchor?
1145 *
1146 * @since 1.0.7
1147 */
1148 public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1149 Point2D source, boolean useAnchor) {
1150 // do nothing
1151 }
1152
1153 /**
1154 * This method is required by the {@link Zoomable} interface, but since
1155 * the plot does not have any domain axes, it does nothing.
1156 *
1157 * @param lowerPercent the new lower bound.
1158 * @param upperPercent the new upper bound.
1159 * @param state the plot state.
1160 * @param source the source point (in Java2D coordinates).
1161 */
1162 public void zoomDomainAxes(double lowerPercent, double upperPercent,
1163 PlotRenderingInfo state, Point2D source) {
1164 // do nothing
1165 }
1166
1167 /**
1168 * Multiplies the range on the range axis/axes by the specified factor.
1169 *
1170 * @param factor the zoom factor.
1171 * @param state the plot state.
1172 * @param source the source point (in Java2D coordinates).
1173 */
1174 public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1175 Point2D source) {
1176 zoom(factor);
1177 }
1178
1179 /**
1180 * Multiplies the range on the range axis by the specified factor.
1181 *
1182 * @param factor the zoom factor.
1183 * @param info the plot rendering info.
1184 * @param source the source point (in Java2D space).
1185 * @param useAnchor use source point as zoom anchor?
1186 *
1187 * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean)
1188 *
1189 * @since 1.0.7
1190 */
1191 public void zoomRangeAxes(double factor, PlotRenderingInfo info,
1192 Point2D source, boolean useAnchor) {
1193
1194 if (useAnchor) {
1195 // get the source coordinate - this plot has always a VERTICAL
1196 // orientation
1197 double sourceX = source.getX();
1198 double anchorX = this.axis.java2DToValue(sourceX,
1199 info.getDataArea(), RectangleEdge.BOTTOM);
1200 this.axis.resizeRange(factor, anchorX);
1201 }
1202 else {
1203 this.axis.resizeRange(factor);
1204 }
1205
1206 }
1207
1208 /**
1209 * Zooms in on the range axes.
1210 *
1211 * @param lowerPercent the new lower bound.
1212 * @param upperPercent the new upper bound.
1213 * @param state the plot state.
1214 * @param source the source point (in Java2D coordinates).
1215 */
1216 public void zoomRangeAxes(double lowerPercent, double upperPercent,
1217 PlotRenderingInfo state, Point2D source) {
1218 zoom((upperPercent + lowerPercent) / 2.0);
1219 }
1220
1221 /**
1222 * Returns <code>false</code> always.
1223 *
1224 * @return <code>false</code> always.
1225 */
1226 public boolean isDomainZoomable() {
1227 return false;
1228 }
1229
1230 /**
1231 * Returns <code>true</code> to indicate that the range axis is zoomable.
1232 *
1233 * @return <code>true</code>.
1234 */
1235 public boolean isRangeZoomable() {
1236 return true;
1237 }
1238
1239 /**
1240 * Returns the orientation of the plot.
1241 *
1242 * @return The orientation.
1243 */
1244 public PlotOrientation getOrientation() {
1245 return PlotOrientation.HORIZONTAL;
1246 }
1247
1248 /**
1249 * Returns the upper bound of the radius axis.
1250 *
1251 * @return The upper bound.
1252 */
1253 public double getMaxRadius() {
1254 return this.axis.getUpperBound();
1255 }
1256
1257 /**
1258 * Translates a (theta, radius) pair into Java2D coordinates. If
1259 * <code>radius</code> is less than the lower bound of the axis, then
1260 * this method returns the centre point.
1261 *
1262 * @param angleDegrees the angle in degrees.
1263 * @param radius the radius.
1264 * @param dataArea the data area.
1265 *
1266 * @return A point in Java2D space.
1267 */
1268 public Point translateValueThetaRadiusToJava2D(double angleDegrees,
1269 double radius,
1270 Rectangle2D dataArea) {
1271
1272 double radians = Math.toRadians(angleDegrees - 90.0);
1273
1274 double minx = dataArea.getMinX() + MARGIN;
1275 double maxx = dataArea.getMaxX() - MARGIN;
1276 double miny = dataArea.getMinY() + MARGIN;
1277 double maxy = dataArea.getMaxY() - MARGIN;
1278
1279 double lengthX = maxx - minx;
1280 double lengthY = maxy - miny;
1281 double length = Math.min(lengthX, lengthY);
1282
1283 double midX = minx + lengthX / 2.0;
1284 double midY = miny + lengthY / 2.0;
1285
1286 double axisMin = this.axis.getLowerBound();
1287 double axisMax = getMaxRadius();
1288 double adjustedRadius = Math.max(radius, axisMin);
1289
1290 double xv = length / 2.0 * Math.cos(radians);
1291 double yv = length / 2.0 * Math.sin(radians);
1292
1293 float x = (float) (midX + (xv * (adjustedRadius - axisMin)
1294 / (axisMax - axisMin)));
1295 float y = (float) (midY + (yv * (adjustedRadius - axisMin)
1296 / (axisMax - axisMin)));
1297
1298 int ix = Math.round(x);
1299 int iy = Math.round(y);
1300
1301 Point p = new Point(ix, iy);
1302 return p;
1303
1304 }
1305
1306 }