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