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