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