001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------- 028 * DialPlot.java 029 * ------------- 030 * (C) Copyright 2006-present, 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.Iterator; 047import java.util.List; 048import java.util.Objects; 049 050import org.jfree.chart.JFreeChart; 051import org.jfree.chart.event.PlotChangeEvent; 052import org.jfree.chart.plot.Plot; 053import org.jfree.chart.plot.PlotRenderingInfo; 054import org.jfree.chart.plot.PlotState; 055import org.jfree.chart.util.ObjectList; 056import org.jfree.chart.util.ObjectUtils; 057import org.jfree.chart.util.Args; 058import org.jfree.data.general.DatasetChangeEvent; 059import org.jfree.data.general.ValueDataset; 060 061/** 062 * A dial plot composed of user-definable layers. 063 * The example shown here is generated by the {@code DialDemo2.java} 064 * program included in the JFreeChart Demo Collection: 065 * <br><br> 066 * <img src="doc-files/DialPlotSample.png" alt="DialPlotSample.png"> 067 */ 068public class DialPlot extends Plot implements DialLayerChangeListener { 069 070 /** 071 * The background layer (optional). 072 */ 073 private DialLayer background; 074 075 /** 076 * The needle cap (optional). 077 */ 078 private DialLayer cap; 079 080 /** 081 * The dial frame. 082 */ 083 private DialFrame dialFrame; 084 085 /** 086 * The dataset(s) for the dial plot. 087 */ 088 private ObjectList datasets; 089 090 /** 091 * The scale(s) for the dial plot. 092 */ 093 private ObjectList scales; 094 095 /** Storage for keys that map datasets to scales. */ 096 private ObjectList datasetToScaleMap; 097 098 /** 099 * The drawing layers for the dial plot. 100 */ 101 private List layers; 102 103 /** 104 * The pointer(s) for the dial. 105 */ 106 private List pointers; 107 108 /** 109 * The x-coordinate for the view window. 110 */ 111 private double viewX; 112 113 /** 114 * The y-coordinate for the view window. 115 */ 116 private double viewY; 117 118 /** 119 * The width of the view window, expressed as a percentage. 120 */ 121 private double viewW; 122 123 /** 124 * The height of the view window, expressed as a percentage. 125 */ 126 private double viewH; 127 128 /** 129 * Creates a new instance of {@code DialPlot}. 130 */ 131 public DialPlot() { 132 this(null); 133 } 134 135 /** 136 * Creates a new instance of {@code DialPlot}. 137 * 138 * @param dataset the dataset ({@code null} permitted). 139 */ 140 public DialPlot(ValueDataset dataset) { 141 this.background = null; 142 this.cap = null; 143 this.dialFrame = new ArcDialFrame(); 144 this.datasets = new ObjectList(); 145 if (dataset != null) { 146 setDataset(dataset); 147 } 148 this.scales = new ObjectList(); 149 this.datasetToScaleMap = new ObjectList(); 150 this.layers = new java.util.ArrayList(); 151 this.pointers = new java.util.ArrayList(); 152 this.viewX = 0.0; 153 this.viewY = 0.0; 154 this.viewW = 1.0; 155 this.viewH = 1.0; 156 } 157 158 /** 159 * Returns the background. 160 * 161 * @return The background (possibly {@code null}). 162 * 163 * @see #setBackground(DialLayer) 164 */ 165 public DialLayer getBackground() { 166 return this.background; 167 } 168 169 /** 170 * Sets the background layer and sends a {@link PlotChangeEvent} to all 171 * registered listeners. 172 * 173 * @param background the background layer ({@code null} permitted). 174 * 175 * @see #getBackground() 176 */ 177 public void setBackground(DialLayer background) { 178 if (this.background != null) { 179 this.background.removeChangeListener(this); 180 } 181 this.background = background; 182 if (background != null) { 183 background.addChangeListener(this); 184 } 185 fireChangeEvent(); 186 } 187 188 /** 189 * Returns the cap. 190 * 191 * @return The cap (possibly {@code null}). 192 * 193 * @see #setCap(DialLayer) 194 */ 195 public DialLayer getCap() { 196 return this.cap; 197 } 198 199 /** 200 * Sets the cap and sends a {@link PlotChangeEvent} to all registered 201 * listeners. 202 * 203 * @param cap the cap ({@code null} permitted). 204 * 205 * @see #getCap() 206 */ 207 public void setCap(DialLayer cap) { 208 if (this.cap != null) { 209 this.cap.removeChangeListener(this); 210 } 211 this.cap = cap; 212 if (cap != null) { 213 cap.addChangeListener(this); 214 } 215 fireChangeEvent(); 216 } 217 218 /** 219 * Returns the dial's frame. 220 * 221 * @return The dial's frame (never {@code null}). 222 * 223 * @see #setDialFrame(DialFrame) 224 */ 225 public DialFrame getDialFrame() { 226 return this.dialFrame; 227 } 228 229 /** 230 * Sets the dial's frame and sends a {@link PlotChangeEvent} to all 231 * registered listeners. 232 * 233 * @param frame the frame ({@code null} not permitted). 234 * 235 * @see #getDialFrame() 236 */ 237 public void setDialFrame(DialFrame frame) { 238 Args.nullNotPermitted(frame, "frame"); 239 this.dialFrame.removeChangeListener(this); 240 this.dialFrame = frame; 241 frame.addChangeListener(this); 242 fireChangeEvent(); 243 } 244 245 /** 246 * Returns the x-coordinate of the viewing rectangle. This is specified 247 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 248 * 249 * @return The x-coordinate of the viewing rectangle. 250 * 251 * @see #setView(double, double, double, double) 252 */ 253 public double getViewX() { 254 return this.viewX; 255 } 256 257 /** 258 * Returns the y-coordinate of the viewing rectangle. This is specified 259 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 260 * 261 * @return The y-coordinate of the viewing rectangle. 262 * 263 * @see #setView(double, double, double, double) 264 */ 265 public double getViewY() { 266 return this.viewY; 267 } 268 269 /** 270 * Returns the width of the viewing rectangle. This is specified 271 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 272 * 273 * @return The width of the viewing rectangle. 274 * 275 * @see #setView(double, double, double, double) 276 */ 277 public double getViewWidth() { 278 return this.viewW; 279 } 280 281 /** 282 * Returns the height of the viewing rectangle. This is specified 283 * in the range 0.0 to 1.0, relative to the dial's framing rectangle. 284 * 285 * @return The height of the viewing rectangle. 286 * 287 * @see #setView(double, double, double, double) 288 */ 289 public double getViewHeight() { 290 return this.viewH; 291 } 292 293 /** 294 * Sets the viewing rectangle, relative to the dial's framing rectangle, 295 * and sends a {@link PlotChangeEvent} to all registered listeners. 296 * 297 * @param x the x-coordinate (in the range 0.0 to 1.0). 298 * @param y the y-coordinate (in the range 0.0 to 1.0). 299 * @param w the width (in the range 0.0 to 1.0). 300 * @param h the height (in the range 0.0 to 1.0). 301 * 302 * @see #getViewX() 303 * @see #getViewY() 304 * @see #getViewWidth() 305 * @see #getViewHeight() 306 */ 307 public void setView(double x, double y, double w, double h) { 308 this.viewX = x; 309 this.viewY = y; 310 this.viewW = w; 311 this.viewH = h; 312 fireChangeEvent(); 313 } 314 315 /** 316 * Adds a layer to the plot and sends a {@link PlotChangeEvent} to all 317 * registered listeners. 318 * 319 * @param layer the layer ({@code null} not permitted). 320 */ 321 public void addLayer(DialLayer layer) { 322 Args.nullNotPermitted(layer, "layer"); 323 this.layers.add(layer); 324 layer.addChangeListener(this); 325 fireChangeEvent(); 326 } 327 328 /** 329 * Returns the index for the specified layer. 330 * 331 * @param layer the layer ({@code null} not permitted). 332 * 333 * @return The layer index. 334 */ 335 public int getLayerIndex(DialLayer layer) { 336 Args.nullNotPermitted(layer, "layer"); 337 return this.layers.indexOf(layer); 338 } 339 340 /** 341 * Removes the layer at the specified index and sends a 342 * {@link PlotChangeEvent} to all registered listeners. 343 * 344 * @param index the index. 345 */ 346 public void removeLayer(int index) { 347 DialLayer layer = (DialLayer) this.layers.get(index); 348 if (layer != null) { 349 layer.removeChangeListener(this); 350 } 351 this.layers.remove(index); 352 fireChangeEvent(); 353 } 354 355 /** 356 * Removes the specified layer and sends a {@link PlotChangeEvent} to all 357 * registered listeners. 358 * 359 * @param layer the layer ({@code null} not permitted). 360 */ 361 public void removeLayer(DialLayer layer) { 362 // defer argument checking 363 removeLayer(getLayerIndex(layer)); 364 } 365 366 /** 367 * Adds a pointer to the plot and sends a {@link PlotChangeEvent} to all 368 * registered listeners. 369 * 370 * @param pointer the pointer ({@code null} not permitted). 371 */ 372 public void addPointer(DialPointer pointer) { 373 Args.nullNotPermitted(pointer, "pointer"); 374 this.pointers.add(pointer); 375 pointer.addChangeListener(this); 376 fireChangeEvent(); 377 } 378 379 /** 380 * Returns the index for the specified pointer. 381 * 382 * @param pointer the pointer ({@code null} not permitted). 383 * 384 * @return The pointer index. 385 */ 386 public int getPointerIndex(DialPointer pointer) { 387 Args.nullNotPermitted(pointer, "pointer"); 388 return this.pointers.indexOf(pointer); 389 } 390 391 /** 392 * Removes the pointer at the specified index and sends a 393 * {@link PlotChangeEvent} to all registered listeners. 394 * 395 * @param index the index. 396 */ 397 public void removePointer(int index) { 398 DialPointer pointer = (DialPointer) this.pointers.get(index); 399 if (pointer != null) { 400 pointer.removeChangeListener(this); 401 } 402 this.pointers.remove(index); 403 fireChangeEvent(); 404 } 405 406 /** 407 * Removes the specified pointer and sends a {@link PlotChangeEvent} to all 408 * registered listeners. 409 * 410 * @param pointer the pointer ({@code null} not permitted). 411 */ 412 public void removePointer(DialPointer pointer) { 413 // defer argument checking 414 removeLayer(getPointerIndex(pointer)); 415 } 416 417 /** 418 * Returns the dial pointer that is associated with the specified 419 * dataset, or {@code null}. 420 * 421 * @param datasetIndex the dataset index. 422 * 423 * @return The pointer. 424 */ 425 public DialPointer getPointerForDataset(int datasetIndex) { 426 DialPointer result = null; 427 Iterator iterator = this.pointers.iterator(); 428 while (iterator.hasNext()) { 429 DialPointer p = (DialPointer) iterator.next(); 430 if (p.getDatasetIndex() == datasetIndex) { 431 return p; 432 } 433 } 434 return result; 435 } 436 437 /** 438 * Returns the primary dataset for the plot. 439 * 440 * @return The primary dataset (possibly {@code null}). 441 */ 442 public ValueDataset getDataset() { 443 return getDataset(0); 444 } 445 446 /** 447 * Returns the dataset at the given index. 448 * 449 * @param index the dataset index. 450 * 451 * @return The dataset (possibly {@code null}). 452 */ 453 public ValueDataset getDataset(int index) { 454 ValueDataset result = null; 455 if (this.datasets.size() > index) { 456 result = (ValueDataset) this.datasets.get(index); 457 } 458 return result; 459 } 460 461 /** 462 * Sets the dataset for the plot, replacing the existing dataset, if there 463 * is one, and sends a {@link PlotChangeEvent} to all registered 464 * listeners. 465 * 466 * @param dataset the dataset ({@code null} permitted). 467 */ 468 public void setDataset(ValueDataset dataset) { 469 setDataset(0, dataset); 470 } 471 472 /** 473 * Sets a dataset for the plot. 474 * 475 * @param index the dataset index. 476 * @param dataset the dataset ({@code null} permitted). 477 */ 478 public void setDataset(int index, ValueDataset dataset) { 479 480 ValueDataset existing = (ValueDataset) this.datasets.get(index); 481 if (existing != null) { 482 existing.removeChangeListener(this); 483 } 484 this.datasets.set(index, dataset); 485 if (dataset != null) { 486 dataset.addChangeListener(this); 487 } 488 489 // send a dataset change event to self... 490 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 491 datasetChanged(event); 492 493 } 494 495 /** 496 * Returns the number of datasets. 497 * 498 * @return The number of datasets. 499 */ 500 public int getDatasetCount() { 501 return this.datasets.size(); 502 } 503 504 /** 505 * Draws the plot. This method is usually called by the {@link JFreeChart} 506 * instance that manages the plot. 507 * 508 * @param g2 the graphics target. 509 * @param area the area in which the plot should be drawn. 510 * @param anchor the anchor point (typically the last point that the 511 * mouse clicked on, {@code null} is permitted). 512 * @param parentState the state for the parent plot (if any). 513 * @param info used to collect plot rendering info ({@code null} 514 * permitted). 515 */ 516 @Override 517 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 518 PlotState parentState, PlotRenderingInfo info) { 519 520 Shape origClip = g2.getClip(); 521 g2.setClip(area); 522 523 // first, expand the viewing area into a drawing frame 524 Rectangle2D frame = viewToFrame(area); 525 526 // draw the background if there is one... 527 if (this.background != null && this.background.isVisible()) { 528 if (this.background.isClippedToWindow()) { 529 Shape savedClip = g2.getClip(); 530 g2.clip(this.dialFrame.getWindow(frame)); 531 this.background.draw(g2, this, frame, area); 532 g2.setClip(savedClip); 533 } 534 else { 535 this.background.draw(g2, this, frame, area); 536 } 537 } 538 539 Iterator iterator = this.layers.iterator(); 540 while (iterator.hasNext()) { 541 DialLayer current = (DialLayer) iterator.next(); 542 if (current.isVisible()) { 543 if (current.isClippedToWindow()) { 544 Shape savedClip = g2.getClip(); 545 g2.clip(this.dialFrame.getWindow(frame)); 546 current.draw(g2, this, frame, area); 547 g2.setClip(savedClip); 548 } 549 else { 550 current.draw(g2, this, frame, area); 551 } 552 } 553 } 554 555 // draw the pointers 556 iterator = this.pointers.iterator(); 557 while (iterator.hasNext()) { 558 DialPointer current = (DialPointer) iterator.next(); 559 if (current.isVisible()) { 560 if (current.isClippedToWindow()) { 561 Shape savedClip = g2.getClip(); 562 g2.clip(this.dialFrame.getWindow(frame)); 563 current.draw(g2, this, frame, area); 564 g2.setClip(savedClip); 565 } 566 else { 567 current.draw(g2, this, frame, area); 568 } 569 } 570 } 571 572 // draw the cap if there is one... 573 if (this.cap != null && this.cap.isVisible()) { 574 if (this.cap.isClippedToWindow()) { 575 Shape savedClip = g2.getClip(); 576 g2.clip(this.dialFrame.getWindow(frame)); 577 this.cap.draw(g2, this, frame, area); 578 g2.setClip(savedClip); 579 } 580 else { 581 this.cap.draw(g2, this, frame, area); 582 } 583 } 584 585 if (this.dialFrame.isVisible()) { 586 this.dialFrame.draw(g2, this, frame, area); 587 } 588 589 g2.setClip(origClip); 590 591 } 592 593 /** 594 * Returns the frame surrounding the specified view rectangle. 595 * 596 * @param view the view rectangle ({@code null} not permitted). 597 * 598 * @return The frame rectangle. 599 */ 600 private Rectangle2D viewToFrame(Rectangle2D view) { 601 double width = view.getWidth() / this.viewW; 602 double height = view.getHeight() / this.viewH; 603 double x = view.getX() - (width * this.viewX); 604 double y = view.getY() - (height * this.viewY); 605 return new Rectangle2D.Double(x, y, width, height); 606 } 607 608 /** 609 * Returns the value from the specified dataset. 610 * 611 * @param datasetIndex the dataset index. 612 * 613 * @return The data value. 614 */ 615 public double getValue(int datasetIndex) { 616 double result = Double.NaN; 617 ValueDataset dataset = getDataset(datasetIndex); 618 if (dataset != null) { 619 Number n = dataset.getValue(); 620 if (n != null) { 621 result = n.doubleValue(); 622 } 623 } 624 return result; 625 } 626 627 /** 628 * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to 629 * all registered listeners. 630 * 631 * @param index the scale index. 632 * @param scale the scale ({@code null} not permitted). 633 */ 634 public void addScale(int index, DialScale scale) { 635 Args.nullNotPermitted(scale, "scale"); 636 DialScale existing = (DialScale) this.scales.get(index); 637 if (existing != null) { 638 removeLayer(existing); 639 } 640 this.layers.add(scale); 641 this.scales.set(index, scale); 642 scale.addChangeListener(this); 643 fireChangeEvent(); 644 } 645 646 /** 647 * Returns the scale at the given index. 648 * 649 * @param index the scale index. 650 * 651 * @return The scale (possibly {@code null}). 652 */ 653 public DialScale getScale(int index) { 654 DialScale result = null; 655 if (this.scales.size() > index) { 656 result = (DialScale) this.scales.get(index); 657 } 658 return result; 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.set(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 = (DialScale) this.scales.get(0); 681 Integer scaleIndex = (Integer) 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 + ObjectUtils.hashCode(this.background); 775 result = 37 * result + ObjectUtils.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 823}