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