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 }