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 * PolarPlot.java
029 * --------------
030 * (C) Copyright 2004-2007, 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 *
035 * $Id: PolarPlot.java,v 1.13.2.8 2007/03/21 10:37:20 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG);
040 * 07-Apr-2004 : Changed text bounds calculation (DG);
041 * 05-May-2005 : Updated draw() method parameters (DG);
042 * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG);
043 * 25-Oct-2005 : Implemented Zoomable (DG);
044 * ------------- JFREECHART 1.0.x ---------------------------------------------
045 * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG);
046 * 21-Mar-2007 : Fixed serialization bug (DG);
047 *
048 */
049
050 package org.jfree.chart.plot;
051
052
053 import java.awt.AlphaComposite;
054 import java.awt.BasicStroke;
055 import java.awt.Color;
056 import java.awt.Composite;
057 import java.awt.Font;
058 import java.awt.FontMetrics;
059 import java.awt.Graphics2D;
060 import java.awt.Paint;
061 import java.awt.Point;
062 import java.awt.Shape;
063 import java.awt.Stroke;
064 import java.awt.geom.Point2D;
065 import java.awt.geom.Rectangle2D;
066 import java.io.IOException;
067 import java.io.ObjectInputStream;
068 import java.io.ObjectOutputStream;
069 import java.io.Serializable;
070 import java.util.ArrayList;
071 import java.util.Iterator;
072 import java.util.List;
073 import java.util.ResourceBundle;
074
075 import org.jfree.chart.LegendItem;
076 import org.jfree.chart.LegendItemCollection;
077 import org.jfree.chart.axis.AxisState;
078 import org.jfree.chart.axis.NumberTick;
079 import org.jfree.chart.axis.ValueAxis;
080 import org.jfree.chart.event.PlotChangeEvent;
081 import org.jfree.chart.event.RendererChangeEvent;
082 import org.jfree.chart.event.RendererChangeListener;
083 import org.jfree.chart.renderer.PolarItemRenderer;
084 import org.jfree.data.Range;
085 import org.jfree.data.general.DatasetChangeEvent;
086 import org.jfree.data.general.DatasetUtilities;
087 import org.jfree.data.xy.XYDataset;
088 import org.jfree.io.SerialUtilities;
089 import org.jfree.text.TextUtilities;
090 import org.jfree.ui.RectangleEdge;
091 import org.jfree.ui.RectangleInsets;
092 import org.jfree.ui.TextAnchor;
093 import org.jfree.util.ObjectUtilities;
094 import org.jfree.util.PaintUtilities;
095
096
097 /**
098 * Plots data that is in (theta, radius) pairs where
099 * theta equal to zero is due north and increases clockwise.
100 */
101 public class PolarPlot extends Plot implements ValueAxisPlot,
102 Zoomable,
103 RendererChangeListener,
104 Cloneable,
105 Serializable {
106
107 /** For serialization. */
108 private static final long serialVersionUID = 3794383185924179525L;
109
110 /** The default margin. */
111 private static final int MARGIN = 20;
112
113 /** The annotation margin. */
114 private static final double ANNOTATION_MARGIN = 7.0;
115
116 /** The default grid line stroke. */
117 public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(
118 0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
119 0.0f, new float[]{2.0f, 2.0f}, 0.0f);
120
121 /** The default grid line paint. */
122 public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray;
123
124 /** The resourceBundle for the localization. */
125 protected static ResourceBundle localizationResources
126 = ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
127
128 /** The angles that are marked with gridlines. */
129 private List angleTicks;
130
131 /** The axis (used for the y-values). */
132 private ValueAxis axis;
133
134 /** The dataset. */
135 private XYDataset dataset;
136
137 /**
138 * Object responsible for drawing the visual representation of each point
139 * on the plot.
140 */
141 private PolarItemRenderer renderer;
142
143 /** A flag that controls whether or not the angle labels are visible. */
144 private boolean angleLabelsVisible = true;
145
146 /** The font used to display the angle labels - never null. */
147 private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12);
148
149 /** The paint used to display the angle labels. */
150 private transient Paint angleLabelPaint = Color.black;
151
152 /** A flag that controls whether the angular grid-lines are visible. */
153 private boolean angleGridlinesVisible;
154
155 /** The stroke used to draw the angular grid-lines. */
156 private transient Stroke angleGridlineStroke;
157
158 /** The paint used to draw the angular grid-lines. */
159 private transient Paint angleGridlinePaint;
160
161 /** A flag that controls whether the radius grid-lines are visible. */
162 private boolean radiusGridlinesVisible;
163
164 /** The stroke used to draw the radius grid-lines. */
165 private transient Stroke radiusGridlineStroke;
166
167 /** The paint used to draw the radius grid-lines. */
168 private transient Paint radiusGridlinePaint;
169
170 /** The annotations for the plot. */
171 private List cornerTextItems = new ArrayList();
172
173 /**
174 * Default constructor.
175 */
176 public PolarPlot() {
177 this(null, null, null);
178 }
179
180 /**
181 * Creates a new plot.
182 *
183 * @param dataset the dataset (<code>null</code> permitted).
184 * @param radiusAxis the radius axis (<code>null</code> permitted).
185 * @param renderer the renderer (<code>null</code> permitted).
186 */
187 public PolarPlot(XYDataset dataset,
188 ValueAxis radiusAxis,
189 PolarItemRenderer renderer) {
190
191 super();
192
193 this.dataset = dataset;
194 if (this.dataset != null) {
195 this.dataset.addChangeListener(this);
196 }
197
198 this.angleTicks = new java.util.ArrayList();
199 this.angleTicks.add(new NumberTick(new Double(0.0), "0",
200 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
201 this.angleTicks.add(new NumberTick(new Double(45.0), "45",
202 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
203 this.angleTicks.add(new NumberTick(new Double(90.0), "90",
204 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
205 this.angleTicks.add(new NumberTick(new Double(135.0), "135",
206 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
207 this.angleTicks.add(new NumberTick(new Double(180.0), "180",
208 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
209 this.angleTicks.add(new NumberTick(new Double(225.0), "225",
210 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
211 this.angleTicks.add(new NumberTick(new Double(270.0), "270",
212 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
213 this.angleTicks.add(new NumberTick(new Double(315.0), "315",
214 TextAnchor.CENTER, TextAnchor.CENTER, 0.0));
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 this.notifyListeners(new PlotChangeEvent(this));
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 this.notifyListeners(new PlotChangeEvent(this));
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 this.notifyListeners(new PlotChangeEvent(this));
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 notifyListeners(new PlotChangeEvent(this));
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
396 notifyListeners(new PlotChangeEvent(this));
397 }
398
399 /**
400 * Returns a flag that controls whether or not the angle labels are visible.
401 *
402 * @return A boolean.
403 *
404 * @see #setAngleLabelsVisible(boolean)
405 */
406 public boolean isAngleLabelsVisible() {
407 return this.angleLabelsVisible;
408 }
409
410 /**
411 * Sets the flag that controls whether or not the angle labels are visible,
412 * and sends a {@link PlotChangeEvent} to all registered listeners.
413 *
414 * @param visible the flag.
415 *
416 * @see #isAngleLabelsVisible()
417 */
418 public void setAngleLabelsVisible(boolean visible) {
419 if (this.angleLabelsVisible != visible) {
420 this.angleLabelsVisible = visible;
421 notifyListeners(new PlotChangeEvent(this));
422 }
423 }
424
425 /**
426 * Returns the font used to display the angle labels.
427 *
428 * @return A font (never <code>null</code>).
429 *
430 * @see #setAngleLabelFont(Font)
431 */
432 public Font getAngleLabelFont() {
433 return this.angleLabelFont;
434 }
435
436 /**
437 * Sets the font used to display the angle labels and sends a
438 * {@link PlotChangeEvent} to all registered listeners.
439 *
440 * @param font the font (<code>null</code> not permitted).
441 *
442 * @see #getAngleLabelFont()
443 */
444 public void setAngleLabelFont(Font font) {
445 if (font == null) {
446 throw new IllegalArgumentException("Null 'font' argument.");
447 }
448 this.angleLabelFont = font;
449 notifyListeners(new PlotChangeEvent(this));
450 }
451
452 /**
453 * Returns the paint used to display the angle labels.
454 *
455 * @return A paint (never <code>null</code>).
456 *
457 * @see #setAngleLabelPaint(Paint)
458 */
459 public Paint getAngleLabelPaint() {
460 return this.angleLabelPaint;
461 }
462
463 /**
464 * Sets the paint used to display the angle labels and sends a
465 * {@link PlotChangeEvent} to all registered listeners.
466 *
467 * @param paint the paint (<code>null</code> not permitted).
468 */
469 public void setAngleLabelPaint(Paint paint) {
470 if (paint == null) {
471 throw new IllegalArgumentException("Null 'paint' argument.");
472 }
473 this.angleLabelPaint = paint;
474 notifyListeners(new PlotChangeEvent(this));
475 }
476
477 /**
478 * Returns <code>true</code> if the angular gridlines are visible, and
479 * <code>false<code> otherwise.
480 *
481 * @return <code>true</code> or <code>false</code>.
482 *
483 * @see #setAngleGridlinesVisible(boolean)
484 */
485 public boolean isAngleGridlinesVisible() {
486 return this.angleGridlinesVisible;
487 }
488
489 /**
490 * Sets the flag that controls whether or not the angular grid-lines are
491 * visible.
492 * <p>
493 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
494 * registered listeners.
495 *
496 * @param visible the new value of the flag.
497 *
498 * @see #isAngleGridlinesVisible()
499 */
500 public void setAngleGridlinesVisible(boolean visible) {
501 if (this.angleGridlinesVisible != visible) {
502 this.angleGridlinesVisible = visible;
503 notifyListeners(new PlotChangeEvent(this));
504 }
505 }
506
507 /**
508 * Returns the stroke for the grid-lines (if any) plotted against the
509 * angular axis.
510 *
511 * @return The stroke (possibly <code>null</code>).
512 *
513 * @see #setAngleGridlineStroke(Stroke)
514 */
515 public Stroke getAngleGridlineStroke() {
516 return this.angleGridlineStroke;
517 }
518
519 /**
520 * Sets the stroke for the grid lines plotted against the angular axis and
521 * sends a {@link PlotChangeEvent} to all registered listeners.
522 * <p>
523 * If you set this to <code>null</code>, no grid lines will be drawn.
524 *
525 * @param stroke the stroke (<code>null</code> permitted).
526 *
527 * @see #getAngleGridlineStroke()
528 */
529 public void setAngleGridlineStroke(Stroke stroke) {
530 this.angleGridlineStroke = stroke;
531 notifyListeners(new PlotChangeEvent(this));
532 }
533
534 /**
535 * Returns the paint for the grid lines (if any) plotted against the
536 * angular axis.
537 *
538 * @return The paint (possibly <code>null</code>).
539 *
540 * @see #setAngleGridlinePaint(Paint)
541 */
542 public Paint getAngleGridlinePaint() {
543 return this.angleGridlinePaint;
544 }
545
546 /**
547 * Sets the paint for the grid lines plotted against the angular axis.
548 * <p>
549 * If you set this to <code>null</code>, no grid lines will be drawn.
550 *
551 * @param paint the paint (<code>null</code> permitted).
552 *
553 * @see #getAngleGridlinePaint()
554 */
555 public void setAngleGridlinePaint(Paint paint) {
556 this.angleGridlinePaint = paint;
557 notifyListeners(new PlotChangeEvent(this));
558 }
559
560 /**
561 * Returns <code>true</code> if the radius axis grid is visible, and
562 * <code>false<code> otherwise.
563 *
564 * @return <code>true</code> or <code>false</code>.
565 *
566 * @see #setRadiusGridlinesVisible(boolean)
567 */
568 public boolean isRadiusGridlinesVisible() {
569 return this.radiusGridlinesVisible;
570 }
571
572 /**
573 * Sets the flag that controls whether or not the radius axis grid lines
574 * are visible.
575 * <p>
576 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all
577 * registered listeners.
578 *
579 * @param visible the new value of the flag.
580 *
581 * @see #isRadiusGridlinesVisible()
582 */
583 public void setRadiusGridlinesVisible(boolean visible) {
584 if (this.radiusGridlinesVisible != visible) {
585 this.radiusGridlinesVisible = visible;
586 notifyListeners(new PlotChangeEvent(this));
587 }
588 }
589
590 /**
591 * Returns the stroke for the grid lines (if any) plotted against the
592 * radius axis.
593 *
594 * @return The stroke (possibly <code>null</code>).
595 *
596 * @see #setRadiusGridlineStroke(Stroke)
597 */
598 public Stroke getRadiusGridlineStroke() {
599 return this.radiusGridlineStroke;
600 }
601
602 /**
603 * Sets the stroke for the grid lines plotted against the radius axis and
604 * sends a {@link PlotChangeEvent} to all registered listeners.
605 * <p>
606 * If you set this to <code>null</code>, no grid lines will be drawn.
607 *
608 * @param stroke the stroke (<code>null</code> permitted).
609 *
610 * @see #getRadiusGridlineStroke()
611 */
612 public void setRadiusGridlineStroke(Stroke stroke) {
613 this.radiusGridlineStroke = stroke;
614 notifyListeners(new PlotChangeEvent(this));
615 }
616
617 /**
618 * Returns the paint for the grid lines (if any) plotted against the radius
619 * axis.
620 *
621 * @return The paint (possibly <code>null</code>).
622 *
623 * @see #setRadiusGridlinePaint(Paint)
624 */
625 public Paint getRadiusGridlinePaint() {
626 return this.radiusGridlinePaint;
627 }
628
629 /**
630 * Sets the paint 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 paint the paint (<code>null</code> permitted).
636 *
637 * @see #getRadiusGridlinePaint()
638 */
639 public void setRadiusGridlinePaint(Paint paint) {
640 this.radiusGridlinePaint = paint;
641 notifyListeners(new PlotChangeEvent(this));
642 }
643
644 /**
645 * Draws the plot on a Java 2D graphics device (such as the screen or a
646 * printer).
647 * <P>
648 * This plot relies on a {@link PolarItemRenderer} to draw each
649 * item in the plot. This allows the visual representation of the data to
650 * be changed easily.
651 * <P>
652 * The optional info argument collects information about the rendering of
653 * the plot (dimensions, tooltip information etc). Just pass in
654 * <code>null</code> if you do not need this information.
655 *
656 * @param g2 the graphics device.
657 * @param area the area within which the plot (including axes and
658 * labels) should be drawn.
659 * @param anchor the anchor point (<code>null</code> permitted).
660 * @param parentState ignored.
661 * @param info collects chart drawing information (<code>null</code>
662 * permitted).
663 */
664 public void draw(Graphics2D g2,
665 Rectangle2D area,
666 Point2D anchor,
667 PlotState parentState,
668 PlotRenderingInfo info) {
669
670 // if the plot area is too small, just return...
671 boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW);
672 boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW);
673 if (b1 || b2) {
674 return;
675 }
676
677 // record the plot area...
678 if (info != null) {
679 info.setPlotArea(area);
680 }
681
682 // adjust the drawing area for the plot insets (if any)...
683 RectangleInsets insets = getInsets();
684 insets.trim(area);
685
686 Rectangle2D dataArea = area;
687 if (info != null) {
688 info.setDataArea(dataArea);
689 }
690
691 // draw the plot background and axes...
692 drawBackground(g2, dataArea);
693 double h = Math.min(dataArea.getWidth() / 2.0,
694 dataArea.getHeight() / 2.0) - MARGIN;
695 Rectangle2D quadrant = new Rectangle2D.Double(dataArea.getCenterX(),
696 dataArea.getCenterY(), h, h);
697 AxisState state = drawAxis(g2, area, quadrant);
698 if (this.renderer != null) {
699 Shape originalClip = g2.getClip();
700 Composite originalComposite = g2.getComposite();
701
702 g2.clip(dataArea);
703 g2.setComposite(AlphaComposite.getInstance(
704 AlphaComposite.SRC_OVER, getForegroundAlpha()));
705
706 drawGridlines(g2, dataArea, this.angleTicks, state.getTicks());
707
708 // draw...
709 render(g2, dataArea, info);
710
711 g2.setClip(originalClip);
712 g2.setComposite(originalComposite);
713 }
714 drawOutline(g2, dataArea);
715 drawCornerTextItems(g2, dataArea);
716 }
717
718 /**
719 * Draws the corner text items.
720 *
721 * @param g2 the drawing surface.
722 * @param area the area.
723 */
724 protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) {
725 if (this.cornerTextItems.isEmpty()) {
726 return;
727 }
728
729 g2.setColor(Color.black);
730 double width = 0.0;
731 double height = 0.0;
732 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
733 String msg = (String) it.next();
734 FontMetrics fm = g2.getFontMetrics();
735 Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm);
736 width = Math.max(width, bounds.getWidth());
737 height += bounds.getHeight();
738 }
739
740 double xadj = ANNOTATION_MARGIN * 2.0;
741 double yadj = ANNOTATION_MARGIN;
742 width += xadj;
743 height += yadj;
744
745 double x = area.getMaxX() - width;
746 double y = area.getMaxY() - height;
747 g2.drawRect((int) x, (int) y, (int) width, (int) height);
748 x += ANNOTATION_MARGIN;
749 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) {
750 String msg = (String) it.next();
751 Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2,
752 g2.getFontMetrics());
753 y += bounds.getHeight();
754 g2.drawString(msg, (int) x, (int) y);
755 }
756 }
757
758 /**
759 * A utility method for drawing the axes.
760 *
761 * @param g2 the graphics device.
762 * @param plotArea the plot area.
763 * @param dataArea the data area.
764 *
765 * @return A map containing the axis states.
766 */
767 protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea,
768 Rectangle2D dataArea) {
769 return this.axis.draw(g2, dataArea.getMinY(), plotArea, dataArea,
770 RectangleEdge.TOP, null);
771 }
772
773 /**
774 * Draws a representation of the data within the dataArea region, using the
775 * current m_Renderer.
776 *
777 * @param g2 the graphics device.
778 * @param dataArea the region in which the data is to be drawn.
779 * @param info an optional object for collection dimension
780 * information (<code>null</code> permitted).
781 */
782 protected void render(Graphics2D g2,
783 Rectangle2D dataArea,
784 PlotRenderingInfo info) {
785
786 // now get the data and plot it (the visual representation will depend
787 // on the m_Renderer that has been set)...
788 if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
789 int seriesCount = this.dataset.getSeriesCount();
790 for (int series = 0; series < seriesCount; series++) {
791 this.renderer.drawSeries(g2, dataArea, info, this,
792 this.dataset, series);
793 }
794 }
795 else {
796 drawNoDataMessage(g2, dataArea);
797 }
798 }
799
800 /**
801 * Draws the gridlines for the plot, if they are visible.
802 *
803 * @param g2 the graphics device.
804 * @param dataArea the data area.
805 * @param angularTicks the ticks for the angular axis.
806 * @param radialTicks the ticks for the radial axis.
807 */
808 protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea,
809 List angularTicks, List radialTicks) {
810
811 // no renderer, no gridlines...
812 if (this.renderer == null) {
813 return;
814 }
815
816 // draw the domain grid lines, if any...
817 if (isAngleGridlinesVisible()) {
818 Stroke gridStroke = getAngleGridlineStroke();
819 Paint gridPaint = getAngleGridlinePaint();
820 if ((gridStroke != null) && (gridPaint != null)) {
821 this.renderer.drawAngularGridLines(g2, this, angularTicks,
822 dataArea);
823 }
824 }
825
826 // draw the radius grid lines, if any...
827 if (isRadiusGridlinesVisible()) {
828 Stroke gridStroke = getRadiusGridlineStroke();
829 Paint gridPaint = getRadiusGridlinePaint();
830 if ((gridStroke != null) && (gridPaint != null)) {
831 this.renderer.drawRadialGridLines(g2, this, this.axis,
832 radialTicks, dataArea);
833 }
834 }
835 }
836
837 /**
838 * Zooms the axis ranges by the specified percentage about the anchor point.
839 *
840 * @param percent the amount of the zoom.
841 */
842 public void zoom(double percent) {
843 if (percent > 0.0) {
844 double radius = getMaxRadius();
845 double scaledRadius = radius * percent;
846 this.axis.setUpperBound(scaledRadius);
847 getAxis().setAutoRange(false);
848 }
849 else {
850 getAxis().setAutoRange(true);
851 }
852 }
853
854 /**
855 * Returns the range for the specified axis.
856 *
857 * @param axis the axis.
858 *
859 * @return The range.
860 */
861 public Range getDataRange(ValueAxis axis) {
862 Range result = null;
863 if (this.dataset != null) {
864 result = Range.combine(result,
865 DatasetUtilities.findRangeBounds(this.dataset));
866 }
867 return result;
868 }
869
870 /**
871 * Receives notification of a change to the plot's m_Dataset.
872 * <P>
873 * The axis ranges are updated if necessary.
874 *
875 * @param event information about the event (not used here).
876 */
877 public void datasetChanged(DatasetChangeEvent event) {
878
879 if (this.axis != null) {
880 this.axis.configure();
881 }
882
883 if (getParent() != null) {
884 getParent().datasetChanged(event);
885 }
886 else {
887 super.datasetChanged(event);
888 }
889 }
890
891 /**
892 * Notifies all registered listeners of a property change.
893 * <P>
894 * One source of property change events is the plot's m_Renderer.
895 *
896 * @param event information about the property change.
897 */
898 public void rendererChanged(RendererChangeEvent event) {
899 notifyListeners(new PlotChangeEvent(this));
900 }
901
902 /**
903 * Returns the number of series in the dataset for this plot. If the
904 * dataset is <code>null</code>, the method returns 0.
905 *
906 * @return The series count.
907 */
908 public int getSeriesCount() {
909 int result = 0;
910
911 if (this.dataset != null) {
912 result = this.dataset.getSeriesCount();
913 }
914 return result;
915 }
916
917 /**
918 * Returns the legend items for the plot. Each legend item is generated by
919 * the plot's m_Renderer, since the m_Renderer is responsible for the visual
920 * representation of the data.
921 *
922 * @return The legend items.
923 */
924 public LegendItemCollection getLegendItems() {
925 LegendItemCollection result = new LegendItemCollection();
926
927 // get the legend items for the main m_Dataset...
928 if (this.dataset != null) {
929 if (this.renderer != null) {
930 int seriesCount = this.dataset.getSeriesCount();
931 for (int i = 0; i < seriesCount; i++) {
932 LegendItem item = this.renderer.getLegendItem(i);
933 result.add(item);
934 }
935 }
936 }
937 return result;
938 }
939
940 /**
941 * Tests this plot for equality with another object.
942 *
943 * @param obj the object (<code>null</code> permitted).
944 *
945 * @return <code>true</code> or <code>false</code>.
946 */
947 public boolean equals(Object obj) {
948 if (obj == this) {
949 return true;
950 }
951 if (!(obj instanceof PolarPlot)) {
952 return false;
953 }
954 PolarPlot that = (PolarPlot) obj;
955 if (!ObjectUtilities.equal(this.axis, that.axis)) {
956 return false;
957 }
958 if (!ObjectUtilities.equal(this.renderer, that.renderer)) {
959 return false;
960 }
961 if (this.angleGridlinesVisible != that.angleGridlinesVisible) {
962 return false;
963 }
964 if (this.angleLabelsVisible != that.angleLabelsVisible) {
965 return false;
966 }
967 if (!this.angleLabelFont.equals(that.angleLabelFont)) {
968 return false;
969 }
970 if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) {
971 return false;
972 }
973 if (!ObjectUtilities.equal(this.angleGridlineStroke,
974 that.angleGridlineStroke)) {
975 return false;
976 }
977 if (!PaintUtilities.equal(
978 this.angleGridlinePaint, that.angleGridlinePaint
979 )) {
980 return false;
981 }
982 if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) {
983 return false;
984 }
985 if (!ObjectUtilities.equal(this.radiusGridlineStroke,
986 that.radiusGridlineStroke)) {
987 return false;
988 }
989 if (!PaintUtilities.equal(this.radiusGridlinePaint,
990 that.radiusGridlinePaint)) {
991 return false;
992 }
993 if (!this.cornerTextItems.equals(that.cornerTextItems)) {
994 return false;
995 }
996 return super.equals(obj);
997 }
998
999 /**
1000 * Returns a clone of the plot.
1001 *
1002 * @return A clone.
1003 *
1004 * @throws CloneNotSupportedException this can occur if some component of
1005 * the plot cannot be cloned.
1006 */
1007 public Object clone() throws CloneNotSupportedException {
1008
1009 PolarPlot clone = (PolarPlot) super.clone();
1010 if (this.axis != null) {
1011 clone.axis = (ValueAxis) ObjectUtilities.clone(this.axis);
1012 clone.axis.setPlot(clone);
1013 clone.axis.addChangeListener(clone);
1014 }
1015
1016 if (clone.dataset != null) {
1017 clone.dataset.addChangeListener(clone);
1018 }
1019
1020 if (this.renderer != null) {
1021 clone.renderer
1022 = (PolarItemRenderer) ObjectUtilities.clone(this.renderer);
1023 }
1024
1025 clone.cornerTextItems = new ArrayList(this.cornerTextItems);
1026
1027 return clone;
1028 }
1029
1030 /**
1031 * Provides serialization support.
1032 *
1033 * @param stream the output stream.
1034 *
1035 * @throws IOException if there is an I/O error.
1036 */
1037 private void writeObject(ObjectOutputStream stream) throws IOException {
1038 stream.defaultWriteObject();
1039 SerialUtilities.writeStroke(this.angleGridlineStroke, stream);
1040 SerialUtilities.writePaint(this.angleGridlinePaint, stream);
1041 SerialUtilities.writeStroke(this.radiusGridlineStroke, stream);
1042 SerialUtilities.writePaint(this.radiusGridlinePaint, stream);
1043 SerialUtilities.writePaint(this.angleLabelPaint, stream);
1044 }
1045
1046 /**
1047 * Provides serialization support.
1048 *
1049 * @param stream the input stream.
1050 *
1051 * @throws IOException if there is an I/O error.
1052 * @throws ClassNotFoundException if there is a classpath problem.
1053 */
1054 private void readObject(ObjectInputStream stream)
1055 throws IOException, ClassNotFoundException {
1056
1057 stream.defaultReadObject();
1058 this.angleGridlineStroke = SerialUtilities.readStroke(stream);
1059 this.angleGridlinePaint = SerialUtilities.readPaint(stream);
1060 this.radiusGridlineStroke = SerialUtilities.readStroke(stream);
1061 this.radiusGridlinePaint = SerialUtilities.readPaint(stream);
1062 this.angleLabelPaint = SerialUtilities.readPaint(stream);
1063
1064 if (this.axis != null) {
1065 this.axis.setPlot(this);
1066 this.axis.addChangeListener(this);
1067 }
1068
1069 if (this.dataset != null) {
1070 this.dataset.addChangeListener(this);
1071 }
1072 }
1073
1074 /**
1075 * This method is required by the {@link Zoomable} interface, but since
1076 * the plot does not have any domain axes, it does nothing.
1077 *
1078 * @param factor the zoom factor.
1079 * @param state the plot state.
1080 * @param source the source point (in Java2D coordinates).
1081 */
1082 public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1083 Point2D source) {
1084 // do nothing
1085 }
1086
1087 /**
1088 * This method is required by the {@link Zoomable} interface, but since
1089 * the plot does not have any domain axes, it does nothing.
1090 *
1091 * @param lowerPercent the new lower bound.
1092 * @param upperPercent the new upper bound.
1093 * @param state the plot state.
1094 * @param source the source point (in Java2D coordinates).
1095 */
1096 public void zoomDomainAxes(double lowerPercent, double upperPercent,
1097 PlotRenderingInfo state, Point2D source) {
1098 // do nothing
1099 }
1100
1101 /**
1102 * Multiplies the range on the range axis/axes by the specified factor.
1103 *
1104 * @param factor the zoom factor.
1105 * @param state the plot state.
1106 * @param source the source point (in Java2D coordinates).
1107 */
1108 public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1109 Point2D source) {
1110 zoom(factor);
1111 }
1112
1113 /**
1114 * Zooms in on the range axes.
1115 *
1116 * @param lowerPercent the new lower bound.
1117 * @param upperPercent the new upper bound.
1118 * @param state the plot state.
1119 * @param source the source point (in Java2D coordinates).
1120 */
1121 public void zoomRangeAxes(double lowerPercent, double upperPercent,
1122 PlotRenderingInfo state, Point2D source) {
1123 zoom((upperPercent + lowerPercent) / 2.0);
1124 }
1125
1126 /**
1127 * Returns <code>false</code> always.
1128 *
1129 * @return <code>false</code> always.
1130 */
1131 public boolean isDomainZoomable() {
1132 return false;
1133 }
1134
1135 /**
1136 * Returns <code>true</code> to indicate that the range axis is zoomable.
1137 *
1138 * @return <code>true</code>.
1139 */
1140 public boolean isRangeZoomable() {
1141 return true;
1142 }
1143
1144 /**
1145 * Returns the orientation of the plot.
1146 *
1147 * @return The orientation.
1148 */
1149 public PlotOrientation getOrientation() {
1150 return PlotOrientation.HORIZONTAL;
1151 }
1152
1153 /**
1154 * Returns the upper bound of the radius axis.
1155 *
1156 * @return The upper bound.
1157 */
1158 public double getMaxRadius() {
1159 return this.axis.getUpperBound();
1160 }
1161
1162 /**
1163 * Translates a (theta, radius) pair into Java2D coordinates. If
1164 * <code>radius</code> is less than the lower bound of the axis, then
1165 * this method returns the centre point.
1166 *
1167 * @param angleDegrees the angle in degrees.
1168 * @param radius the radius.
1169 * @param dataArea the data area.
1170 *
1171 * @return A point in Java2D space.
1172 */
1173 public Point translateValueThetaRadiusToJava2D(double angleDegrees,
1174 double radius,
1175 Rectangle2D dataArea) {
1176
1177 double radians = Math.toRadians(angleDegrees - 90.0);
1178
1179 double minx = dataArea.getMinX() + MARGIN;
1180 double maxx = dataArea.getMaxX() - MARGIN;
1181 double miny = dataArea.getMinY() + MARGIN;
1182 double maxy = dataArea.getMaxY() - MARGIN;
1183
1184 double lengthX = maxx - minx;
1185 double lengthY = maxy - miny;
1186 double length = Math.min(lengthX, lengthY);
1187
1188 double midX = minx + lengthX / 2.0;
1189 double midY = miny + lengthY / 2.0;
1190
1191 double axisMin = this.axis.getLowerBound();
1192 double axisMax = getMaxRadius();
1193 double adjustedRadius = Math.max(radius, axisMin);
1194
1195 double xv = length / 2.0 * Math.cos(radians);
1196 double yv = length / 2.0 * Math.sin(radians);
1197
1198 float x = (float) (midX + (xv * (adjustedRadius - axisMin)
1199 / (axisMax - axisMin)));
1200 float y = (float) (midY + (yv * (adjustedRadius - axisMin)
1201 / (axisMax - axisMin)));
1202
1203 int ix = Math.round(x);
1204 int iy = Math.round(y);
1205
1206 Point p = new Point(ix, iy);
1207 return p;
1208
1209 }
1210
1211 }