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