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