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