001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -------------
028 * DialPlot.java
029 * -------------
030 * (C) Copyright 2006-2014, 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-2008 : Fixed clipping bug (1873160) (DG);
043 * 03-Jul-2013 : Use ParamChecks (DG);
044 *
045 */
046
047package org.jfree.chart.plot.dial;
048
049import java.awt.Graphics2D;
050import java.awt.Shape;
051import java.awt.geom.Point2D;
052import java.awt.geom.Rectangle2D;
053import java.io.IOException;
054import java.io.ObjectInputStream;
055import java.io.ObjectOutputStream;
056import java.util.Iterator;
057import java.util.List;
058
059import org.jfree.chart.JFreeChart;
060import org.jfree.chart.event.PlotChangeEvent;
061import org.jfree.chart.plot.Plot;
062import org.jfree.chart.plot.PlotRenderingInfo;
063import org.jfree.chart.plot.PlotState;
064import org.jfree.chart.util.ParamChecks;
065import org.jfree.data.general.DatasetChangeEvent;
066import org.jfree.data.general.ValueDataset;
067import org.jfree.util.ObjectList;
068import org.jfree.util.ObjectUtilities;
069
070/**
071 * A dial plot composed of user-definable layers.
072 * The example shown here is generated by the <code>DialDemo2.java</code>
073 * program included in the JFreeChart Demo Collection:
074 * <br><br>
075 * <img src="../../../../../images/DialPlotSample.png"
076 * alt="DialPlotSample.png">
077 *
078 * @since 1.0.7
079 */
080public class DialPlot extends Plot implements DialLayerChangeListener {
081
082    /**
083     * The background layer (optional).
084     */
085    private DialLayer background;
086
087    /**
088     * The needle cap (optional).
089     */
090    private DialLayer cap;
091
092    /**
093     * The dial frame.
094     */
095    private DialFrame dialFrame;
096
097    /**
098     * The dataset(s) for the dial plot.
099     */
100    private ObjectList datasets;
101
102    /**
103     * The scale(s) for the dial plot.
104     */
105    private ObjectList scales;
106
107    /** Storage for keys that map datasets to scales. */
108    private ObjectList datasetToScaleMap;
109
110    /**
111     * The drawing layers for the dial plot.
112     */
113    private List layers;
114
115    /**
116     * The pointer(s) for the dial.
117     */
118    private List pointers;
119
120    /**
121     * The x-coordinate for the view window.
122     */
123    private double viewX;
124
125    /**
126     * The y-coordinate for the view window.
127     */
128    private double viewY;
129
130    /**
131     * The width of the view window, expressed as a percentage.
132     */
133    private double viewW;
134
135    /**
136     * The height of the view window, expressed as a percentage.
137     */
138    private double viewH;
139
140    /**
141     * Creates a new instance of <code>DialPlot</code>.
142     */
143    public DialPlot() {
144        this(null);
145    }
146
147    /**
148     * Creates a new instance of <code>DialPlot</code>.
149     *
150     * @param dataset  the dataset (<code>null</code> permitted).
151     */
152    public DialPlot(ValueDataset dataset) {
153        this.background = null;
154        this.cap = null;
155        this.dialFrame = new ArcDialFrame();
156        this.datasets = new ObjectList();
157        if (dataset != null) {
158            setDataset(dataset);
159        }
160        this.scales = new ObjectList();
161        this.datasetToScaleMap = new ObjectList();
162        this.layers = new java.util.ArrayList();
163        this.pointers = new java.util.ArrayList();
164        this.viewX = 0.0;
165        this.viewY = 0.0;
166        this.viewW = 1.0;
167        this.viewH = 1.0;
168    }
169
170    /**
171     * Returns the background.
172     *
173     * @return The background (possibly <code>null</code>).
174     *
175     * @see #setBackground(DialLayer)
176     */
177    public DialLayer getBackground() {
178        return this.background;
179    }
180
181    /**
182     * Sets the background layer and sends a {@link PlotChangeEvent} to all
183     * registered listeners.
184     *
185     * @param background  the background layer (<code>null</code> permitted).
186     *
187     * @see #getBackground()
188     */
189    public void setBackground(DialLayer background) {
190        if (this.background != null) {
191            this.background.removeChangeListener(this);
192        }
193        this.background = background;
194        if (background != null) {
195            background.addChangeListener(this);
196        }
197        fireChangeEvent();
198    }
199
200    /**
201     * Returns the cap.
202     *
203     * @return The cap (possibly <code>null</code>).
204     *
205     * @see #setCap(DialLayer)
206     */
207    public DialLayer getCap() {
208        return this.cap;
209    }
210
211    /**
212     * Sets the cap and sends a {@link PlotChangeEvent} to all registered
213     * listeners.
214     *
215     * @param cap  the cap (<code>null</code> permitted).
216     *
217     * @see #getCap()
218     */
219    public void setCap(DialLayer cap) {
220        if (this.cap != null) {
221            this.cap.removeChangeListener(this);
222        }
223        this.cap = cap;
224        if (cap != null) {
225            cap.addChangeListener(this);
226        }
227        fireChangeEvent();
228    }
229
230    /**
231     * Returns the dial's frame.
232     *
233     * @return The dial's frame (never <code>null</code>).
234     *
235     * @see #setDialFrame(DialFrame)
236     */
237    public DialFrame getDialFrame() {
238        return this.dialFrame;
239    }
240
241    /**
242     * Sets the dial's frame and sends a {@link PlotChangeEvent} to all
243     * registered listeners.
244     *
245     * @param frame  the frame (<code>null</code> not permitted).
246     *
247     * @see #getDialFrame()
248     */
249    public void setDialFrame(DialFrame frame) {
250        ParamChecks.nullNotPermitted(frame, "frame");
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        ParamChecks.nullNotPermitted(layer, "layer");
335        this.layers.add(layer);
336        layer.addChangeListener(this);
337        fireChangeEvent();
338    }
339
340    /**
341     * Returns the index for the specified layer.
342     *
343     * @param layer  the layer (<code>null</code> not permitted).
344     *
345     * @return The layer index.
346     */
347    public int getLayerIndex(DialLayer layer) {
348        ParamChecks.nullNotPermitted(layer, "layer");
349        return this.layers.indexOf(layer);
350    }
351
352    /**
353     * Removes the layer at the specified index and sends a
354     * {@link PlotChangeEvent} to all registered listeners.
355     *
356     * @param index  the index.
357     */
358    public void removeLayer(int index) {
359        DialLayer layer = (DialLayer) this.layers.get(index);
360        if (layer != null) {
361            layer.removeChangeListener(this);
362        }
363        this.layers.remove(index);
364        fireChangeEvent();
365    }
366
367    /**
368     * Removes the specified layer and sends a {@link PlotChangeEvent} to all
369     * registered listeners.
370     *
371     * @param layer  the layer (<code>null</code> not permitted).
372     */
373    public void removeLayer(DialLayer layer) {
374        // defer argument checking
375        removeLayer(getLayerIndex(layer));
376    }
377
378    /**
379     * Adds a pointer to the plot and sends a {@link PlotChangeEvent} to all
380     * registered listeners.
381     *
382     * @param pointer  the pointer (<code>null</code> not permitted).
383     */
384    public void addPointer(DialPointer pointer) {
385        ParamChecks.nullNotPermitted(pointer, "pointer");
386        this.pointers.add(pointer);
387        pointer.addChangeListener(this);
388        fireChangeEvent();
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        ParamChecks.nullNotPermitted(pointer, "pointer");
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        fireChangeEvent();
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    @Override
529    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
530            PlotState parentState, PlotRenderingInfo info) {
531
532        Shape origClip = g2.getClip();
533        g2.setClip(area);
534
535        // first, expand the viewing area into a drawing frame
536        Rectangle2D frame = viewToFrame(area);
537
538        // draw the background if there is one...
539        if (this.background != null && this.background.isVisible()) {
540            if (this.background.isClippedToWindow()) {
541                Shape savedClip = g2.getClip();
542                g2.clip(this.dialFrame.getWindow(frame));
543                this.background.draw(g2, this, frame, area);
544                g2.setClip(savedClip);
545            }
546            else {
547                this.background.draw(g2, this, frame, area);
548            }
549        }
550
551        Iterator iterator = this.layers.iterator();
552        while (iterator.hasNext()) {
553            DialLayer current = (DialLayer) iterator.next();
554            if (current.isVisible()) {
555                if (current.isClippedToWindow()) {
556                    Shape savedClip = g2.getClip();
557                    g2.clip(this.dialFrame.getWindow(frame));
558                    current.draw(g2, this, frame, area);
559                    g2.setClip(savedClip);
560                }
561                else {
562                    current.draw(g2, this, frame, area);
563                }
564            }
565        }
566
567        // draw the pointers
568        iterator = this.pointers.iterator();
569        while (iterator.hasNext()) {
570            DialPointer current = (DialPointer) iterator.next();
571            if (current.isVisible()) {
572                if (current.isClippedToWindow()) {
573                    Shape savedClip = g2.getClip();
574                    g2.clip(this.dialFrame.getWindow(frame));
575                    current.draw(g2, this, frame, area);
576                    g2.setClip(savedClip);
577                }
578                else {
579                    current.draw(g2, this, frame, area);
580                }
581            }
582        }
583
584        // draw the cap if there is one...
585        if (this.cap != null && this.cap.isVisible()) {
586            if (this.cap.isClippedToWindow()) {
587                Shape savedClip = g2.getClip();
588                g2.clip(this.dialFrame.getWindow(frame));
589                this.cap.draw(g2, this, frame, area);
590                g2.setClip(savedClip);
591            }
592            else {
593                this.cap.draw(g2, this, frame, area);
594            }
595        }
596
597        if (this.dialFrame.isVisible()) {
598            this.dialFrame.draw(g2, this, frame, area);
599        }
600
601        g2.setClip(origClip);
602
603    }
604
605    /**
606     * Returns the frame surrounding the specified view rectangle.
607     *
608     * @param view  the view rectangle (<code>null</code> not permitted).
609     *
610     * @return The frame rectangle.
611     */
612    private Rectangle2D viewToFrame(Rectangle2D view) {
613        double width = view.getWidth() / this.viewW;
614        double height = view.getHeight() / this.viewH;
615        double x = view.getX() - (width * this.viewX);
616        double y = view.getY() - (height * this.viewY);
617        return new Rectangle2D.Double(x, y, width, height);
618    }
619
620    /**
621     * Returns the value from the specified dataset.
622     *
623     * @param datasetIndex  the dataset index.
624     *
625     * @return The data value.
626     */
627    public double getValue(int datasetIndex) {
628        double result = Double.NaN;
629        ValueDataset dataset = getDataset(datasetIndex);
630        if (dataset != null) {
631            Number n = dataset.getValue();
632            if (n != null) {
633                result = n.doubleValue();
634            }
635        }
636        return result;
637    }
638
639    /**
640     * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to
641     * all registered listeners.
642     *
643     * @param index  the scale index.
644     * @param scale  the scale (<code>null</code> not permitted).
645     */
646    public void addScale(int index, DialScale scale) {
647        ParamChecks.nullNotPermitted(scale, "scale");
648        DialScale existing = (DialScale) this.scales.get(index);
649        if (existing != null) {
650            removeLayer(existing);
651        }
652        this.layers.add(scale);
653        this.scales.set(index, scale);
654        scale.addChangeListener(this);
655        fireChangeEvent();
656    }
657
658    /**
659     * Returns the scale at the given index.
660     *
661     * @param index  the scale index.
662     *
663     * @return The scale (possibly <code>null</code>).
664     */
665    public DialScale getScale(int index) {
666        DialScale result = null;
667        if (this.scales.size() > index) {
668            result = (DialScale) this.scales.get(index);
669        }
670        return result;
671    }
672
673    /**
674     * Maps a dataset to a particular scale.
675     *
676     * @param index  the dataset index (zero-based).
677     * @param scaleIndex  the scale index (zero-based).
678     */
679    public void mapDatasetToScale(int index, int scaleIndex) {
680        this.datasetToScaleMap.set(index, new Integer(scaleIndex));
681        fireChangeEvent();
682    }
683
684    /**
685     * Returns the dial scale for a specific dataset.
686     *
687     * @param datasetIndex  the dataset index.
688     *
689     * @return The dial scale.
690     */
691    public DialScale getScaleForDataset(int datasetIndex) {
692        DialScale result = (DialScale) this.scales.get(0);
693        Integer scaleIndex = (Integer) this.datasetToScaleMap.get(datasetIndex);
694        if (scaleIndex != null) {
695            result = getScale(scaleIndex.intValue());
696        }
697        return result;
698    }
699
700    /**
701     * A utility method that computes a rectangle using relative radius values.
702     *
703     * @param rect  the reference rectangle ({@code null} not permitted).
704     * @param radiusW  the width radius (must be &gt; 0.0)
705     * @param radiusH  the height radius.
706     *
707     * @return A new rectangle.
708     */
709    public static Rectangle2D rectangleByRadius(Rectangle2D rect,
710            double radiusW, double radiusH) {
711        ParamChecks.nullNotPermitted(rect, "rect");
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    @Override
726    public void dialLayerChanged(DialLayerChangeEvent event) {
727        fireChangeEvent();
728    }
729
730    /**
731     * Tests this <code>DialPlot</code> instance for equality with an
732     * arbitrary object.  The plot's dataset(s) is (are) not included in
733     * the test.
734     *
735     * @param obj  the object (<code>null</code> permitted).
736     *
737     * @return A boolean.
738     */
739    @Override
740    public boolean equals(Object obj) {
741        if (obj == this) {
742            return true;
743        }
744        if (!(obj instanceof DialPlot)) {
745            return false;
746        }
747        DialPlot that = (DialPlot) obj;
748        if (!ObjectUtilities.equal(this.background, that.background)) {
749            return false;
750        }
751        if (!ObjectUtilities.equal(this.cap, that.cap)) {
752            return false;
753        }
754        if (!this.dialFrame.equals(that.dialFrame)) {
755            return false;
756        }
757        if (this.viewX != that.viewX) {
758            return false;
759        }
760        if (this.viewY != that.viewY) {
761            return false;
762        }
763        if (this.viewW != that.viewW) {
764            return false;
765        }
766        if (this.viewH != that.viewH) {
767            return false;
768        }
769        if (!this.layers.equals(that.layers)) {
770            return false;
771        }
772        if (!this.pointers.equals(that.pointers)) {
773            return false;
774        }
775        return super.equals(obj);
776    }
777
778    /**
779     * Returns a hash code for this instance.
780     *
781     * @return The hash code.
782     */
783    @Override
784    public int hashCode() {
785        int result = 193;
786        result = 37 * result + ObjectUtilities.hashCode(this.background);
787        result = 37 * result + ObjectUtilities.hashCode(this.cap);
788        result = 37 * result + this.dialFrame.hashCode();
789        long temp = Double.doubleToLongBits(this.viewX);
790        result = 37 * result + (int) (temp ^ (temp >>> 32));
791        temp = Double.doubleToLongBits(this.viewY);
792        result = 37 * result + (int) (temp ^ (temp >>> 32));
793        temp = Double.doubleToLongBits(this.viewW);
794        result = 37 * result + (int) (temp ^ (temp >>> 32));
795        temp = Double.doubleToLongBits(this.viewH);
796        result = 37 * result + (int) (temp ^ (temp >>> 32));
797        return result;
798    }
799
800    /**
801     * Returns the plot type.
802     *
803     * @return <code>"DialPlot"</code>
804     */
805    @Override
806    public String getPlotType() {
807        return "DialPlot";
808    }
809
810    /**
811     * Provides serialization support.
812     *
813     * @param stream  the output stream.
814     *
815     * @throws IOException  if there is an I/O error.
816     */
817    private void writeObject(ObjectOutputStream stream) throws IOException {
818        stream.defaultWriteObject();
819    }
820
821    /**
822     * Provides serialization support.
823     *
824     * @param stream  the input stream.
825     *
826     * @throws IOException  if there is an I/O error.
827     * @throws ClassNotFoundException  if there is a classpath problem.
828     */
829    private void readObject(ObjectInputStream stream)
830            throws IOException, ClassNotFoundException {
831        stream.defaultReadObject();
832    }
833
834
835}