001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------- 028 * DialPlot.java 029 * ------------- 030 * (C) Copyright 2006-2020, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.chart.plot.dial; 038 039import java.awt.Graphics2D; 040import java.awt.Shape; 041import java.awt.geom.Point2D; 042import java.awt.geom.Rectangle2D; 043import java.io.IOException; 044import java.io.ObjectInputStream; 045import java.io.ObjectOutputStream; 046import java.util.Iterator; 047import java.util.List; 048 049import org.jfree.chart.JFreeChart; 050import org.jfree.chart.event.PlotChangeEvent; 051import org.jfree.chart.plot.Plot; 052import org.jfree.chart.plot.PlotRenderingInfo; 053import org.jfree.chart.plot.PlotState; 054import org.jfree.chart.util.ObjectList; 055import org.jfree.chart.util.ObjectUtils; 056import org.jfree.chart.util.Args; 057import org.jfree.data.general.DatasetChangeEvent; 058import org.jfree.data.general.ValueDataset; 059 060/** 061 * A dial plot composed of user-definable layers. 062 * The example shown here is generated by the {@code DialDemo2.java} 063 * program included in the JFreeChart Demo Collection: 064 * <br><br> 065 * <img src="doc-files/DialPlotSample.png" alt="DialPlotSample.png"> 066 * 067 * @since 1.0.7 068 */ 069public class DialPlot extends Plot implements DialLayerChangeListener { 070 071 /** 072 * The background layer (optional). 073 */ 074 private DialLayer background; 075 076 /** 077 * The needle cap (optional). 078 */ 079 private DialLayer cap; 080 081 /** 082 * The dial frame. 083 */ 084 private DialFrame dialFrame; 085 086 /** 087 * The dataset(s) for the dial plot. 088 */ 089 private ObjectList datasets; 090 091 /** 092 * The scale(s) for the dial plot. 093 */ 094 private ObjectList scales; 095 096 /** Storage for keys that map datasets to scales. */ 097 private ObjectList datasetToScaleMap; 098 099 /** 100 * The drawing layers for the dial plot. 101 */ 102 private List layers; 103 104 /** 105 * The pointer(s) for the dial. 106 */ 107 private List pointers; 108 109 /** 110 * The x-coordinate for the view window. 111 */ 112 private double viewX; 113 114 /** 115 * The y-coordinate for the view window. 116 */ 117 private double viewY; 118 119 /** 120 * The width of the view window, expressed as a percentage. 121 */ 122 private double viewW; 123 124 /** 125 * The height of the view window, expressed as a percentage. 126 */ 127 private double viewH; 128 129 /** 130 * Creates a new instance of {@code DialPlot}. 131 */ 132 public DialPlot() { 133 this(null); 134 } 135 136 /** 137 * Creates a new instance of {@code DialPlot}. 138 * 139 * @param dataset the dataset ({@code null} permitted). 140 */ 141 public DialPlot(ValueDataset dataset) { 142 this.background = null; 143 this.cap = null; 144 this.dialFrame = new ArcDialFrame(); 145 this.datasets = new ObjectList(); 146 if (dataset != null) { 147 setDataset(dataset); 148 } 149 this.scales = new ObjectList(); 150 this.datasetToScaleMap = new ObjectList(); 151 this.layers = new java.util.ArrayList(); 152 this.pointers = new java.util.ArrayList(); 153 this.viewX = 0.0; 154 this.viewY = 0.0; 155 this.viewW = 1.0; 156 this.viewH = 1.0; 157 } 158 159 /** 160 * Returns the background. 161 * 162 * @return The background (possibly {@code null}). 163 * 164 * @see #setBackground(DialLayer) 165 */ 166 public DialLayer getBackground() { 167 return this.background; 168 } 169 170 /** 171 * Sets the background layer and sends a {@link PlotChangeEvent} to all 172 * registered listeners. 173 * 174 * @param background the background layer ({@code null} permitted). 175 * 176 * @see #getBackground() 177 */ 178 public void setBackground(DialLayer background) { 179 if (this.background != null) { 180 this.background.removeChangeListener(this); 181 } 182 this.background = background; 183 if (background != null) { 184 background.addChangeListener(this); 185 } 186 fireChangeEvent(); 187 } 188 189 /** 190 * Returns the cap. 191 * 192 * @return The cap (possibly {@code null}). 193 * 194 * @see #setCap(DialLayer) 195 */ 196 public DialLayer getCap() { 197 return this.cap; 198 } 199 200 /** 201 * Sets the cap and sends a {@link PlotChangeEvent} to all registered 202 * listeners. 203 * 204 * @param cap the cap ({@code null} permitted). 205 * 206 * @see #getCap() 207 */ 208 public void setCap(DialLayer cap) { 209 if (this.cap != null) { 210 this.cap.removeChangeListener(this); 211 } 212 this.cap = cap; 213 if (cap != null) { 214 cap.addChangeListener(this); 215 } 216 fireChangeEvent(); 217 } 218 219 /** 220 * Returns the dial's frame. 221 * 222 * @return The dial's frame (never {@code null}). 223 * 224 * @see #setDialFrame(DialFrame) 225 */ 226 public DialFrame getDialFrame() { 227 return this.dialFrame; 228 } 229 230 /** 231 * Sets the dial's frame and sends a {@link PlotChangeEvent} to all 232 * registered listeners. 233 * 234 * @param frame the frame ({@code null} not permitted). 235 * 236 * @see #getDialFrame() 237 */ 238 public void setDialFrame(DialFrame frame) { 239 Args.nullNotPermitted(frame, "frame"); 240 this.dialFrame.removeChangeListener(this); 241 this.dialFrame = frame; 242 frame.addChangeListener(this); 243 fireChangeEvent(); 244 } 245 246 /** 247 * Returns the x-coordinate of the viewing rectangle. This is specified 248 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 249 * 250 * @return The x-coordinate of the viewing rectangle. 251 * 252 * @see #setView(double, double, double, double) 253 */ 254 public double getViewX() { 255 return this.viewX; 256 } 257 258 /** 259 * Returns the y-coordinate of the viewing rectangle. This is specified 260 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 261 * 262 * @return The y-coordinate of the viewing rectangle. 263 * 264 * @see #setView(double, double, double, double) 265 */ 266 public double getViewY() { 267 return this.viewY; 268 } 269 270 /** 271 * Returns the width of the viewing rectangle. This is specified 272 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 273 * 274 * @return The width of the viewing rectangle. 275 * 276 * @see #setView(double, double, double, double) 277 */ 278 public double getViewWidth() { 279 return this.viewW; 280 } 281 282 /** 283 * Returns the height of the viewing rectangle. This is specified 284 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 285 * 286 * @return The height of the viewing rectangle. 287 * 288 * @see #setView(double, double, double, double) 289 */ 290 public double getViewHeight() { 291 return this.viewH; 292 } 293 294 /** 295 * Sets the viewing rectangle, relative to the dial's framing rectangle, 296 * and sends a {@link PlotChangeEvent} to all registered listeners. 297 * 298 * @param x the x-coordinate (in the range 0.0 to 1.0). 299 * @param y the y-coordinate (in the range 0.0 to 1.0). 300 * @param w the width (in the range 0.0 to 1.0). 301 * @param h the height (in the range 0.0 to 1.0). 302 * 303 * @see #getViewX() 304 * @see #getViewY() 305 * @see #getViewWidth() 306 * @see #getViewHeight() 307 */ 308 public void setView(double x, double y, double w, double h) { 309 this.viewX = x; 310 this.viewY = y; 311 this.viewW = w; 312 this.viewH = h; 313 fireChangeEvent(); 314 } 315 316 /** 317 * Adds a layer to the plot and sends a {@link PlotChangeEvent} to all 318 * registered listeners. 319 * 320 * @param layer the layer ({@code null} not permitted). 321 */ 322 public void addLayer(DialLayer layer) { 323 Args.nullNotPermitted(layer, "layer"); 324 this.layers.add(layer); 325 layer.addChangeListener(this); 326 fireChangeEvent(); 327 } 328 329 /** 330 * Returns the index for the specified layer. 331 * 332 * @param layer the layer ({@code null} not permitted). 333 * 334 * @return The layer index. 335 */ 336 public int getLayerIndex(DialLayer layer) { 337 Args.nullNotPermitted(layer, "layer"); 338 return this.layers.indexOf(layer); 339 } 340 341 /** 342 * Removes the layer at the specified index and sends a 343 * {@link PlotChangeEvent} to all registered listeners. 344 * 345 * @param index the index. 346 */ 347 public void removeLayer(int index) { 348 DialLayer layer = (DialLayer) this.layers.get(index); 349 if (layer != null) { 350 layer.removeChangeListener(this); 351 } 352 this.layers.remove(index); 353 fireChangeEvent(); 354 } 355 356 /** 357 * Removes the specified layer and sends a {@link PlotChangeEvent} to all 358 * registered listeners. 359 * 360 * @param layer the layer ({@code null} not permitted). 361 */ 362 public void removeLayer(DialLayer layer) { 363 // defer argument checking 364 removeLayer(getLayerIndex(layer)); 365 } 366 367 /** 368 * Adds a pointer to the plot and sends a {@link PlotChangeEvent} to all 369 * registered listeners. 370 * 371 * @param pointer the pointer ({@code null} not permitted). 372 */ 373 public void addPointer(DialPointer pointer) { 374 Args.nullNotPermitted(pointer, "pointer"); 375 this.pointers.add(pointer); 376 pointer.addChangeListener(this); 377 fireChangeEvent(); 378 } 379 380 /** 381 * Returns the index for the specified pointer. 382 * 383 * @param pointer the pointer ({@code null} not permitted). 384 * 385 * @return The pointer index. 386 */ 387 public int getPointerIndex(DialPointer pointer) { 388 Args.nullNotPermitted(pointer, "pointer"); 389 return this.pointers.indexOf(pointer); 390 } 391 392 /** 393 * Removes the pointer at the specified index and sends a 394 * {@link PlotChangeEvent} to all registered listeners. 395 * 396 * @param index the index. 397 */ 398 public void removePointer(int index) { 399 DialPointer pointer = (DialPointer) this.pointers.get(index); 400 if (pointer != null) { 401 pointer.removeChangeListener(this); 402 } 403 this.pointers.remove(index); 404 fireChangeEvent(); 405 } 406 407 /** 408 * Removes the specified pointer and sends a {@link PlotChangeEvent} to all 409 * registered listeners. 410 * 411 * @param pointer the pointer ({@code null} not permitted). 412 */ 413 public void removePointer(DialPointer pointer) { 414 // defer argument checking 415 removeLayer(getPointerIndex(pointer)); 416 } 417 418 /** 419 * Returns the dial pointer that is associated with the specified 420 * dataset, or {@code null}. 421 * 422 * @param datasetIndex the dataset index. 423 * 424 * @return The pointer. 425 */ 426 public DialPointer getPointerForDataset(int datasetIndex) { 427 DialPointer result = null; 428 Iterator iterator = this.pointers.iterator(); 429 while (iterator.hasNext()) { 430 DialPointer p = (DialPointer) iterator.next(); 431 if (p.getDatasetIndex() == datasetIndex) { 432 return p; 433 } 434 } 435 return result; 436 } 437 438 /** 439 * Returns the primary dataset for the plot. 440 * 441 * @return The primary dataset (possibly {@code null}). 442 */ 443 public ValueDataset getDataset() { 444 return getDataset(0); 445 } 446 447 /** 448 * Returns the dataset at the given index. 449 * 450 * @param index the dataset index. 451 * 452 * @return The dataset (possibly {@code null}). 453 */ 454 public ValueDataset getDataset(int index) { 455 ValueDataset result = null; 456 if (this.datasets.size() > index) { 457 result = (ValueDataset) this.datasets.get(index); 458 } 459 return result; 460 } 461 462 /** 463 * Sets the dataset for the plot, replacing the existing dataset, if there 464 * is one, and sends a {@link PlotChangeEvent} to all registered 465 * listeners. 466 * 467 * @param dataset the dataset ({@code null} permitted). 468 */ 469 public void setDataset(ValueDataset dataset) { 470 setDataset(0, dataset); 471 } 472 473 /** 474 * Sets a dataset for the plot. 475 * 476 * @param index the dataset index. 477 * @param dataset the dataset ({@code null} permitted). 478 */ 479 public void setDataset(int index, ValueDataset dataset) { 480 481 ValueDataset existing = (ValueDataset) this.datasets.get(index); 482 if (existing != null) { 483 existing.removeChangeListener(this); 484 } 485 this.datasets.set(index, dataset); 486 if (dataset != null) { 487 dataset.addChangeListener(this); 488 } 489 490 // send a dataset change event to self... 491 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 492 datasetChanged(event); 493 494 } 495 496 /** 497 * Returns the number of datasets. 498 * 499 * @return The number of datasets. 500 */ 501 public int getDatasetCount() { 502 return this.datasets.size(); 503 } 504 505 /** 506 * Draws the plot. This method is usually called by the {@link JFreeChart} 507 * instance that manages the plot. 508 * 509 * @param g2 the graphics target. 510 * @param area the area in which the plot should be drawn. 511 * @param anchor the anchor point (typically the last point that the 512 * mouse clicked on, {@code null} is permitted). 513 * @param parentState the state for the parent plot (if any). 514 * @param info used to collect plot rendering info ({@code null} 515 * permitted). 516 */ 517 @Override 518 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 519 PlotState parentState, PlotRenderingInfo info) { 520 521 Shape origClip = g2.getClip(); 522 g2.setClip(area); 523 524 // first, expand the viewing area into a drawing frame 525 Rectangle2D frame = viewToFrame(area); 526 527 // draw the background if there is one... 528 if (this.background != null && this.background.isVisible()) { 529 if (this.background.isClippedToWindow()) { 530 Shape savedClip = g2.getClip(); 531 g2.clip(this.dialFrame.getWindow(frame)); 532 this.background.draw(g2, this, frame, area); 533 g2.setClip(savedClip); 534 } 535 else { 536 this.background.draw(g2, this, frame, area); 537 } 538 } 539 540 Iterator iterator = this.layers.iterator(); 541 while (iterator.hasNext()) { 542 DialLayer current = (DialLayer) iterator.next(); 543 if (current.isVisible()) { 544 if (current.isClippedToWindow()) { 545 Shape savedClip = g2.getClip(); 546 g2.clip(this.dialFrame.getWindow(frame)); 547 current.draw(g2, this, frame, area); 548 g2.setClip(savedClip); 549 } 550 else { 551 current.draw(g2, this, frame, area); 552 } 553 } 554 } 555 556 // draw the pointers 557 iterator = this.pointers.iterator(); 558 while (iterator.hasNext()) { 559 DialPointer current = (DialPointer) iterator.next(); 560 if (current.isVisible()) { 561 if (current.isClippedToWindow()) { 562 Shape savedClip = g2.getClip(); 563 g2.clip(this.dialFrame.getWindow(frame)); 564 current.draw(g2, this, frame, area); 565 g2.setClip(savedClip); 566 } 567 else { 568 current.draw(g2, this, frame, area); 569 } 570 } 571 } 572 573 // draw the cap if there is one... 574 if (this.cap != null && this.cap.isVisible()) { 575 if (this.cap.isClippedToWindow()) { 576 Shape savedClip = g2.getClip(); 577 g2.clip(this.dialFrame.getWindow(frame)); 578 this.cap.draw(g2, this, frame, area); 579 g2.setClip(savedClip); 580 } 581 else { 582 this.cap.draw(g2, this, frame, area); 583 } 584 } 585 586 if (this.dialFrame.isVisible()) { 587 this.dialFrame.draw(g2, this, frame, area); 588 } 589 590 g2.setClip(origClip); 591 592 } 593 594 /** 595 * Returns the frame surrounding the specified view rectangle. 596 * 597 * @param view the view rectangle ({@code null} not permitted). 598 * 599 * @return The frame rectangle. 600 */ 601 private Rectangle2D viewToFrame(Rectangle2D view) { 602 double width = view.getWidth() / this.viewW; 603 double height = view.getHeight() / this.viewH; 604 double x = view.getX() - (width * this.viewX); 605 double y = view.getY() - (height * this.viewY); 606 return new Rectangle2D.Double(x, y, width, height); 607 } 608 609 /** 610 * Returns the value from the specified dataset. 611 * 612 * @param datasetIndex the dataset index. 613 * 614 * @return The data value. 615 */ 616 public double getValue(int datasetIndex) { 617 double result = Double.NaN; 618 ValueDataset dataset = getDataset(datasetIndex); 619 if (dataset != null) { 620 Number n = dataset.getValue(); 621 if (n != null) { 622 result = n.doubleValue(); 623 } 624 } 625 return result; 626 } 627 628 /** 629 * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to 630 * all registered listeners. 631 * 632 * @param index the scale index. 633 * @param scale the scale ({@code null} not permitted). 634 */ 635 public void addScale(int index, DialScale scale) { 636 Args.nullNotPermitted(scale, "scale"); 637 DialScale existing = (DialScale) this.scales.get(index); 638 if (existing != null) { 639 removeLayer(existing); 640 } 641 this.layers.add(scale); 642 this.scales.set(index, scale); 643 scale.addChangeListener(this); 644 fireChangeEvent(); 645 } 646 647 /** 648 * Returns the scale at the given index. 649 * 650 * @param index the scale index. 651 * 652 * @return The scale (possibly {@code null}). 653 */ 654 public DialScale getScale(int index) { 655 DialScale result = null; 656 if (this.scales.size() > index) { 657 result = (DialScale) this.scales.get(index); 658 } 659 return result; 660 } 661 662 /** 663 * Maps a dataset to a particular scale. 664 * 665 * @param index the dataset index (zero-based). 666 * @param scaleIndex the scale index (zero-based). 667 */ 668 public void mapDatasetToScale(int index, int scaleIndex) { 669 this.datasetToScaleMap.set(index, scaleIndex); 670 fireChangeEvent(); 671 } 672 673 /** 674 * Returns the dial scale for a specific dataset. 675 * 676 * @param datasetIndex the dataset index. 677 * 678 * @return The dial scale. 679 */ 680 public DialScale getScaleForDataset(int datasetIndex) { 681 DialScale result = (DialScale) this.scales.get(0); 682 Integer scaleIndex = (Integer) this.datasetToScaleMap.get(datasetIndex); 683 if (scaleIndex != null) { 684 result = getScale(scaleIndex); 685 } 686 return result; 687 } 688 689 /** 690 * A utility method that computes a rectangle using relative radius values. 691 * 692 * @param rect the reference rectangle ({@code null} not permitted). 693 * @param radiusW the width radius (must be > 0.0) 694 * @param radiusH the height radius. 695 * 696 * @return A new rectangle. 697 */ 698 public static Rectangle2D rectangleByRadius(Rectangle2D rect, 699 double radiusW, double radiusH) { 700 Args.nullNotPermitted(rect, "rect"); 701 double x = rect.getCenterX(); 702 double y = rect.getCenterY(); 703 double w = rect.getWidth() * radiusW; 704 double h = rect.getHeight() * radiusH; 705 return new Rectangle2D.Double(x - w / 2.0, y - h / 2.0, w, h); 706 } 707 708 /** 709 * Receives notification when a layer has changed, and responds by 710 * forwarding a {@link PlotChangeEvent} to all registered listeners. 711 * 712 * @param event the event. 713 */ 714 @Override 715 public void dialLayerChanged(DialLayerChangeEvent event) { 716 fireChangeEvent(); 717 } 718 719 /** 720 * Tests this {@code DialPlot} instance for equality with an 721 * arbitrary object. The plot's dataset(s) is (are) not included in 722 * the test. 723 * 724 * @param obj the object ({@code null} permitted). 725 * 726 * @return A boolean. 727 */ 728 @Override 729 public boolean equals(Object obj) { 730 if (obj == this) { 731 return true; 732 } 733 if (!(obj instanceof DialPlot)) { 734 return false; 735 } 736 DialPlot that = (DialPlot) obj; 737 if (!ObjectUtils.equal(this.background, that.background)) { 738 return false; 739 } 740 if (!ObjectUtils.equal(this.cap, that.cap)) { 741 return false; 742 } 743 if (!this.dialFrame.equals(that.dialFrame)) { 744 return false; 745 } 746 if (this.viewX != that.viewX) { 747 return false; 748 } 749 if (this.viewY != that.viewY) { 750 return false; 751 } 752 if (this.viewW != that.viewW) { 753 return false; 754 } 755 if (this.viewH != that.viewH) { 756 return false; 757 } 758 if (!this.layers.equals(that.layers)) { 759 return false; 760 } 761 if (!this.pointers.equals(that.pointers)) { 762 return false; 763 } 764 return super.equals(obj); 765 } 766 767 /** 768 * Returns a hash code for this instance. 769 * 770 * @return The hash code. 771 */ 772 @Override 773 public int hashCode() { 774 int result = 193; 775 result = 37 * result + ObjectUtils.hashCode(this.background); 776 result = 37 * result + ObjectUtils.hashCode(this.cap); 777 result = 37 * result + this.dialFrame.hashCode(); 778 long temp = Double.doubleToLongBits(this.viewX); 779 result = 37 * result + (int) (temp ^ (temp >>> 32)); 780 temp = Double.doubleToLongBits(this.viewY); 781 result = 37 * result + (int) (temp ^ (temp >>> 32)); 782 temp = Double.doubleToLongBits(this.viewW); 783 result = 37 * result + (int) (temp ^ (temp >>> 32)); 784 temp = Double.doubleToLongBits(this.viewH); 785 result = 37 * result + (int) (temp ^ (temp >>> 32)); 786 return result; 787 } 788 789 /** 790 * Returns the plot type. 791 * 792 * @return {@code "DialPlot"} 793 */ 794 @Override 795 public String getPlotType() { 796 return "DialPlot"; 797 } 798 799 /** 800 * Provides serialization support. 801 * 802 * @param stream the output stream. 803 * 804 * @throws IOException if there is an I/O error. 805 */ 806 private void writeObject(ObjectOutputStream stream) throws IOException { 807 stream.defaultWriteObject(); 808 } 809 810 /** 811 * Provides serialization support. 812 * 813 * @param stream the input stream. 814 * 815 * @throws IOException if there is an I/O error. 816 * @throws ClassNotFoundException if there is a classpath problem. 817 */ 818 private void readObject(ObjectInputStream stream) 819 throws IOException, ClassNotFoundException { 820 stream.defaultReadObject(); 821 } 822 823 824}