001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, 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-2022, 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.ArrayList;
047import java.util.HashMap;
048import java.util.List;
049import java.util.Map;
050import java.util.Objects;
051import org.jfree.chart.ChartElementVisitor;
052
053import org.jfree.chart.JFreeChart;
054import org.jfree.chart.event.PlotChangeEvent;
055import org.jfree.chart.plot.Plot;
056import org.jfree.chart.plot.PlotRenderingInfo;
057import org.jfree.chart.plot.PlotState;
058import org.jfree.chart.internal.Args;
059import org.jfree.data.general.DatasetChangeEvent;
060import org.jfree.data.general.ValueDataset;
061
062/**
063 * A dial plot composed of user-definable layers.
064 * The example shown here is generated by the {@code DialDemo2.java}
065 * program included in the JFreeChart Demo Collection:
066 * <br><br>
067 * <img src="doc-files/DialPlotSample.png" alt="DialPlotSample.png">
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 Map<Integer, ValueDataset> datasets;
090
091    /**
092     * The scale(s) for the dial plot.
093     */
094    private Map<Integer, DialScale> scales;
095
096    /** Storage for keys that map datasets to scales. */
097    private Map<Integer, Integer> datasetToScaleMap;
098
099    /**
100     * The drawing layers for the dial plot.
101     */
102    private List<DialLayer> layers;
103
104    /**
105     * The pointer(s) for the dial.
106     */
107    private List<DialPointer> 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 HashMap<>();
146        if (dataset != null) {
147            setDataset(dataset);
148        }
149        this.scales = new HashMap<>();
150        this.datasetToScaleMap = new HashMap<>();
151        this.layers = new ArrayList<>();
152        this.pointers = new 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 = 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 = 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        for (DialPointer p : this.pointers) {
429            if (p.getDatasetIndex() == datasetIndex) {
430                return p;
431            }
432        }
433        return result;
434    }
435
436    /**
437     * Returns the primary dataset for the plot.
438     *
439     * @return The primary dataset (possibly {@code null}).
440     */
441    public ValueDataset getDataset() {
442        return getDataset(0);
443    }
444
445    /**
446     * Returns the dataset at the given index.
447     *
448     * @param index  the dataset index.
449     *
450     * @return The dataset (possibly {@code null}).
451     */
452    public ValueDataset getDataset(int index) {
453        ValueDataset result = null;
454        if (this.datasets.size() > index) {
455            result = (ValueDataset) this.datasets.get(index);
456        }
457        return result;
458    }
459
460    /**
461     * Sets the dataset for the plot, replacing the existing dataset, if there
462     * is one, and sends a {@link PlotChangeEvent} to all registered
463     * listeners.
464     *
465     * @param dataset  the dataset ({@code null} permitted).
466     */
467    public void setDataset(ValueDataset dataset) {
468        setDataset(0, dataset);
469    }
470
471    /**
472     * Sets a dataset for the plot.
473     *
474     * @param index  the dataset index.
475     * @param dataset  the dataset ({@code null} permitted).
476     */
477    public void setDataset(int index, ValueDataset dataset) {
478        ValueDataset existing = this.datasets.get(index);
479        if (existing != null) {
480            existing.removeChangeListener(this);
481        }
482        this.datasets.put(index, dataset);
483        if (dataset != null) {
484            dataset.addChangeListener(this);
485        }
486
487        // send a dataset change event to self...
488        DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
489        datasetChanged(event);
490    }
491
492    /**
493     * Returns the number of datasets.
494     *
495     * @return The number of datasets.
496     */
497    public int getDatasetCount() {
498        return this.datasets.size();
499    }
500
501    /**
502     * Receives a chart element visitor.  Many plot subclasses will override
503     * this method to handle their subcomponents.
504     * 
505     * @param visitor  the visitor ({@code null} not permitted).
506     */
507    @Override
508    public void receive(ChartElementVisitor visitor) {
509        // FIXME : handle the subcomponents
510        super.receive(visitor);
511    }
512
513
514    /**
515     * Draws the plot.  This method is usually called by the {@link JFreeChart}
516     * instance that manages the plot.
517     *
518     * @param g2  the graphics target.
519     * @param area  the area in which the plot should be drawn.
520     * @param anchor  the anchor point (typically the last point that the
521     *     mouse clicked on, {@code null} is permitted).
522     * @param parentState  the state for the parent plot (if any).
523     * @param info  used to collect plot rendering info ({@code null}
524     *     permitted).
525     */
526    @Override
527    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
528            PlotState parentState, PlotRenderingInfo info) {
529
530        Shape origClip = g2.getClip();
531        g2.setClip(area);
532
533        // first, expand the viewing area into a drawing frame
534        Rectangle2D frame = viewToFrame(area);
535
536        // draw the background if there is one...
537        if (this.background != null && this.background.isVisible()) {
538            if (this.background.isClippedToWindow()) {
539                Shape savedClip = g2.getClip();
540                g2.clip(this.dialFrame.getWindow(frame));
541                this.background.draw(g2, this, frame, area);
542                g2.setClip(savedClip);
543            }
544            else {
545                this.background.draw(g2, this, frame, area);
546            }
547        }
548
549        for (DialLayer current : this.layers) {
550            if (current.isVisible()) {
551                if (current.isClippedToWindow()) {
552                    Shape savedClip = g2.getClip();
553                    g2.clip(this.dialFrame.getWindow(frame));
554                    current.draw(g2, this, frame, area);
555                    g2.setClip(savedClip);
556                }
557                else {
558                    current.draw(g2, this, frame, area);
559                }
560            }
561        }
562
563        // draw the pointers
564        for (DialPointer current : this.pointers) {
565            if (current.isVisible()) {
566                if (current.isClippedToWindow()) {
567                    Shape savedClip = g2.getClip();
568                    g2.clip(this.dialFrame.getWindow(frame));
569                    current.draw(g2, this, frame, area);
570                    g2.setClip(savedClip);
571                } else {
572                    current.draw(g2, this, frame, area);
573                }
574            }
575        }
576
577        // draw the cap if there is one...
578        if (this.cap != null && this.cap.isVisible()) {
579            if (this.cap.isClippedToWindow()) {
580                Shape savedClip = g2.getClip();
581                g2.clip(this.dialFrame.getWindow(frame));
582                this.cap.draw(g2, this, frame, area);
583                g2.setClip(savedClip);
584            } else {
585                this.cap.draw(g2, this, frame, area);
586            }
587        }
588
589        if (this.dialFrame.isVisible()) {
590            this.dialFrame.draw(g2, this, frame, area);
591        }
592
593        g2.setClip(origClip);
594
595    }
596
597    /**
598     * Returns the frame surrounding the specified view rectangle.
599     *
600     * @param view  the view rectangle ({@code null} not permitted).
601     *
602     * @return The frame rectangle.
603     */
604    private Rectangle2D viewToFrame(Rectangle2D view) {
605        double width = view.getWidth() / this.viewW;
606        double height = view.getHeight() / this.viewH;
607        double x = view.getX() - (width * this.viewX);
608        double y = view.getY() - (height * this.viewY);
609        return new Rectangle2D.Double(x, y, width, height);
610    }
611
612    /**
613     * Returns the value from the specified dataset.
614     *
615     * @param datasetIndex  the dataset index.
616     *
617     * @return The data value.
618     */
619    public double getValue(int datasetIndex) {
620        double result = Double.NaN;
621        ValueDataset dataset = getDataset(datasetIndex);
622        if (dataset != null) {
623            Number n = dataset.getValue();
624            if (n != null) {
625                result = n.doubleValue();
626            }
627        }
628        return result;
629    }
630
631    /**
632     * Adds a dial scale to the plot and sends a {@link PlotChangeEvent} to
633     * all registered listeners.
634     *
635     * @param index  the scale index.
636     * @param scale  the scale ({@code null} not permitted).
637     */
638    public void addScale(int index, DialScale scale) {
639        Args.nullNotPermitted(scale, "scale");
640        DialScale existing = this.scales.get(index);
641        if (existing != null) {
642            removeLayer(existing);
643        }
644        this.layers.add(scale);
645        this.scales.put(index, scale);
646        scale.addChangeListener(this);
647        fireChangeEvent();
648    }
649
650    /**
651     * Returns the scale at the given index.
652     *
653     * @param index  the scale index.
654     *
655     * @return The scale (possibly {@code null}).
656     */
657    public DialScale getScale(int index) {
658        return this.scales.get(index);
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.put(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 = this.scales.get(0);
681        Integer scaleIndex = 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 + Objects.hashCode(this.background);
775        result = 37 * result + Objects.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}