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 * DialPlot.java 029 * ------------- 030 * (C) Copyright 2006-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 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.ArrayList; 047import java.util.HashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.Objects; 051import org.jfree.chart.ChartElementVisitor; 052 053import org.jfree.chart.JFreeChart; 054import org.jfree.chart.event.PlotChangeEvent; 055import org.jfree.chart.plot.Plot; 056import org.jfree.chart.plot.PlotRenderingInfo; 057import org.jfree.chart.plot.PlotState; 058import org.jfree.chart.internal.Args; 059import org.jfree.data.general.DatasetChangeEvent; 060import org.jfree.data.general.ValueDataset; 061 062/** 063 * A dial plot composed of user-definable layers. 064 * The example shown here is generated by the {@code DialDemo2.java} 065 * program included in the JFreeChart Demo Collection: 066 * <br><br> 067 * <img src="doc-files/DialPlotSample.png" alt="DialPlotSample.png"> 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 Map<Integer, ValueDataset> datasets; 090 091 /** 092 * The scale(s) for the dial plot. 093 */ 094 private Map<Integer, DialScale> scales; 095 096 /** Storage for keys that map datasets to scales. */ 097 private Map<Integer, Integer> datasetToScaleMap; 098 099 /** 100 * The drawing layers for the dial plot. 101 */ 102 private List<DialLayer> layers; 103 104 /** 105 * The pointer(s) for the dial. 106 */ 107 private List<DialPointer> 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 HashMap<>(); 146 if (dataset != null) { 147 setDataset(dataset); 148 } 149 this.scales = new HashMap<>(); 150 this.datasetToScaleMap = new HashMap<>(); 151 this.layers = new ArrayList<>(); 152 this.pointers = new 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 = 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 = 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 for (DialPointer p : this.pointers) { 429 if (p.getDatasetIndex() == datasetIndex) { 430 return p; 431 } 432 } 433 return result; 434 } 435 436 /** 437 * Returns the primary dataset for the plot. 438 * 439 * @return The primary dataset (possibly {@code null}). 440 */ 441 public ValueDataset getDataset() { 442 return getDataset(0); 443 } 444 445 /** 446 * Returns the dataset at the given index. 447 * 448 * @param index the dataset index. 449 * 450 * @return The dataset (possibly {@code null}). 451 */ 452 public ValueDataset getDataset(int index) { 453 ValueDataset result = null; 454 if (this.datasets.size() > index) { 455 result = (ValueDataset) this.datasets.get(index); 456 } 457 return result; 458 } 459 460 /** 461 * Sets the dataset for the plot, replacing the existing dataset, if there 462 * is one, and sends a {@link PlotChangeEvent} to all registered 463 * listeners. 464 * 465 * @param dataset the dataset ({@code null} permitted). 466 */ 467 public void setDataset(ValueDataset dataset) { 468 setDataset(0, dataset); 469 } 470 471 /** 472 * Sets a dataset for the plot. 473 * 474 * @param index the dataset index. 475 * @param dataset the dataset ({@code null} permitted). 476 */ 477 public void setDataset(int index, ValueDataset dataset) { 478 ValueDataset existing = this.datasets.get(index); 479 if (existing != null) { 480 existing.removeChangeListener(this); 481 } 482 this.datasets.put(index, dataset); 483 if (dataset != null) { 484 dataset.addChangeListener(this); 485 } 486 487 // send a dataset change event to self... 488 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 489 datasetChanged(event); 490 } 491 492 /** 493 * Returns the number of datasets. 494 * 495 * @return The number of datasets. 496 */ 497 public int getDatasetCount() { 498 return this.datasets.size(); 499 } 500 501 /** 502 * Receives a chart element visitor. Many plot subclasses will override 503 * this method to handle their subcomponents. 504 * 505 * @param visitor the visitor ({@code null} not permitted). 506 */ 507 @Override 508 public void receive(ChartElementVisitor visitor) { 509 // FIXME : handle the subcomponents 510 super.receive(visitor); 511 } 512 513 514 /** 515 * Draws the plot. This method is usually called by the {@link JFreeChart} 516 * instance that manages the plot. 517 * 518 * @param g2 the graphics target. 519 * @param area the area in which the plot should be drawn. 520 * @param anchor the anchor point (typically the last point that the 521 * mouse clicked on, {@code null} is permitted). 522 * @param parentState the state for the parent plot (if any). 523 * @param info used to collect plot rendering info ({@code null} 524 * permitted). 525 */ 526 @Override 527 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 528 PlotState parentState, PlotRenderingInfo info) { 529 530 Shape origClip = g2.getClip(); 531 g2.setClip(area); 532 533 // first, expand the viewing area into a drawing frame 534 Rectangle2D frame = viewToFrame(area); 535 536 // draw the background if there is one... 537 if (this.background != null && this.background.isVisible()) { 538 if (this.background.isClippedToWindow()) { 539 Shape savedClip = g2.getClip(); 540 g2.clip(this.dialFrame.getWindow(frame)); 541 this.background.draw(g2, this, frame, area); 542 g2.setClip(savedClip); 543 } 544 else { 545 this.background.draw(g2, this, frame, area); 546 } 547 } 548 549 for (DialLayer current : this.layers) { 550 if (current.isVisible()) { 551 if (current.isClippedToWindow()) { 552 Shape savedClip = g2.getClip(); 553 g2.clip(this.dialFrame.getWindow(frame)); 554 current.draw(g2, this, frame, area); 555 g2.setClip(savedClip); 556 } 557 else { 558 current.draw(g2, this, frame, area); 559 } 560 } 561 } 562 563 // draw the pointers 564 for (DialPointer current : this.pointers) { 565 if (current.isVisible()) { 566 if (current.isClippedToWindow()) { 567 Shape savedClip = g2.getClip(); 568 g2.clip(this.dialFrame.getWindow(frame)); 569 current.draw(g2, this, frame, area); 570 g2.setClip(savedClip); 571 } else { 572 current.draw(g2, this, frame, area); 573 } 574 } 575 } 576 577 // draw the cap if there is one... 578 if (this.cap != null && this.cap.isVisible()) { 579 if (this.cap.isClippedToWindow()) { 580 Shape savedClip = g2.getClip(); 581 g2.clip(this.dialFrame.getWindow(frame)); 582 this.cap.draw(g2, this, frame, area); 583 g2.setClip(savedClip); 584 } else { 585 this.cap.draw(g2, this, frame, area); 586 } 587 } 588 589 if (this.dialFrame.isVisible()) { 590 this.dialFrame.draw(g2, this, frame, area); 591 } 592 593 g2.setClip(origClip); 594 595 } 596 597 /** 598 * Returns the frame surrounding the specified view rectangle. 599 * 600 * @param view the view rectangle ({@code null} not permitted). 601 * 602 * @return The frame rectangle. 603 */ 604 private Rectangle2D viewToFrame(Rectangle2D view) { 605 double width = view.getWidth() / this.viewW; 606 double height = view.getHeight() / this.viewH; 607 double x = view.getX() - (width * this.viewX); 608 double y = view.getY() - (height * this.viewY); 609 return new Rectangle2D.Double(x, y, width, height); 610 } 611 612 /** 613 * Returns the value from the specified dataset. 614 * 615 * @param datasetIndex the dataset index. 616 * 617 * @return The data value. 618 */ 619 public double getValue(int datasetIndex) { 620 double result = Double.NaN; 621 ValueDataset dataset = getDataset(datasetIndex); 622 if (dataset != null) { 623 Number n = dataset.getValue(); 624 if (n != null) { 625 result = n.doubleValue(); 626 } 627 } 628 return result; 629 } 630 631 /** 632 * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to 633 * all registered listeners. 634 * 635 * @param index the scale index. 636 * @param scale the scale ({@code null} not permitted). 637 */ 638 public void addScale(int index, DialScale scale) { 639 Args.nullNotPermitted(scale, "scale"); 640 DialScale existing = this.scales.get(index); 641 if (existing != null) { 642 removeLayer(existing); 643 } 644 this.layers.add(scale); 645 this.scales.put(index, scale); 646 scale.addChangeListener(this); 647 fireChangeEvent(); 648 } 649 650 /** 651 * Returns the scale at the given index. 652 * 653 * @param index the scale index. 654 * 655 * @return The scale (possibly {@code null}). 656 */ 657 public DialScale getScale(int index) { 658 return this.scales.get(index); 659 } 660 661 /** 662 * Maps a dataset to a particular scale. 663 * 664 * @param index the dataset index (zero-based). 665 * @param scaleIndex the scale index (zero-based). 666 */ 667 public void mapDatasetToScale(int index, int scaleIndex) { 668 this.datasetToScaleMap.put(index, scaleIndex); 669 fireChangeEvent(); 670 } 671 672 /** 673 * Returns the dial scale for a specific dataset. 674 * 675 * @param datasetIndex the dataset index. 676 * 677 * @return The dial scale. 678 */ 679 public DialScale getScaleForDataset(int datasetIndex) { 680 DialScale result = this.scales.get(0); 681 Integer scaleIndex = this.datasetToScaleMap.get(datasetIndex); 682 if (scaleIndex != null) { 683 result = getScale(scaleIndex); 684 } 685 return result; 686 } 687 688 /** 689 * A utility method that computes a rectangle using relative radius values. 690 * 691 * @param rect the reference rectangle ({@code null} not permitted). 692 * @param radiusW the width radius (must be > 0.0) 693 * @param radiusH the height radius. 694 * 695 * @return A new rectangle. 696 */ 697 public static Rectangle2D rectangleByRadius(Rectangle2D rect, 698 double radiusW, double radiusH) { 699 Args.nullNotPermitted(rect, "rect"); 700 double x = rect.getCenterX(); 701 double y = rect.getCenterY(); 702 double w = rect.getWidth() * radiusW; 703 double h = rect.getHeight() * radiusH; 704 return new Rectangle2D.Double(x - w / 2.0, y - h / 2.0, w, h); 705 } 706 707 /** 708 * Receives notification when a layer has changed, and responds by 709 * forwarding a {@link PlotChangeEvent} to all registered listeners. 710 * 711 * @param event the event. 712 */ 713 @Override 714 public void dialLayerChanged(DialLayerChangeEvent event) { 715 fireChangeEvent(); 716 } 717 718 /** 719 * Tests this {@code DialPlot} instance for equality with an 720 * arbitrary object. The plot's dataset(s) is (are) not included in 721 * the test. 722 * 723 * @param obj the object ({@code null} permitted). 724 * 725 * @return A boolean. 726 */ 727 @Override 728 public boolean equals(Object obj) { 729 if (obj == this) { 730 return true; 731 } 732 if (!(obj instanceof DialPlot)) { 733 return false; 734 } 735 DialPlot that = (DialPlot) obj; 736 if (!Objects.equals(this.background, that.background)) { 737 return false; 738 } 739 if (!Objects.equals(this.cap, that.cap)) { 740 return false; 741 } 742 if (!this.dialFrame.equals(that.dialFrame)) { 743 return false; 744 } 745 if (this.viewX != that.viewX) { 746 return false; 747 } 748 if (this.viewY != that.viewY) { 749 return false; 750 } 751 if (this.viewW != that.viewW) { 752 return false; 753 } 754 if (this.viewH != that.viewH) { 755 return false; 756 } 757 if (!this.layers.equals(that.layers)) { 758 return false; 759 } 760 if (!this.pointers.equals(that.pointers)) { 761 return false; 762 } 763 return super.equals(obj); 764 } 765 766 /** 767 * Returns a hash code for this instance. 768 * 769 * @return The hash code. 770 */ 771 @Override 772 public int hashCode() { 773 int result = 193; 774 result = 37 * result + Objects.hashCode(this.background); 775 result = 37 * result + Objects.hashCode(this.cap); 776 result = 37 * result + this.dialFrame.hashCode(); 777 long temp = Double.doubleToLongBits(this.viewX); 778 result = 37 * result + (int) (temp ^ (temp >>> 32)); 779 temp = Double.doubleToLongBits(this.viewY); 780 result = 37 * result + (int) (temp ^ (temp >>> 32)); 781 temp = Double.doubleToLongBits(this.viewW); 782 result = 37 * result + (int) (temp ^ (temp >>> 32)); 783 temp = Double.doubleToLongBits(this.viewH); 784 result = 37 * result + (int) (temp ^ (temp >>> 32)); 785 return result; 786 } 787 788 /** 789 * Returns the plot type. 790 * 791 * @return {@code "DialPlot"} 792 */ 793 @Override 794 public String getPlotType() { 795 return "DialPlot"; 796 } 797 798 /** 799 * Provides serialization support. 800 * 801 * @param stream the output stream. 802 * 803 * @throws IOException if there is an I/O error. 804 */ 805 private void writeObject(ObjectOutputStream stream) throws IOException { 806 stream.defaultWriteObject(); 807 } 808 809 /** 810 * Provides serialization support. 811 * 812 * @param stream the input stream. 813 * 814 * @throws IOException if there is an I/O error. 815 * @throws ClassNotFoundException if there is a classpath problem. 816 */ 817 private void readObject(ObjectInputStream stream) 818 throws IOException, ClassNotFoundException { 819 stream.defaultReadObject(); 820 } 821 822}