001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * -------------- 028 * PolarPlot.java 029 * -------------- 030 * (C) Copyright 2004-2011, by Solution Engineering, Inc. and Contributors. 031 * 032 * Original Author: Daniel Bridenbecker, Solution Engineering, Inc.; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Martin Hoeller (patches 1871902 and 2850344); 035 * 036 * Changes 037 * ------- 038 * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG); 039 * 07-Apr-2004 : Changed text bounds calculation (DG); 040 * 05-May-2005 : Updated draw() method parameters (DG); 041 * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG); 042 * 25-Oct-2005 : Implemented Zoomable (DG); 043 * ------------- JFREECHART 1.0.x --------------------------------------------- 044 * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG); 045 * 21-Mar-2007 : Fixed serialization bug (DG); 046 * 24-Sep-2007 : Implemented new zooming methods (DG); 047 * 17-Feb-2007 : Added angle tick unit attribute (see patch 1871902 by 048 * Martin Hoeller) (DG); 049 * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by 050 * Jess Thrysoee (DG); 051 * 03-Sep-2009 : Applied patch 2850344 by Martin Hoeller (DG); 052 * 27-Nov-2009 : Added support for multiple datasets, renderers and axes (DG); 053 * 09-Dec-2009 : Extended getLegendItems() to handle multiple datasets (DG); 054 * 25-Jun-2010 : Better support for multiple axes (MH); 055 * 03-Oct-2011 : Added support for angleOffset and direction (MH); 056 * 12-Nov-2011 : Fixed bug 3432721, log-axis doesn't work (MH); 057 * 058 */ 059 060 package org.jfree.chart.plot; 061 062 import java.awt.AlphaComposite; 063 import java.awt.BasicStroke; 064 import java.awt.Color; 065 import java.awt.Composite; 066 import java.awt.Font; 067 import java.awt.FontMetrics; 068 import java.awt.Graphics2D; 069 import java.awt.Paint; 070 import java.awt.Point; 071 import java.awt.Shape; 072 import java.awt.Stroke; 073 import java.awt.geom.Point2D; 074 import java.awt.geom.Rectangle2D; 075 import java.io.IOException; 076 import java.io.ObjectInputStream; 077 import java.io.ObjectOutputStream; 078 import java.io.Serializable; 079 import java.util.ArrayList; 080 import java.util.HashSet; 081 import java.util.Iterator; 082 import java.util.List; 083 import java.util.Map; 084 import java.util.ResourceBundle; 085 import java.util.TreeMap; 086 087 import org.jfree.chart.LegendItem; 088 import org.jfree.chart.LegendItemCollection; 089 import org.jfree.chart.axis.Axis; 090 import org.jfree.chart.axis.AxisState; 091 import org.jfree.chart.axis.NumberTick; 092 import org.jfree.chart.axis.NumberTickUnit; 093 import org.jfree.chart.axis.TickUnit; 094 import org.jfree.chart.axis.ValueAxis; 095 import org.jfree.chart.event.PlotChangeEvent; 096 import org.jfree.chart.event.RendererChangeEvent; 097 import org.jfree.chart.event.RendererChangeListener; 098 import org.jfree.chart.renderer.PolarItemRenderer; 099 import org.jfree.chart.util.ResourceBundleWrapper; 100 import org.jfree.data.Range; 101 import org.jfree.data.general.Dataset; 102 import org.jfree.data.general.DatasetChangeEvent; 103 import org.jfree.data.general.DatasetUtilities; 104 import org.jfree.data.xy.XYDataset; 105 import org.jfree.io.SerialUtilities; 106 import org.jfree.text.TextUtilities; 107 import org.jfree.ui.RectangleEdge; 108 import org.jfree.ui.RectangleInsets; 109 import org.jfree.ui.TextAnchor; 110 import org.jfree.util.ObjectList; 111 import org.jfree.util.ObjectUtilities; 112 import org.jfree.util.PaintUtilities; 113 import org.jfree.util.PublicCloneable; 114 115 /** 116 * Plots data that is in (theta, radius) pairs where 117 * theta equal to zero is due north and increases clockwise. 118 */ 119 public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable, 120 RendererChangeListener, Cloneable, Serializable { 121 122 /** For serialization. */ 123 private static final long serialVersionUID = 3794383185924179525L; 124 125 /** The default margin. */ 126 private static final int DEFAULT_MARGIN = 20; 127 128 /** The annotation margin. */ 129 private static final double ANNOTATION_MARGIN = 7.0; 130 131 /** 132 * The default angle tick unit size. 133 * 134 * @since 1.0.10 135 */ 136 public static final double DEFAULT_ANGLE_TICK_UNIT_SIZE = 45.0; 137 138 /** 139 * The default angle offset. 140 * 141 * @since 1.0.14 142 */ 143 public static final double DEFAULT_ANGLE_OFFSET = -90.0; 144 145 /** The default grid line stroke. */ 146 public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke( 147 0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 148 0.0f, new float[]{2.0f, 2.0f}, 0.0f); 149 150 /** The default grid line paint. */ 151 public static final Paint DEFAULT_GRIDLINE_PAINT = Color.gray; 152 153 /** The resourceBundle for the localization. */ 154 protected static ResourceBundle localizationResources 155 = ResourceBundleWrapper.getBundle( 156 "org.jfree.chart.plot.LocalizationBundle"); 157 158 /** The angles that are marked with gridlines. */ 159 private List angleTicks; 160 161 /** The range axis (used for the y-values). */ 162 private ObjectList axes; 163 164 /** The axis locations. */ 165 private ObjectList axisLocations; 166 167 /** Storage for the datasets. */ 168 private ObjectList datasets; 169 170 /** Storage for the renderers. */ 171 private ObjectList renderers; 172 173 /** 174 * The tick unit that controls the spacing between the angular grid lines. 175 * 176 * @since 1.0.10 177 */ 178 private TickUnit angleTickUnit; 179 180 /** 181 * An offset for the angles, to start with 0 degrees at north, east, south 182 * or west. 183 * 184 * @since 1.0.14 185 */ 186 private double angleOffset; 187 188 /** 189 * A flag indicating if the angles increase counterclockwise or clockwise. 190 * 191 * @since 1.0.14 192 */ 193 private boolean counterClockwise; 194 195 /** A flag that controls whether or not the angle labels are visible. */ 196 private boolean angleLabelsVisible = true; 197 198 /** The font used to display the angle labels - never null. */ 199 private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12); 200 201 /** The paint used to display the angle labels. */ 202 private transient Paint angleLabelPaint = Color.black; 203 204 /** A flag that controls whether the angular grid-lines are visible. */ 205 private boolean angleGridlinesVisible; 206 207 /** The stroke used to draw the angular grid-lines. */ 208 private transient Stroke angleGridlineStroke; 209 210 /** The paint used to draw the angular grid-lines. */ 211 private transient Paint angleGridlinePaint; 212 213 /** A flag that controls whether the radius grid-lines are visible. */ 214 private boolean radiusGridlinesVisible; 215 216 /** The stroke used to draw the radius grid-lines. */ 217 private transient Stroke radiusGridlineStroke; 218 219 /** The paint used to draw the radius grid-lines. */ 220 private transient Paint radiusGridlinePaint; 221 222 /** The annotations for the plot. */ 223 private List cornerTextItems = new ArrayList(); 224 225 /** 226 * The actual margin in pixels. 227 * 228 * @since 1.0.14 229 */ 230 private int margin; 231 232 /** 233 * An optional collection of legend items that can be returned by the 234 * getLegendItems() method. 235 */ 236 private LegendItemCollection fixedLegendItems; 237 238 /** 239 * Storage for the mapping between datasets/renderers and range axes. The 240 * keys in the map are Integer objects, corresponding to the dataset 241 * index. The values in the map are List objects containing Integer 242 * objects (corresponding to the axis indices). If the map contains no 243 * entry for a dataset, it is assumed to map to the primary domain axis 244 * (index = 0). 245 */ 246 private Map datasetToAxesMap; 247 248 /** 249 * Default constructor. 250 */ 251 public PolarPlot() { 252 this(null, null, null); 253 } 254 255 /** 256 * Creates a new plot. 257 * 258 * @param dataset the dataset (<code>null</code> permitted). 259 * @param radiusAxis the radius axis (<code>null</code> permitted). 260 * @param renderer the renderer (<code>null</code> permitted). 261 */ 262 public PolarPlot(XYDataset dataset, ValueAxis radiusAxis, 263 PolarItemRenderer renderer) { 264 265 super(); 266 267 this.datasets = new ObjectList(); 268 this.datasets.set(0, dataset); 269 if (dataset != null) { 270 dataset.addChangeListener(this); 271 } 272 this.angleTickUnit = new NumberTickUnit(DEFAULT_ANGLE_TICK_UNIT_SIZE); 273 274 this.axes = new ObjectList(); 275 this.datasetToAxesMap = new TreeMap(); 276 this.axes.set(0, radiusAxis); 277 if (radiusAxis != null) { 278 radiusAxis.setPlot(this); 279 radiusAxis.addChangeListener(this); 280 } 281 282 // define the default locations for up to 8 axes... 283 this.axisLocations = new ObjectList(); 284 this.axisLocations.set(0, PolarAxisLocation.EAST_ABOVE); 285 this.axisLocations.set(1, PolarAxisLocation.NORTH_LEFT); 286 this.axisLocations.set(2, PolarAxisLocation.WEST_BELOW); 287 this.axisLocations.set(3, PolarAxisLocation.SOUTH_RIGHT); 288 this.axisLocations.set(4, PolarAxisLocation.EAST_BELOW); 289 this.axisLocations.set(5, PolarAxisLocation.NORTH_RIGHT); 290 this.axisLocations.set(6, PolarAxisLocation.WEST_ABOVE); 291 this.axisLocations.set(7, PolarAxisLocation.SOUTH_LEFT); 292 293 this.renderers = new ObjectList(); 294 this.renderers.set(0, renderer); 295 if (renderer != null) { 296 renderer.setPlot(this); 297 renderer.addChangeListener(this); 298 } 299 300 this.angleOffset = DEFAULT_ANGLE_OFFSET; 301 this.counterClockwise = false; 302 this.angleGridlinesVisible = true; 303 this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE; 304 this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT; 305 306 this.radiusGridlinesVisible = true; 307 this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE; 308 this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT; 309 this.margin = DEFAULT_MARGIN; 310 } 311 312 /** 313 * Returns the plot type as a string. 314 * 315 * @return A short string describing the type of plot. 316 */ 317 public String getPlotType() { 318 return PolarPlot.localizationResources.getString("Polar_Plot"); 319 } 320 321 /** 322 * Returns the primary axis for the plot. 323 * 324 * @return The primary axis (possibly <code>null</code>). 325 * 326 * @see #setAxis(ValueAxis) 327 */ 328 public ValueAxis getAxis() { 329 return getAxis(0); 330 } 331 332 /** 333 * Returns an axis for the plot. 334 * 335 * @param index the axis index. 336 * 337 * @return The axis (<code>null</code> possible). 338 * 339 * @see #setAxis(int, ValueAxis) 340 * 341 * @since 1.0.14 342 */ 343 public ValueAxis getAxis(int index) { 344 ValueAxis result = null; 345 if (index < this.axes.size()) { 346 result = (ValueAxis) this.axes.get(index); 347 } 348 return result; 349 } 350 351 /** 352 * Sets the primary axis for the plot and sends a {@link PlotChangeEvent} 353 * to all registered listeners. 354 * 355 * @param axis the new primary axis (<code>null</code> permitted). 356 */ 357 public void setAxis(ValueAxis axis) { 358 setAxis(0, axis); 359 } 360 361 /** 362 * Sets an axis for the plot and sends a {@link PlotChangeEvent} to all 363 * registered listeners. 364 * 365 * @param index the axis index. 366 * @param axis the axis (<code>null</code> permitted). 367 * 368 * @see #getAxis(int) 369 * 370 * @since 1.0.14 371 */ 372 public void setAxis(int index, ValueAxis axis) { 373 setAxis(index, axis, true); 374 } 375 376 /** 377 * Sets an axis for the plot and, if requested, sends a 378 * {@link PlotChangeEvent} to all registered listeners. 379 * 380 * @param index the axis index. 381 * @param axis the axis (<code>null</code> permitted). 382 * @param notify notify listeners? 383 * 384 * @see #getAxis(int) 385 * 386 * @since 1.0.14 387 */ 388 public void setAxis(int index, ValueAxis axis, boolean notify) { 389 ValueAxis existing = getAxis(index); 390 if (existing != null) { 391 existing.removeChangeListener(this); 392 } 393 if (axis != null) { 394 axis.setPlot(this); 395 } 396 this.axes.set(index, axis); 397 if (axis != null) { 398 axis.configure(); 399 axis.addChangeListener(this); 400 } 401 if (notify) { 402 fireChangeEvent(); 403 } 404 } 405 406 /** 407 * Returns the location of the primary axis. 408 * 409 * @return The location (never <code>null</code>). 410 * 411 * @see #setAxisLocation(PolarAxisLocation) 412 * 413 * @since 1.0.14 414 */ 415 public PolarAxisLocation getAxisLocation() { 416 return getAxisLocation(0); 417 } 418 419 /** 420 * Returns the location for an axis. 421 * 422 * @param index the axis index. 423 * 424 * @return The location (never <code>null</code>). 425 * 426 * @see #setAxisLocation(int, PolarAxisLocation) 427 * 428 * @since 1.0.14 429 */ 430 public PolarAxisLocation getAxisLocation(int index) { 431 PolarAxisLocation result = null; 432 if (index < this.axisLocations.size()) { 433 result = (PolarAxisLocation) this.axisLocations.get(index); 434 } 435 return result; 436 } 437 438 /** 439 * Sets the location of the primary axis and sends a 440 * {@link PlotChangeEvent} to all registered listeners. 441 * 442 * @param location the location (<code>null</code> not permitted). 443 * 444 * @see #getAxisLocation() 445 * 446 * @since 1.0.14 447 */ 448 public void setAxisLocation(PolarAxisLocation location) { 449 // delegate... 450 setAxisLocation(0, location, true); 451 } 452 453 /** 454 * Sets the location of the primary axis and, if requested, sends a 455 * {@link PlotChangeEvent} to all registered listeners. 456 * 457 * @param location the location (<code>null</code> not permitted). 458 * @param notify notify listeners? 459 * 460 * @see #getAxisLocation() 461 * 462 * @since 1.0.14 463 */ 464 public void setAxisLocation(PolarAxisLocation location, boolean notify) { 465 // delegate... 466 setAxisLocation(0, location, notify); 467 } 468 469 /** 470 * Sets the location for an axis and sends a {@link PlotChangeEvent} 471 * to all registered listeners. 472 * 473 * @param index the axis index. 474 * @param location the location (<code>null</code> not permitted). 475 * 476 * @see #getAxisLocation(int) 477 * 478 * @since 1.0.14 479 */ 480 public void setAxisLocation(int index, PolarAxisLocation location) { 481 // delegate... 482 setAxisLocation(index, location, true); 483 } 484 485 /** 486 * Sets the axis location for an axis and, if requested, sends a 487 * {@link PlotChangeEvent} to all registered listeners. 488 * 489 * @param index the axis index. 490 * @param location the location (<code>null</code> not permitted). 491 * @param notify notify listeners? 492 * 493 * @since 1.0.14 494 */ 495 public void setAxisLocation(int index, PolarAxisLocation location, 496 boolean notify) { 497 if (location == null) { 498 throw new IllegalArgumentException("Null 'location' argument."); 499 } 500 this.axisLocations.set(index, location); 501 if (notify) { 502 fireChangeEvent(); 503 } 504 } 505 506 /** 507 * Returns the number of domain axes. 508 * 509 * @return The axis count. 510 * 511 * @since 1.0.14 512 **/ 513 public int getAxisCount() { 514 return this.axes.size(); 515 } 516 517 /** 518 * Returns the primary dataset for the plot. 519 * 520 * @return The primary dataset (possibly <code>null</code>). 521 * 522 * @see #setDataset(XYDataset) 523 */ 524 public XYDataset getDataset() { 525 return getDataset(0); 526 } 527 528 /** 529 * Returns the dataset with the specified index, if any. 530 * 531 * @param index the dataset index. 532 * 533 * @return The dataset (possibly <code>null</code>). 534 * 535 * @see #setDataset(int, XYDataset) 536 * 537 * @since 1.0.14 538 */ 539 public XYDataset getDataset(int index) { 540 XYDataset result = null; 541 if (index < this.datasets.size()) { 542 result = (XYDataset) this.datasets.get(index); 543 } 544 return result; 545 } 546 547 /** 548 * Sets the primary dataset for the plot, replacing the existing dataset 549 * if there is one, and sends a {@code link PlotChangeEvent} to all 550 * registered listeners. 551 * 552 * @param dataset the dataset (<code>null</code> permitted). 553 * 554 * @see #getDataset() 555 */ 556 public void setDataset(XYDataset dataset) { 557 setDataset(0, dataset); 558 } 559 560 /** 561 * Sets a dataset for the plot, replacing the existing dataset at the same 562 * index if there is one, and sends a {@code link PlotChangeEvent} to all 563 * registered listeners. 564 * 565 * @param index the dataset index. 566 * @param dataset the dataset (<code>null</code> permitted). 567 * 568 * @see #getDataset(int) 569 * 570 * @since 1.0.14 571 */ 572 public void setDataset(int index, XYDataset dataset) { 573 XYDataset existing = getDataset(index); 574 if (existing != null) { 575 existing.removeChangeListener(this); 576 } 577 this.datasets.set(index, dataset); 578 if (dataset != null) { 579 dataset.addChangeListener(this); 580 } 581 582 // send a dataset change event to self... 583 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 584 datasetChanged(event); 585 } 586 587 /** 588 * Returns the number of datasets. 589 * 590 * @return The number of datasets. 591 * 592 * @since 1.0.14 593 */ 594 public int getDatasetCount() { 595 return this.datasets.size(); 596 } 597 598 /** 599 * Returns the index of the specified dataset, or <code>-1</code> if the 600 * dataset does not belong to the plot. 601 * 602 * @param dataset the dataset (<code>null</code> not permitted). 603 * 604 * @return The index. 605 * 606 * @since 1.0.14 607 */ 608 public int indexOf(XYDataset dataset) { 609 int result = -1; 610 for (int i = 0; i < this.datasets.size(); i++) { 611 if (dataset == this.datasets.get(i)) { 612 result = i; 613 break; 614 } 615 } 616 return result; 617 } 618 619 /** 620 * Returns the primary renderer. 621 * 622 * @return The renderer (possibly <code>null</code>). 623 * 624 * @see #setRenderer(PolarItemRenderer) 625 */ 626 public PolarItemRenderer getRenderer() { 627 return getRenderer(0); 628 } 629 630 /** 631 * Returns the renderer at the specified index, if there is one. 632 * 633 * @param index the renderer index. 634 * 635 * @return The renderer (possibly <code>null</code>). 636 * 637 * @see #setRenderer(int, PolarItemRenderer) 638 * 639 * @since 1.0.14 640 */ 641 public PolarItemRenderer getRenderer(int index) { 642 PolarItemRenderer result = null; 643 if (index < this.renderers.size()) { 644 result = (PolarItemRenderer) this.renderers.get(index); 645 } 646 return result; 647 } 648 649 /** 650 * Sets the primary renderer, and notifies all listeners of a change to the 651 * plot. If the renderer is set to <code>null</code>, no data items will 652 * be drawn for the corresponding dataset. 653 * 654 * @param renderer the new renderer (<code>null</code> permitted). 655 * 656 * @see #getRenderer() 657 */ 658 public void setRenderer(PolarItemRenderer renderer) { 659 setRenderer(0, renderer); 660 } 661 662 /** 663 * Sets a renderer and sends a {@link PlotChangeEvent} to all 664 * registered listeners. 665 * 666 * @param index the index. 667 * @param renderer the renderer. 668 * 669 * @see #getRenderer(int) 670 * 671 * @since 1.0.14 672 */ 673 public void setRenderer(int index, PolarItemRenderer renderer) { 674 setRenderer(index, renderer, true); 675 } 676 677 /** 678 * Sets a renderer and, if requested, sends a {@link PlotChangeEvent} to 679 * all registered listeners. 680 * 681 * @param index the index. 682 * @param renderer the renderer. 683 * @param notify notify listeners? 684 * 685 * @see #getRenderer(int) 686 * 687 * @since 1.0.14 688 */ 689 public void setRenderer(int index, PolarItemRenderer renderer, 690 boolean notify) { 691 PolarItemRenderer existing = getRenderer(index); 692 if (existing != null) { 693 existing.removeChangeListener(this); 694 } 695 this.renderers.set(index, renderer); 696 if (renderer != null) { 697 renderer.setPlot(this); 698 renderer.addChangeListener(this); 699 } 700 if (notify) { 701 fireChangeEvent(); 702 } 703 } 704 705 /** 706 * Returns the tick unit that controls the spacing of the angular grid 707 * lines. 708 * 709 * @return The tick unit (never <code>null</code>). 710 * 711 * @since 1.0.10 712 */ 713 public TickUnit getAngleTickUnit() { 714 return this.angleTickUnit; 715 } 716 717 /** 718 * Sets the tick unit that controls the spacing of the angular grid 719 * lines, and sends a {@link PlotChangeEvent} to all registered listeners. 720 * 721 * @param unit the tick unit (<code>null</code> not permitted). 722 * 723 * @since 1.0.10 724 */ 725 public void setAngleTickUnit(TickUnit unit) { 726 if (unit == null) { 727 throw new IllegalArgumentException("Null 'unit' argument."); 728 } 729 this.angleTickUnit = unit; 730 fireChangeEvent(); 731 } 732 733 /** 734 * Returns the offset that is used for all angles. 735 * 736 * @return The offset for the angles. 737 * @since 1.0.14 738 */ 739 public double getAngleOffset() { 740 return this.angleOffset; 741 } 742 743 /** 744 * Sets the offset that is used for all angles and sends a 745 * {@link PlotChangeEvent} to all registered listeners. 746 * 747 * This is useful to let 0 degrees be at the north, east, south or west 748 * side of the chart. 749 * 750 * @param offset The offset 751 * @since 1.0.14 752 */ 753 public void setAngleOffset(double offset) { 754 this.angleOffset = offset; 755 fireChangeEvent(); 756 } 757 758 /** 759 * Get the direction for growing angle degrees. 760 * 761 * @return <code>true</code> if angle increases counterclockwise, 762 * <code>false</code> otherwise. 763 * @since 1.0.14 764 */ 765 public boolean isCounterClockwise() { 766 return this.counterClockwise; 767 } 768 769 /** 770 * Sets the flag for increasing angle degrees direction. 771 * 772 * <code>true</code> for counterclockwise, <code>false</code> for 773 * clockwise. 774 * 775 * @param counterClockwise The flag. 776 * @since 1.0.14 777 */ 778 public void setCounterClockwise(boolean counterClockwise) 779 { 780 this.counterClockwise = counterClockwise; 781 } 782 783 /** 784 * Returns a flag that controls whether or not the angle labels are visible. 785 * 786 * @return A boolean. 787 * 788 * @see #setAngleLabelsVisible(boolean) 789 */ 790 public boolean isAngleLabelsVisible() { 791 return this.angleLabelsVisible; 792 } 793 794 /** 795 * Sets the flag that controls whether or not the angle labels are visible, 796 * and sends a {@link PlotChangeEvent} to all registered listeners. 797 * 798 * @param visible the flag. 799 * 800 * @see #isAngleLabelsVisible() 801 */ 802 public void setAngleLabelsVisible(boolean visible) { 803 if (this.angleLabelsVisible != visible) { 804 this.angleLabelsVisible = visible; 805 fireChangeEvent(); 806 } 807 } 808 809 /** 810 * Returns the font used to display the angle labels. 811 * 812 * @return A font (never <code>null</code>). 813 * 814 * @see #setAngleLabelFont(Font) 815 */ 816 public Font getAngleLabelFont() { 817 return this.angleLabelFont; 818 } 819 820 /** 821 * Sets the font used to display the angle labels and sends a 822 * {@link PlotChangeEvent} to all registered listeners. 823 * 824 * @param font the font (<code>null</code> not permitted). 825 * 826 * @see #getAngleLabelFont() 827 */ 828 public void setAngleLabelFont(Font font) { 829 if (font == null) { 830 throw new IllegalArgumentException("Null 'font' argument."); 831 } 832 this.angleLabelFont = font; 833 fireChangeEvent(); 834 } 835 836 /** 837 * Returns the paint used to display the angle labels. 838 * 839 * @return A paint (never <code>null</code>). 840 * 841 * @see #setAngleLabelPaint(Paint) 842 */ 843 public Paint getAngleLabelPaint() { 844 return this.angleLabelPaint; 845 } 846 847 /** 848 * Sets the paint used to display the angle labels and sends a 849 * {@link PlotChangeEvent} to all registered listeners. 850 * 851 * @param paint the paint (<code>null</code> not permitted). 852 */ 853 public void setAngleLabelPaint(Paint paint) { 854 if (paint == null) { 855 throw new IllegalArgumentException("Null 'paint' argument."); 856 } 857 this.angleLabelPaint = paint; 858 fireChangeEvent(); 859 } 860 861 /** 862 * Returns <code>true</code> if the angular gridlines are visible, and 863 * <code>false</code> otherwise. 864 * 865 * @return <code>true</code> or <code>false</code>. 866 * 867 * @see #setAngleGridlinesVisible(boolean) 868 */ 869 public boolean isAngleGridlinesVisible() { 870 return this.angleGridlinesVisible; 871 } 872 873 /** 874 * Sets the flag that controls whether or not the angular grid-lines are 875 * visible. 876 * <p> 877 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 878 * registered listeners. 879 * 880 * @param visible the new value of the flag. 881 * 882 * @see #isAngleGridlinesVisible() 883 */ 884 public void setAngleGridlinesVisible(boolean visible) { 885 if (this.angleGridlinesVisible != visible) { 886 this.angleGridlinesVisible = visible; 887 fireChangeEvent(); 888 } 889 } 890 891 /** 892 * Returns the stroke for the grid-lines (if any) plotted against the 893 * angular axis. 894 * 895 * @return The stroke (possibly <code>null</code>). 896 * 897 * @see #setAngleGridlineStroke(Stroke) 898 */ 899 public Stroke getAngleGridlineStroke() { 900 return this.angleGridlineStroke; 901 } 902 903 /** 904 * Sets the stroke for the grid lines plotted against the angular axis and 905 * sends a {@link PlotChangeEvent} to all registered listeners. 906 * <p> 907 * If you set this to <code>null</code>, no grid lines will be drawn. 908 * 909 * @param stroke the stroke (<code>null</code> permitted). 910 * 911 * @see #getAngleGridlineStroke() 912 */ 913 public void setAngleGridlineStroke(Stroke stroke) { 914 this.angleGridlineStroke = stroke; 915 fireChangeEvent(); 916 } 917 918 /** 919 * Returns the paint for the grid lines (if any) plotted against the 920 * angular axis. 921 * 922 * @return The paint (possibly <code>null</code>). 923 * 924 * @see #setAngleGridlinePaint(Paint) 925 */ 926 public Paint getAngleGridlinePaint() { 927 return this.angleGridlinePaint; 928 } 929 930 /** 931 * Sets the paint for the grid lines plotted against the angular axis. 932 * <p> 933 * If you set this to <code>null</code>, no grid lines will be drawn. 934 * 935 * @param paint the paint (<code>null</code> permitted). 936 * 937 * @see #getAngleGridlinePaint() 938 */ 939 public void setAngleGridlinePaint(Paint paint) { 940 this.angleGridlinePaint = paint; 941 fireChangeEvent(); 942 } 943 944 /** 945 * Returns <code>true</code> if the radius axis grid is visible, and 946 * <code>false</code> otherwise. 947 * 948 * @return <code>true</code> or <code>false</code>. 949 * 950 * @see #setRadiusGridlinesVisible(boolean) 951 */ 952 public boolean isRadiusGridlinesVisible() { 953 return this.radiusGridlinesVisible; 954 } 955 956 /** 957 * Sets the flag that controls whether or not the radius axis grid lines 958 * are visible. 959 * <p> 960 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 961 * registered listeners. 962 * 963 * @param visible the new value of the flag. 964 * 965 * @see #isRadiusGridlinesVisible() 966 */ 967 public void setRadiusGridlinesVisible(boolean visible) { 968 if (this.radiusGridlinesVisible != visible) { 969 this.radiusGridlinesVisible = visible; 970 fireChangeEvent(); 971 } 972 } 973 974 /** 975 * Returns the stroke for the grid lines (if any) plotted against the 976 * radius axis. 977 * 978 * @return The stroke (possibly <code>null</code>). 979 * 980 * @see #setRadiusGridlineStroke(Stroke) 981 */ 982 public Stroke getRadiusGridlineStroke() { 983 return this.radiusGridlineStroke; 984 } 985 986 /** 987 * Sets the stroke for the grid lines plotted against the radius axis and 988 * sends a {@link PlotChangeEvent} to all registered listeners. 989 * <p> 990 * If you set this to <code>null</code>, no grid lines will be drawn. 991 * 992 * @param stroke the stroke (<code>null</code> permitted). 993 * 994 * @see #getRadiusGridlineStroke() 995 */ 996 public void setRadiusGridlineStroke(Stroke stroke) { 997 this.radiusGridlineStroke = stroke; 998 fireChangeEvent(); 999 } 1000 1001 /** 1002 * Returns the paint for the grid lines (if any) plotted against the radius 1003 * axis. 1004 * 1005 * @return The paint (possibly <code>null</code>). 1006 * 1007 * @see #setRadiusGridlinePaint(Paint) 1008 */ 1009 public Paint getRadiusGridlinePaint() { 1010 return this.radiusGridlinePaint; 1011 } 1012 1013 /** 1014 * Sets the paint for the grid lines plotted against the radius axis and 1015 * sends a {@link PlotChangeEvent} to all registered listeners. 1016 * <p> 1017 * If you set this to <code>null</code>, no grid lines will be drawn. 1018 * 1019 * @param paint the paint (<code>null</code> permitted). 1020 * 1021 * @see #getRadiusGridlinePaint() 1022 */ 1023 public void setRadiusGridlinePaint(Paint paint) { 1024 this.radiusGridlinePaint = paint; 1025 fireChangeEvent(); 1026 } 1027 1028 /** 1029 * Returns the margin around the plot area. 1030 * 1031 * @return The actual margin in pixels. 1032 * 1033 * @since 1.0.14 1034 */ 1035 public int getMargin() { 1036 return this.margin; 1037 } 1038 1039 /** 1040 * Set the margin around the plot area and sends a 1041 * {@link PlotChangeEvent} to all registered listeners. 1042 * 1043 * @param margin The new margin in pixels. 1044 * 1045 * @since 1.0.14 1046 */ 1047 public void setMargin(int margin) { 1048 this.margin = margin; 1049 fireChangeEvent(); 1050 } 1051 1052 /** 1053 * Returns the fixed legend items, if any. 1054 * 1055 * @return The legend items (possibly <code>null</code>). 1056 * 1057 * @see #setFixedLegendItems(LegendItemCollection) 1058 * 1059 * @since 1.0.14 1060 */ 1061 public LegendItemCollection getFixedLegendItems() { 1062 return this.fixedLegendItems; 1063 } 1064 1065 /** 1066 * Sets the fixed legend items for the plot. Leave this set to 1067 * <code>null</code> if you prefer the legend items to be created 1068 * automatically. 1069 * 1070 * @param items the legend items (<code>null</code> permitted). 1071 * 1072 * @see #getFixedLegendItems() 1073 * 1074 * @since 1.0.14 1075 */ 1076 public void setFixedLegendItems(LegendItemCollection items) { 1077 this.fixedLegendItems = items; 1078 fireChangeEvent(); 1079 } 1080 1081 /** 1082 * Add text to be displayed in the lower right hand corner and sends a 1083 * {@link PlotChangeEvent} to all registered listeners. 1084 * 1085 * @param text the text to display (<code>null</code> not permitted). 1086 * 1087 * @see #removeCornerTextItem(String) 1088 */ 1089 public void addCornerTextItem(String text) { 1090 if (text == null) { 1091 throw new IllegalArgumentException("Null 'text' argument."); 1092 } 1093 this.cornerTextItems.add(text); 1094 fireChangeEvent(); 1095 } 1096 1097 /** 1098 * Remove the given text from the list of corner text items and 1099 * sends a {@link PlotChangeEvent} to all registered listeners. 1100 * 1101 * @param text the text to remove (<code>null</code> ignored). 1102 * 1103 * @see #addCornerTextItem(String) 1104 */ 1105 public void removeCornerTextItem(String text) { 1106 boolean removed = this.cornerTextItems.remove(text); 1107 if (removed) { 1108 fireChangeEvent(); 1109 } 1110 } 1111 1112 /** 1113 * Clear the list of corner text items and sends a {@link PlotChangeEvent} 1114 * to all registered listeners. 1115 * 1116 * @see #addCornerTextItem(String) 1117 * @see #removeCornerTextItem(String) 1118 */ 1119 public void clearCornerTextItems() { 1120 if (this.cornerTextItems.size() > 0) { 1121 this.cornerTextItems.clear(); 1122 fireChangeEvent(); 1123 } 1124 } 1125 1126 /** 1127 * Generates a list of tick values for the angular tick marks. 1128 * 1129 * @return A list of {@link NumberTick} instances. 1130 * 1131 * @since 1.0.10 1132 */ 1133 protected List refreshAngleTicks() { 1134 List ticks = new ArrayList(); 1135 for (double currentTickVal = 0.0; currentTickVal < 360.0; 1136 currentTickVal += this.angleTickUnit.getSize()) { 1137 1138 TextAnchor ta = calculateTextAnchor(currentTickVal); 1139 NumberTick tick = new NumberTick(new Double(currentTickVal), 1140 this.angleTickUnit.valueToString(currentTickVal), 1141 ta, TextAnchor.CENTER, 0.0); 1142 ticks.add(tick); 1143 } 1144 return ticks; 1145 } 1146 1147 /** 1148 * Calculate the text position for the given degrees. 1149 * 1150 * @return The optimal text anchor. 1151 * @since 1.0.14 1152 */ 1153 protected TextAnchor calculateTextAnchor(double angleDegrees) 1154 { 1155 TextAnchor ta = TextAnchor.CENTER; 1156 1157 // normalize angle 1158 double offset = angleOffset; 1159 while (offset < 0.0) 1160 offset += 360.0; 1161 double normalizedAngle = (((counterClockwise ? -1 : 1) * angleDegrees) 1162 + offset) % 360; 1163 while (counterClockwise && (normalizedAngle < 0.0)) 1164 normalizedAngle += 360.0; 1165 1166 if (normalizedAngle == 0.0) { 1167 ta = TextAnchor.CENTER_LEFT; 1168 } 1169 else if (normalizedAngle > 0.0 && normalizedAngle < 90.0) { 1170 ta = TextAnchor.TOP_LEFT; 1171 } 1172 else if (normalizedAngle == 90.0) { 1173 ta = TextAnchor.TOP_CENTER; 1174 } 1175 else if (normalizedAngle > 90.0 && normalizedAngle < 180.0) { 1176 ta = TextAnchor.TOP_RIGHT; 1177 } 1178 else if (normalizedAngle == 180) { 1179 ta = TextAnchor.CENTER_RIGHT; 1180 } 1181 else if (normalizedAngle > 180.0 && normalizedAngle < 270.0) { 1182 ta = TextAnchor.BOTTOM_RIGHT; 1183 } 1184 else if (normalizedAngle == 270) { 1185 ta = TextAnchor.BOTTOM_CENTER; 1186 } 1187 else if (normalizedAngle > 270.0 && normalizedAngle < 360.0) { 1188 ta = TextAnchor.BOTTOM_LEFT; 1189 } 1190 return ta; 1191 } 1192 1193 /** 1194 * Maps a dataset to a particular axis. All data will be plotted 1195 * against axis zero by default, no mapping is required for this case. 1196 * 1197 * @param index the dataset index (zero-based). 1198 * @param axisIndex the axis index. 1199 * 1200 * @since 1.0.14 1201 */ 1202 public void mapDatasetToAxis(int index, int axisIndex) { 1203 List axisIndices = new java.util.ArrayList(1); 1204 axisIndices.add(new Integer(axisIndex)); 1205 mapDatasetToAxes(index, axisIndices); 1206 } 1207 1208 /** 1209 * Maps the specified dataset to the axes in the list. Note that the 1210 * conversion of data values into Java2D space is always performed using 1211 * the first axis in the list. 1212 * 1213 * @param index the dataset index (zero-based). 1214 * @param axisIndices the axis indices (<code>null</code> permitted). 1215 * 1216 * @since 1.0.14 1217 */ 1218 public void mapDatasetToAxes(int index, List axisIndices) { 1219 if (index < 0) { 1220 throw new IllegalArgumentException("Requires 'index' >= 0."); 1221 } 1222 checkAxisIndices(axisIndices); 1223 Integer key = new Integer(index); 1224 this.datasetToAxesMap.put(key, new ArrayList(axisIndices)); 1225 // fake a dataset change event to update axes... 1226 datasetChanged(new DatasetChangeEvent(this, getDataset(index))); 1227 } 1228 1229 /** 1230 * This method is used to perform argument checking on the list of 1231 * axis indices passed to mapDatasetToAxes(). 1232 * 1233 * @param indices the list of indices (<code>null</code> permitted). 1234 */ 1235 private void checkAxisIndices(List indices) { 1236 // axisIndices can be: 1237 // 1. null; 1238 // 2. non-empty, containing only Integer objects that are unique. 1239 if (indices == null) { 1240 return; // OK 1241 } 1242 int count = indices.size(); 1243 if (count == 0) { 1244 throw new IllegalArgumentException("Empty list not permitted."); 1245 } 1246 HashSet set = new HashSet(); 1247 for (int i = 0; i < count; i++) { 1248 Object item = indices.get(i); 1249 if (!(item instanceof Integer)) { 1250 throw new IllegalArgumentException( 1251 "Indices must be Integer instances."); 1252 } 1253 if (set.contains(item)) { 1254 throw new IllegalArgumentException("Indices must be unique."); 1255 } 1256 set.add(item); 1257 } 1258 } 1259 1260 /** 1261 * Returns the axis for a dataset. 1262 * 1263 * @param index the dataset index. 1264 * 1265 * @return The axis. 1266 * 1267 * @since 1.0.14 1268 */ 1269 public ValueAxis getAxisForDataset(int index) { 1270 ValueAxis valueAxis = null; 1271 List axisIndices = (List) this.datasetToAxesMap.get( 1272 new Integer(index)); 1273 if (axisIndices != null) { 1274 // the first axis in the list is used for data <--> Java2D 1275 Integer axisIndex = (Integer) axisIndices.get(0); 1276 valueAxis = getAxis(axisIndex.intValue()); 1277 } 1278 else { 1279 valueAxis = getAxis(0); 1280 } 1281 return valueAxis; 1282 } 1283 1284 /** 1285 * Returns the index of the given axis. 1286 * 1287 * @param axis the axis. 1288 * 1289 * @return The axis index or -1 if axis is not used in this plot. 1290 * 1291 * @since 1.0.14 1292 */ 1293 public int getAxisIndex(ValueAxis axis) { 1294 int result = this.axes.indexOf(axis); 1295 if (result < 0) { 1296 // try the parent plot 1297 Plot parent = getParent(); 1298 if (parent instanceof PolarPlot) { 1299 PolarPlot p = (PolarPlot) parent; 1300 result = p.getAxisIndex(axis); 1301 } 1302 } 1303 return result; 1304 } 1305 1306 /** 1307 * Returns the index of the specified renderer, or <code>-1</code> if the 1308 * renderer is not assigned to this plot. 1309 * 1310 * @param renderer the renderer (<code>null</code> permitted). 1311 * 1312 * @return The renderer index. 1313 * 1314 * @since 1.0.14 1315 */ 1316 public int getIndexOf(PolarItemRenderer renderer) { 1317 return this.renderers.indexOf(renderer); 1318 } 1319 1320 /** 1321 * Draws the plot on a Java 2D graphics device (such as the screen or a 1322 * printer). 1323 * <P> 1324 * This plot relies on a {@link PolarItemRenderer} to draw each 1325 * item in the plot. This allows the visual representation of the data to 1326 * be changed easily. 1327 * <P> 1328 * The optional info argument collects information about the rendering of 1329 * the plot (dimensions, tooltip information etc). Just pass in 1330 * <code>null</code> if you do not need this information. 1331 * 1332 * @param g2 the graphics device. 1333 * @param area the area within which the plot (including axes and 1334 * labels) should be drawn. 1335 * @param anchor the anchor point (<code>null</code> permitted). 1336 * @param parentState ignored. 1337 * @param info collects chart drawing information (<code>null</code> 1338 * permitted). 1339 */ 1340 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 1341 PlotState parentState, PlotRenderingInfo info) { 1342 1343 // if the plot area is too small, just return... 1344 boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW); 1345 boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW); 1346 if (b1 || b2) { 1347 return; 1348 } 1349 1350 // record the plot area... 1351 if (info != null) { 1352 info.setPlotArea(area); 1353 } 1354 1355 // adjust the drawing area for the plot insets (if any)... 1356 RectangleInsets insets = getInsets(); 1357 insets.trim(area); 1358 1359 Rectangle2D dataArea = area; 1360 if (info != null) { 1361 info.setDataArea(dataArea); 1362 } 1363 1364 // draw the plot background and axes... 1365 drawBackground(g2, dataArea); 1366 int axisCount = this.axes.size(); 1367 AxisState state = null; 1368 for (int i = 0; i < axisCount; i++) { 1369 ValueAxis axis = getAxis(i); 1370 if (axis != null) { 1371 PolarAxisLocation location 1372 = (PolarAxisLocation) this.axisLocations.get(i); 1373 AxisState s = this.drawAxis(axis, location, g2, dataArea); 1374 if (i == 0) { 1375 state = s; 1376 } 1377 } 1378 } 1379 1380 // now for each dataset, get the renderer and the appropriate axis 1381 // and render the dataset... 1382 Shape originalClip = g2.getClip(); 1383 Composite originalComposite = g2.getComposite(); 1384 1385 g2.clip(dataArea); 1386 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1387 getForegroundAlpha())); 1388 this.angleTicks = refreshAngleTicks(); 1389 drawGridlines(g2, dataArea, this.angleTicks, state.getTicks()); 1390 render(g2, dataArea, info); 1391 g2.setClip(originalClip); 1392 g2.setComposite(originalComposite); 1393 drawOutline(g2, dataArea); 1394 drawCornerTextItems(g2, dataArea); 1395 } 1396 1397 /** 1398 * Draws the corner text items. 1399 * 1400 * @param g2 the drawing surface. 1401 * @param area the area. 1402 */ 1403 protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) { 1404 if (this.cornerTextItems.isEmpty()) { 1405 return; 1406 } 1407 1408 g2.setColor(Color.black); 1409 double width = 0.0; 1410 double height = 0.0; 1411 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) { 1412 String msg = (String) it.next(); 1413 FontMetrics fm = g2.getFontMetrics(); 1414 Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, fm); 1415 width = Math.max(width, bounds.getWidth()); 1416 height += bounds.getHeight(); 1417 } 1418 1419 double xadj = ANNOTATION_MARGIN * 2.0; 1420 double yadj = ANNOTATION_MARGIN; 1421 width += xadj; 1422 height += yadj; 1423 1424 double x = area.getMaxX() - width; 1425 double y = area.getMaxY() - height; 1426 g2.drawRect((int) x, (int) y, (int) width, (int) height); 1427 x += ANNOTATION_MARGIN; 1428 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) { 1429 String msg = (String) it.next(); 1430 Rectangle2D bounds = TextUtilities.getTextBounds(msg, g2, 1431 g2.getFontMetrics()); 1432 y += bounds.getHeight(); 1433 g2.drawString(msg, (int) x, (int) y); 1434 } 1435 } 1436 1437 /** 1438 * Draws the axis with the specified index. 1439 * 1440 * @param axis the axis. 1441 * @param location the axis location. 1442 * @param g2 the graphics target. 1443 * @param plotArea the plot area. 1444 * 1445 * @return The axis state. 1446 * 1447 * @since 1.0.14 1448 */ 1449 protected AxisState drawAxis(ValueAxis axis, PolarAxisLocation location, 1450 Graphics2D g2, Rectangle2D plotArea) { 1451 1452 double centerX = plotArea.getCenterX(); 1453 double centerY = plotArea.getCenterY(); 1454 double r = Math.min(plotArea.getWidth() / 2.0, 1455 plotArea.getHeight() / 2.0) - this.margin; 1456 double x = centerX - r; 1457 double y = centerY - r; 1458 1459 Rectangle2D dataArea = null; 1460 AxisState result = null; 1461 if (location == PolarAxisLocation.NORTH_RIGHT) { 1462 dataArea = new Rectangle2D.Double(x, y, r, r); 1463 result = axis.draw(g2, centerX, plotArea, dataArea, 1464 RectangleEdge.RIGHT, null); 1465 } 1466 else if (location == PolarAxisLocation.NORTH_LEFT) { 1467 dataArea = new Rectangle2D.Double(centerX, y, r, r); 1468 result = axis.draw(g2, centerX, plotArea, dataArea, 1469 RectangleEdge.LEFT, null); 1470 } 1471 else if (location == PolarAxisLocation.SOUTH_LEFT) { 1472 dataArea = new Rectangle2D.Double(centerX, centerY, r, r); 1473 result = axis.draw(g2, centerX, plotArea, dataArea, 1474 RectangleEdge.LEFT, null); 1475 } 1476 else if (location == PolarAxisLocation.SOUTH_RIGHT) { 1477 dataArea = new Rectangle2D.Double(x, centerY, r, r); 1478 result = axis.draw(g2, centerX, plotArea, dataArea, 1479 RectangleEdge.RIGHT, null); 1480 } 1481 else if (location == PolarAxisLocation.EAST_ABOVE) { 1482 dataArea = new Rectangle2D.Double(centerX, centerY, r, r); 1483 result = axis.draw(g2, centerY, plotArea, dataArea, 1484 RectangleEdge.TOP, null); 1485 } 1486 else if (location == PolarAxisLocation.EAST_BELOW) { 1487 dataArea = new Rectangle2D.Double(centerX, y, r, r); 1488 result = axis.draw(g2, centerY, plotArea, dataArea, 1489 RectangleEdge.BOTTOM, null); 1490 } 1491 else if (location == PolarAxisLocation.WEST_ABOVE) { 1492 dataArea = new Rectangle2D.Double(x, centerY, r, r); 1493 result = axis.draw(g2, centerY, plotArea, dataArea, 1494 RectangleEdge.TOP, null); 1495 } 1496 else if (location == PolarAxisLocation.WEST_BELOW) { 1497 dataArea = new Rectangle2D.Double(x, y, r, r); 1498 result = axis.draw(g2, centerY, plotArea, dataArea, 1499 RectangleEdge.BOTTOM, null); 1500 } 1501 1502 return result; 1503 } 1504 1505 /** 1506 * Draws a representation of the data within the dataArea region, using the 1507 * current m_Renderer. 1508 * 1509 * @param g2 the graphics device. 1510 * @param dataArea the region in which the data is to be drawn. 1511 * @param info an optional object for collection dimension 1512 * information (<code>null</code> permitted). 1513 */ 1514 protected void render(Graphics2D g2, Rectangle2D dataArea, 1515 PlotRenderingInfo info) { 1516 1517 // now get the data and plot it (the visual representation will depend 1518 // on the m_Renderer that has been set)... 1519 boolean hasData = false; 1520 int datasetCount = this.datasets.size(); 1521 for (int i = datasetCount - 1; i >= 0; i--) { 1522 XYDataset dataset = getDataset(i); 1523 if (dataset == null) { 1524 continue; 1525 } 1526 PolarItemRenderer renderer = getRenderer(i); 1527 if (renderer == null) { 1528 continue; 1529 } 1530 if (!DatasetUtilities.isEmptyOrNull(dataset)) { 1531 hasData = true; 1532 int seriesCount = dataset.getSeriesCount(); 1533 for (int series = 0; series < seriesCount; series++) { 1534 renderer.drawSeries(g2, dataArea, info, this, dataset, 1535 series); 1536 } 1537 } 1538 } 1539 if (!hasData) { 1540 drawNoDataMessage(g2, dataArea); 1541 } 1542 } 1543 1544 /** 1545 * Draws the gridlines for the plot, if they are visible. 1546 * 1547 * @param g2 the graphics device. 1548 * @param dataArea the data area. 1549 * @param angularTicks the ticks for the angular axis. 1550 * @param radialTicks the ticks for the radial axis. 1551 */ 1552 protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea, 1553 List angularTicks, List radialTicks) { 1554 1555 PolarItemRenderer renderer = getRenderer(); 1556 // no renderer, no gridlines... 1557 if (renderer == null) { 1558 return; 1559 } 1560 1561 // draw the domain grid lines, if any... 1562 if (isAngleGridlinesVisible()) { 1563 Stroke gridStroke = getAngleGridlineStroke(); 1564 Paint gridPaint = getAngleGridlinePaint(); 1565 if ((gridStroke != null) && (gridPaint != null)) { 1566 renderer.drawAngularGridLines(g2, this, angularTicks, 1567 dataArea); 1568 } 1569 } 1570 1571 // draw the radius grid lines, if any... 1572 if (isRadiusGridlinesVisible()) { 1573 Stroke gridStroke = getRadiusGridlineStroke(); 1574 Paint gridPaint = getRadiusGridlinePaint(); 1575 if ((gridStroke != null) && (gridPaint != null)) { 1576 renderer.drawRadialGridLines(g2, this, getAxis(), 1577 radialTicks, dataArea); 1578 } 1579 } 1580 } 1581 1582 /** 1583 * Zooms the axis ranges by the specified percentage about the anchor point. 1584 * 1585 * @param percent the amount of the zoom. 1586 */ 1587 public void zoom(double percent) { 1588 for (int axisIdx = 0; axisIdx < getAxisCount(); axisIdx++) { 1589 final ValueAxis axis = getAxis(axisIdx); 1590 if (axis != null) { 1591 if (percent > 0.0) { 1592 double radius = axis.getUpperBound(); 1593 double scaledRadius = radius * percent; 1594 axis.setUpperBound(scaledRadius); 1595 axis.setAutoRange(false); 1596 } 1597 else { 1598 axis.setAutoRange(true); 1599 } 1600 } 1601 } 1602 } 1603 1604 /** 1605 * A utility method that returns a list of datasets that are mapped to a 1606 * particular axis. 1607 * 1608 * @param axisIndex the axis index (<code>null</code> not permitted). 1609 * 1610 * @return A list of datasets. 1611 * 1612 * @since 1.0.14 1613 */ 1614 private List getDatasetsMappedToAxis(Integer axisIndex) { 1615 if (axisIndex == null) { 1616 throw new IllegalArgumentException("Null 'axisIndex' argument."); 1617 } 1618 List result = new ArrayList(); 1619 for (int i = 0; i < this.datasets.size(); i++) { 1620 List mappedAxes = (List) this.datasetToAxesMap.get(new Integer(i)); 1621 if (mappedAxes == null) { 1622 if (axisIndex.equals(ZERO)) { 1623 result.add(this.datasets.get(i)); 1624 } 1625 } 1626 else { 1627 if (mappedAxes.contains(axisIndex)) { 1628 result.add(this.datasets.get(i)); 1629 } 1630 } 1631 } 1632 return result; 1633 } 1634 1635 /** 1636 * Returns the range for the specified axis. 1637 * 1638 * @param axis the axis. 1639 * 1640 * @return The range. 1641 */ 1642 public Range getDataRange(ValueAxis axis) { 1643 Range result = null; 1644 int axisIdx = getAxisIndex(axis); 1645 List mappedDatasets = new ArrayList(); 1646 1647 if (axisIdx >= 0) { 1648 mappedDatasets = getDatasetsMappedToAxis(new Integer(axisIdx)); 1649 } 1650 1651 // iterate through the datasets that map to the axis and get the union 1652 // of the ranges. 1653 Iterator iterator = mappedDatasets.iterator(); 1654 int datasetIdx = -1; 1655 while (iterator.hasNext()) { 1656 datasetIdx++; 1657 XYDataset d = (XYDataset) iterator.next(); 1658 if (d != null) { 1659 // FIXME better ask the renderer instead of DatasetUtilities 1660 result = Range.combine(result, 1661 DatasetUtilities.findRangeBounds(d)); 1662 } 1663 } 1664 1665 return result; 1666 } 1667 1668 /** 1669 * Receives notification of a change to the plot's m_Dataset. 1670 * <P> 1671 * The axis ranges are updated if necessary. 1672 * 1673 * @param event information about the event (not used here). 1674 */ 1675 public void datasetChanged(DatasetChangeEvent event) { 1676 for (int i = 0; i < this.axes.size(); i++) { 1677 final ValueAxis axis = (ValueAxis) this.axes.get(i); 1678 if (axis != null) { 1679 axis.configure(); 1680 } 1681 } 1682 if (getParent() != null) { 1683 getParent().datasetChanged(event); 1684 } 1685 else { 1686 super.datasetChanged(event); 1687 } 1688 } 1689 1690 /** 1691 * Notifies all registered listeners of a property change. 1692 * <P> 1693 * One source of property change events is the plot's m_Renderer. 1694 * 1695 * @param event information about the property change. 1696 */ 1697 public void rendererChanged(RendererChangeEvent event) { 1698 fireChangeEvent(); 1699 } 1700 1701 /** 1702 * Returns the legend items for the plot. Each legend item is generated by 1703 * the plot's m_Renderer, since the m_Renderer is responsible for the visual 1704 * representation of the data. 1705 * 1706 * @return The legend items. 1707 */ 1708 public LegendItemCollection getLegendItems() { 1709 if (this.fixedLegendItems != null) { 1710 return this.fixedLegendItems; 1711 } 1712 LegendItemCollection result = new LegendItemCollection(); 1713 int count = this.datasets.size(); 1714 for (int datasetIndex = 0; datasetIndex < count; datasetIndex++) { 1715 XYDataset dataset = getDataset(datasetIndex); 1716 PolarItemRenderer renderer = getRenderer(datasetIndex); 1717 if (dataset != null && renderer != null) { 1718 int seriesCount = dataset.getSeriesCount(); 1719 for (int i = 0; i < seriesCount; i++) { 1720 LegendItem item = renderer.getLegendItem(i); 1721 result.add(item); 1722 } 1723 } 1724 } 1725 return result; 1726 } 1727 1728 /** 1729 * Tests this plot for equality with another object. 1730 * 1731 * @param obj the object (<code>null</code> permitted). 1732 * 1733 * @return <code>true</code> or <code>false</code>. 1734 */ 1735 public boolean equals(Object obj) { 1736 if (obj == this) { 1737 return true; 1738 } 1739 if (!(obj instanceof PolarPlot)) { 1740 return false; 1741 } 1742 PolarPlot that = (PolarPlot) obj; 1743 if (!this.axes.equals(that.axes)) { 1744 return false; 1745 } 1746 if (!this.axisLocations.equals(that.axisLocations)) { 1747 return false; 1748 } 1749 if (!this.renderers.equals(that.renderers)) { 1750 return false; 1751 } 1752 if (!this.angleTickUnit.equals(that.angleTickUnit)) { 1753 return false; 1754 } 1755 if (this.angleGridlinesVisible != that.angleGridlinesVisible) { 1756 return false; 1757 } 1758 if (this.angleOffset != that.angleOffset) 1759 { 1760 return false; 1761 } 1762 if (this.counterClockwise != that.counterClockwise) 1763 { 1764 return false; 1765 } 1766 if (this.angleLabelsVisible != that.angleLabelsVisible) { 1767 return false; 1768 } 1769 if (!this.angleLabelFont.equals(that.angleLabelFont)) { 1770 return false; 1771 } 1772 if (!PaintUtilities.equal(this.angleLabelPaint, that.angleLabelPaint)) { 1773 return false; 1774 } 1775 if (!ObjectUtilities.equal(this.angleGridlineStroke, 1776 that.angleGridlineStroke)) { 1777 return false; 1778 } 1779 if (!PaintUtilities.equal( 1780 this.angleGridlinePaint, that.angleGridlinePaint 1781 )) { 1782 return false; 1783 } 1784 if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) { 1785 return false; 1786 } 1787 if (!ObjectUtilities.equal(this.radiusGridlineStroke, 1788 that.radiusGridlineStroke)) { 1789 return false; 1790 } 1791 if (!PaintUtilities.equal(this.radiusGridlinePaint, 1792 that.radiusGridlinePaint)) { 1793 return false; 1794 } 1795 if (!this.cornerTextItems.equals(that.cornerTextItems)) { 1796 return false; 1797 } 1798 if (this.margin != that.margin) { 1799 return false; 1800 } 1801 if (!ObjectUtilities.equal(this.fixedLegendItems, 1802 that.fixedLegendItems)) { 1803 return false; 1804 } 1805 return super.equals(obj); 1806 } 1807 1808 /** 1809 * Returns a clone of the plot. 1810 * 1811 * @return A clone. 1812 * 1813 * @throws CloneNotSupportedException this can occur if some component of 1814 * the plot cannot be cloned. 1815 */ 1816 public Object clone() throws CloneNotSupportedException { 1817 1818 PolarPlot clone = (PolarPlot) super.clone(); 1819 clone.axes = (ObjectList) ObjectUtilities.clone(this.axes); 1820 for (int i = 0; i < this.axes.size(); i++) { 1821 ValueAxis axis = (ValueAxis) this.axes.get(i); 1822 if (axis != null) { 1823 ValueAxis clonedAxis = (ValueAxis) axis.clone(); 1824 clone.axes.set(i, clonedAxis); 1825 clonedAxis.setPlot(clone); 1826 clonedAxis.addChangeListener(clone); 1827 } 1828 } 1829 1830 // the datasets are not cloned, but listeners need to be added... 1831 clone.datasets = (ObjectList) ObjectUtilities.clone(this.datasets); 1832 for (int i = 0; i < clone.datasets.size(); ++i) { 1833 XYDataset d = getDataset(i); 1834 if (d != null) { 1835 d.addChangeListener(clone); 1836 } 1837 } 1838 1839 clone.renderers = (ObjectList) ObjectUtilities.clone(this.renderers); 1840 for (int i = 0; i < this.renderers.size(); i++) { 1841 PolarItemRenderer renderer2 = (PolarItemRenderer) this.renderers.get(i); 1842 if (renderer2 instanceof PublicCloneable) { 1843 PublicCloneable pc = (PublicCloneable) renderer2; 1844 PolarItemRenderer rc = (PolarItemRenderer) pc.clone(); 1845 clone.renderers.set(i, rc); 1846 rc.setPlot(clone); 1847 rc.addChangeListener(clone); 1848 } 1849 } 1850 1851 clone.cornerTextItems = new ArrayList(this.cornerTextItems); 1852 1853 return clone; 1854 } 1855 1856 /** 1857 * Provides serialization support. 1858 * 1859 * @param stream the output stream. 1860 * 1861 * @throws IOException if there is an I/O error. 1862 */ 1863 private void writeObject(ObjectOutputStream stream) throws IOException { 1864 stream.defaultWriteObject(); 1865 SerialUtilities.writeStroke(this.angleGridlineStroke, stream); 1866 SerialUtilities.writePaint(this.angleGridlinePaint, stream); 1867 SerialUtilities.writeStroke(this.radiusGridlineStroke, stream); 1868 SerialUtilities.writePaint(this.radiusGridlinePaint, stream); 1869 SerialUtilities.writePaint(this.angleLabelPaint, stream); 1870 } 1871 1872 /** 1873 * Provides serialization support. 1874 * 1875 * @param stream the input stream. 1876 * 1877 * @throws IOException if there is an I/O error. 1878 * @throws ClassNotFoundException if there is a classpath problem. 1879 */ 1880 private void readObject(ObjectInputStream stream) 1881 throws IOException, ClassNotFoundException { 1882 1883 stream.defaultReadObject(); 1884 this.angleGridlineStroke = SerialUtilities.readStroke(stream); 1885 this.angleGridlinePaint = SerialUtilities.readPaint(stream); 1886 this.radiusGridlineStroke = SerialUtilities.readStroke(stream); 1887 this.radiusGridlinePaint = SerialUtilities.readPaint(stream); 1888 this.angleLabelPaint = SerialUtilities.readPaint(stream); 1889 1890 int rangeAxisCount = this.axes.size(); 1891 for (int i = 0; i < rangeAxisCount; i++) { 1892 Axis axis = (Axis) this.axes.get(i); 1893 if (axis != null) { 1894 axis.setPlot(this); 1895 axis.addChangeListener(this); 1896 } 1897 } 1898 int datasetCount = this.datasets.size(); 1899 for (int i = 0; i < datasetCount; i++) { 1900 Dataset dataset = (Dataset) this.datasets.get(i); 1901 if (dataset != null) { 1902 dataset.addChangeListener(this); 1903 } 1904 } 1905 int rendererCount = this.renderers.size(); 1906 for (int i = 0; i < rendererCount; i++) { 1907 PolarItemRenderer renderer = (PolarItemRenderer) this.renderers.get(i); 1908 if (renderer != null) { 1909 renderer.addChangeListener(this); 1910 } 1911 } 1912 } 1913 1914 /** 1915 * This method is required by the {@link Zoomable} interface, but since 1916 * the plot does not have any domain axes, it does nothing. 1917 * 1918 * @param factor the zoom factor. 1919 * @param state the plot state. 1920 * @param source the source point (in Java2D coordinates). 1921 */ 1922 public void zoomDomainAxes(double factor, PlotRenderingInfo state, 1923 Point2D source) { 1924 // do nothing 1925 } 1926 1927 /** 1928 * This method is required by the {@link Zoomable} interface, but since 1929 * the plot does not have any domain axes, it does nothing. 1930 * 1931 * @param factor the zoom factor. 1932 * @param state the plot state. 1933 * @param source the source point (in Java2D coordinates). 1934 * @param useAnchor use source point as zoom anchor? 1935 * 1936 * @since 1.0.7 1937 */ 1938 public void zoomDomainAxes(double factor, PlotRenderingInfo state, 1939 Point2D source, boolean useAnchor) { 1940 // do nothing 1941 } 1942 1943 /** 1944 * This method is required by the {@link Zoomable} interface, but since 1945 * the plot does not have any domain axes, it does nothing. 1946 * 1947 * @param lowerPercent the new lower bound. 1948 * @param upperPercent the new upper bound. 1949 * @param state the plot state. 1950 * @param source the source point (in Java2D coordinates). 1951 */ 1952 public void zoomDomainAxes(double lowerPercent, double upperPercent, 1953 PlotRenderingInfo state, Point2D source) { 1954 // do nothing 1955 } 1956 1957 /** 1958 * Multiplies the range on the range axis/axes by the specified factor. 1959 * 1960 * @param factor the zoom factor. 1961 * @param state the plot state. 1962 * @param source the source point (in Java2D coordinates). 1963 */ 1964 public void zoomRangeAxes(double factor, PlotRenderingInfo state, 1965 Point2D source) { 1966 zoom(factor); 1967 } 1968 1969 /** 1970 * Multiplies the range on the range axis by the specified factor. 1971 * 1972 * @param factor the zoom factor. 1973 * @param info the plot rendering info. 1974 * @param source the source point (in Java2D space). 1975 * @param useAnchor use source point as zoom anchor? 1976 * 1977 * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean) 1978 * 1979 * @since 1.0.7 1980 */ 1981 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 1982 Point2D source, boolean useAnchor) { 1983 // get the source coordinate - this plot has always a VERTICAL 1984 // orientation 1985 final double sourceX = source.getX(); 1986 1987 for (int axisIdx = 0; axisIdx < getAxisCount(); axisIdx++) { 1988 final ValueAxis axis = getAxis(axisIdx); 1989 if (axis != null) { 1990 if (useAnchor) { 1991 double anchorX = axis.java2DToValue(sourceX, 1992 info.getDataArea(), RectangleEdge.BOTTOM); 1993 axis.resizeRange(factor, anchorX); 1994 } 1995 else { 1996 axis.resizeRange(factor); 1997 } 1998 } 1999 } 2000 } 2001 2002 /** 2003 * Zooms in on the range axes. 2004 * 2005 * @param lowerPercent the new lower bound. 2006 * @param upperPercent the new upper bound. 2007 * @param state the plot state. 2008 * @param source the source point (in Java2D coordinates). 2009 */ 2010 public void zoomRangeAxes(double lowerPercent, double upperPercent, 2011 PlotRenderingInfo state, Point2D source) { 2012 zoom((upperPercent + lowerPercent) / 2.0); 2013 } 2014 2015 /** 2016 * Returns <code>false</code> always. 2017 * 2018 * @return <code>false</code> always. 2019 */ 2020 public boolean isDomainZoomable() { 2021 return false; 2022 } 2023 2024 /** 2025 * Returns <code>true</code> to indicate that the range axis is zoomable. 2026 * 2027 * @return <code>true</code>. 2028 */ 2029 public boolean isRangeZoomable() { 2030 return true; 2031 } 2032 2033 /** 2034 * Returns the orientation of the plot. 2035 * 2036 * @return The orientation. 2037 */ 2038 public PlotOrientation getOrientation() { 2039 return PlotOrientation.HORIZONTAL; 2040 } 2041 2042 /** 2043 * Translates a (theta, radius) pair into Java2D coordinates. If 2044 * <code>radius</code> is less than the lower bound of the axis, then 2045 * this method returns the centre point. 2046 * 2047 * @param angleDegrees the angle in degrees. 2048 * @param radius the radius. 2049 * @param dataArea the data area. 2050 * 2051 * @return A point in Java2D space. 2052 * 2053 * @since 1.0.14 2054 */ 2055 public Point translateToJava2D(double angleDegrees, double radius, 2056 ValueAxis axis, Rectangle2D dataArea) { 2057 2058 if (counterClockwise) 2059 angleDegrees = -angleDegrees; 2060 double radians = Math.toRadians(angleDegrees + this.angleOffset); 2061 2062 double minx = dataArea.getMinX() + this.margin; 2063 double maxx = dataArea.getMaxX() - this.margin; 2064 double miny = dataArea.getMinY() + this.margin; 2065 double maxy = dataArea.getMaxY() - this.margin; 2066 2067 double halfWidth = (maxx - minx) / 2.0; 2068 double halfHeight = (maxy - miny) / 2.0; 2069 2070 double midX = minx + halfWidth; 2071 double midY = miny + halfHeight; 2072 2073 double l = Math.min(halfWidth, halfHeight); 2074 Rectangle2D quadrant = new Rectangle2D.Double(midX, midY, l, l); 2075 2076 double axisMin = axis.getLowerBound(); 2077 double adjustedRadius = Math.max(radius, axisMin); 2078 2079 double length = axis.valueToJava2D(adjustedRadius, quadrant, RectangleEdge.BOTTOM) - midX; 2080 float x = (float) (midX + Math.cos(radians) * length); 2081 float y = (float) (midY + Math.sin(radians) * length); 2082 2083 int ix = Math.round(x); 2084 int iy = Math.round(y); 2085 2086 Point p = new Point(ix, iy); 2087 return p; 2088 2089 } 2090 2091 /** 2092 * Translates a (theta, radius) pair into Java2D coordinates. If 2093 * <code>radius</code> is less than the lower bound of the axis, then 2094 * this method returns the centre point. 2095 * 2096 * @param angleDegrees the angle in degrees. 2097 * @param radius the radius. 2098 * @param dataArea the data area. 2099 * 2100 * @return A point in Java2D space. 2101 * 2102 * @deprecated Since 1.0.14, use {@link #translateToJava2D(double, double, 2103 * org.jfree.chart.axis.ValueAxis, java.awt.geom.Rectangle2D)} instead. 2104 */ 2105 public Point translateValueThetaRadiusToJava2D(double angleDegrees, 2106 double radius, Rectangle2D dataArea) { 2107 2108 return translateToJava2D(angleDegrees, radius, getAxis(), dataArea); 2109 } 2110 2111 /** 2112 * Returns the upper bound of the radius axis. 2113 * 2114 * @return The upper bound. 2115 * 2116 * @deprecated Since 1.0.14, use {@link #getAxis()} and call the 2117 * getUpperBound() method. 2118 */ 2119 public double getMaxRadius() { 2120 return getAxis().getUpperBound(); 2121 } 2122 2123 /** 2124 * Returns the number of series in the dataset for this plot. If the 2125 * dataset is <code>null</code>, the method returns 0. 2126 * 2127 * @return The series count. 2128 * 2129 * @deprecated Since 1.0.14, grab a reference to the dataset and check 2130 * the series count directly. 2131 */ 2132 public int getSeriesCount() { 2133 int result = 0; 2134 XYDataset dataset = getDataset(0); 2135 if (dataset != null) { 2136 result = dataset.getSeriesCount(); 2137 } 2138 return result; 2139 } 2140 2141 /** 2142 * A utility method for drawing the axes. 2143 * 2144 * @param g2 the graphics device. 2145 * @param plotArea the plot area. 2146 * @param dataArea the data area. 2147 * 2148 * @return A map containing the axis states. 2149 * 2150 * @deprecated As of version 1.0.14, this method is no longer used. 2151 */ 2152 protected AxisState drawAxis(Graphics2D g2, Rectangle2D plotArea, 2153 Rectangle2D dataArea) { 2154 return getAxis().draw(g2, dataArea.getMinY(), plotArea, dataArea, 2155 RectangleEdge.TOP, null); 2156 } 2157 2158 }