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