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