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