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