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