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 }