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