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