001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
006 * 
007 * http://www.object-refinery.com/orsoncharts/index.html
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package com.orsoncharts.plot;
034
035import java.awt.Color;
036import java.awt.Font;
037import java.io.Serializable;
038import java.util.ArrayList;
039import java.util.List;
040
041import com.orsoncharts.Chart3D;
042import com.orsoncharts.data.PieDataset3D;
043import com.orsoncharts.util.ArgChecks;
044import com.orsoncharts.graphics3d.Dimension3D;
045import com.orsoncharts.graphics3d.Dot3D;
046import com.orsoncharts.graphics3d.Object3D;
047import com.orsoncharts.graphics3d.World;
048import com.orsoncharts.legend.LegendItemInfo;
049import com.orsoncharts.legend.StandardLegendItemInfo;
050import com.orsoncharts.Chart3DFactory;
051import com.orsoncharts.ChartElementVisitor;
052import com.orsoncharts.data.DataUtils;
053import com.orsoncharts.data.ItemKey;
054import com.orsoncharts.data.KeyedValuesItemKey;
055import com.orsoncharts.label.PieLabelGenerator;
056import com.orsoncharts.label.StandardPieLabelGenerator;
057
058/**
059 * A plot for creating 3D pie charts.  To create a pie chart, you can use the 
060 * {@code createPieChart()} method in the {@link Chart3DFactory} class.  
061 * A typical pie chart will look like this:  
062 * <div>
063 * <object id="ABC" data="../../../doc-files/PieChart3DDemo1.svg"  
064 * type="image/svg+xml" width="500" height="359"> 
065 * </object>
066 * </div>
067 * <br><br>
068 * NOTE: This class is serializable, but the serialization format is subject 
069 * to change in future releases and should not be relied upon for persisting 
070 * instances of this class. 
071 */
072@SuppressWarnings("serial")
073public class PiePlot3D extends AbstractPlot3D implements Serializable {
074    
075    /** The default font for section labels on the chart. */
076    public static final Font DEFAULT_SECTION_LABEL_FONT 
077            = new Font("Dialog", Font.PLAIN, 14);
078    
079    /** The dataset. */
080    private PieDataset3D<? extends Comparable> dataset;
081
082    /** The radius of the pie chart. */
083    private double radius; 
084  
085    /** The depth of the pie chart. */
086    private double depth;
087  
088    /** The section color source. */
089    private ColorSource sectionColorSource;
090
091    /** The section label generator. */
092    private PieLabelGenerator sectionLabelGenerator;
093    
094    /** The font source used to determine the font for section labels. */
095    private FontSource sectionLabelFontSource;
096
097    /** 
098     * The color source used to determine the foreground color for section 
099     * labels. 
100     */
101    private ColorSource sectionLabelColorSource;
102    
103    /** The legend label generator. */
104    private PieLabelGenerator legendLabelGenerator;
105    
106    /** 
107     * The tool tip generator (can be null, in which case there will be no
108     * tool tips. */
109    private PieLabelGenerator toolTipGenerator;
110    
111    /** 
112     * The number of segments used to render 360 degrees of the pie.  A higher
113     * number will give better output but slower performance.
114     */
115    private int segments = 40;
116  
117    /**
118     * Creates a new pie plot in 3D.
119     * 
120     * @param dataset  the dataset ({@code null} not permitted). 
121     */
122    public PiePlot3D(PieDataset3D<? extends Comparable> dataset) {
123        ArgChecks.nullNotPermitted(dataset, "dataset");
124        this.dataset = dataset;
125        this.dataset.addChangeListener(this);
126        this.radius = 4.0;    
127        this.depth = 0.5;
128        this.sectionColorSource = new StandardColorSource();
129        this.sectionLabelGenerator = new StandardPieLabelGenerator(
130                StandardPieLabelGenerator.KEY_ONLY_TEMPLATE);
131        this.sectionLabelFontSource = new StandardFontSource(
132                DEFAULT_SECTION_LABEL_FONT);
133        this.sectionLabelColorSource = new StandardColorSource(Color.BLACK);
134        this.legendLabelGenerator = new StandardPieLabelGenerator();
135        this.toolTipGenerator = new StandardPieLabelGenerator(
136                StandardPieLabelGenerator.PERCENT_TEMPLATE_2DP);
137    }
138
139    /**
140     * Returns the dataset.
141     * 
142     * @return The dataset (never {@code null}). 
143     */
144    public PieDataset3D<? extends Comparable> getDataset() {
145        return this.dataset;
146    }
147
148    /**
149     * Sets the dataset and notifies registered listeners that the dataset has
150     * been updated.
151     * 
152     * @param dataset  the dataset ({@code null} not permitted). 
153     */
154    public void setDataset(PieDataset3D<? extends Comparable> dataset) {
155        ArgChecks.nullNotPermitted(dataset, "dataset");
156        this.dataset.removeChangeListener(this);
157        this.dataset = dataset;
158        this.dataset.addChangeListener(this);
159        fireChangeEvent(true);
160    }
161
162    /**
163     * Returns the radius of the pie (the default value is 8.0).
164     * 
165     * @return The radius of the pie. 
166     */
167    public double getRadius() {
168        return this.radius;
169    }
170  
171    /**
172     * Sets the radius of the pie chart and sends a change event to all 
173     * registered listeners.
174     * 
175     * @param radius  the radius. 
176     */
177    public void setRadius(double radius) {
178        this.radius = radius;
179        fireChangeEvent(true);
180    }
181  
182    /**
183     * Returns the depth of the pie (the default value is 2.0).
184     * 
185     * @return The depth of the pie. 
186     */
187    public double getDepth() {
188        return this.depth;
189    }
190
191    /**
192     * Sets the depth of the pie chart and sends a change event to all 
193     * registered listeners.
194     * 
195     * @param depth  the depth. 
196     */
197    public void setDepth(double depth) {
198        this.depth = depth;
199        fireChangeEvent(true);
200    }
201    
202    /**
203     * Returns the color source for section colors.
204     * 
205     * @return The color source (never {@code null}).
206     */
207    public ColorSource getSectionColorSource() {
208        return this.sectionColorSource;
209    }
210    
211    /**
212     * Sets the color source and sends a {@link Plot3DChangeEvent} to all 
213     * registered listeners.
214     * 
215     * @param source  the color source ({@code null} not permitted). 
216     */
217    public void setSectionColorSource(ColorSource source) {
218        ArgChecks.nullNotPermitted(source, "source");
219        this.sectionColorSource = source;
220        fireChangeEvent(true);
221    }
222    
223    /**
224     * Sets a new color source for the plot using the specified colors and
225     * sends a {@link Plot3DChangeEvent} to all registered listeners. This 
226     * is a convenience method that is equivalent to 
227     * {@code setSectionColorSource(new StandardColorSource(colors))}.
228     * 
229     * @param colors  one or more colors ({@code null} not permitted).
230     * 
231     * @since 1.2
232     */
233    public void setSectionColors(Color... colors) {
234        setSectionColorSource(new StandardColorSource(colors));
235    }
236
237    /**
238     * Returns the object that creates labels for each section of the pie
239     * chart.
240     * 
241     * @return The section label generator (never {@code null}).
242     * 
243     * @since 1.2
244     */
245    public PieLabelGenerator getSectionLabelGenerator() {
246        return this.sectionLabelGenerator;    
247    }
248    
249    /**
250     * Sets the object that creates labels for each section of the pie chart,
251     * and sends a {@link Plot3DChangeEvent} to all registered listeners.
252     * 
253     * @param generator  the generator ({@code null} not permitted).
254     * 
255     * @since 1.2
256     */
257    public void setSectionLabelGenerator(PieLabelGenerator generator) {
258        ArgChecks.nullNotPermitted(generator, "generator");
259        this.sectionLabelGenerator = generator;
260        fireChangeEvent(false);
261    }
262    
263    /**
264     * Returns the font source that is used to determine the font to use for 
265     * the section labels.
266     * 
267     * @return The font source for the section labels (never {@code null}). 
268     */
269    public FontSource getSectionLabelFontSource() {
270        return this.sectionLabelFontSource; 
271    }
272    
273    /**
274     * Sets the font source and sends a {@link Plot3DChangeEvent} to all
275     * registered listeners.
276     * 
277     * @param source  the source ({@code null} not permitted). 
278     */
279    public void setSectionLabelFontSource(FontSource source) {
280        ArgChecks.nullNotPermitted(source, "source");
281        this.sectionLabelFontSource = source;
282        fireChangeEvent(false);
283    }
284
285    /**
286     * Returns the color source for section labels.  The default value is
287     * an instance of {@link StandardColorSource} that always returns
288     * {@code Color.BLACK}.
289     * 
290     * @return The color source (never {@code null}).
291     * 
292     * @see #setSectionLabelColorSource(ColorSource) 
293     */
294    public ColorSource getSectionLabelColorSource() {
295        return this.sectionLabelColorSource;
296    }
297    
298    /**
299     * Sets the color source for the section labels and sends a 
300     * {@link Plot3DChangeEvent} to all registered listeners.
301     * 
302     * @param source  the color source. 
303     * 
304     * @see #getSectionLabelColorSource() 
305     */
306    public void setSectionLabelColorSource(ColorSource source) {
307        ArgChecks.nullNotPermitted(source, "source");
308        this.sectionLabelColorSource = source;
309        fireChangeEvent(false);
310    }
311    
312    /**
313     * Returns the object that creates legend labels for each section of the pie
314     * chart.
315     * 
316     * @return The legend label generator (never {@code null}).
317     * 
318     * @since 1.2
319     */
320    public PieLabelGenerator getLegendLabelGenerator() {
321        return this.legendLabelGenerator;
322    }
323    
324    /**
325     * Sets the object that creates legend labels for each section of the pie 
326     * chart, and sends a {@link Plot3DChangeEvent} to all registered 
327     * listeners.
328     * 
329     * @param generator  the generator ({@code null} not permitted).
330     * 
331     * @since 1.2
332     */
333    public void setLegendLabelGenerator(PieLabelGenerator generator) {
334        ArgChecks.nullNotPermitted(generator, "generator");
335        this.legendLabelGenerator = generator;
336        fireChangeEvent(false);
337    }
338    
339    /**
340     * Returns the tool tip generator.
341     * 
342     * @return The tool tip generator (possibly {@code null}).
343     * 
344     * @since 1.3
345     */
346    public PieLabelGenerator getToolTipGenerator() {
347        return this.toolTipGenerator;
348    }
349    
350    /**
351     * Sets the tool tip generator and sends a change event to all registered
352     * listeners.
353     * 
354     * @param generator  the generator ({@code null} permitted).
355     * 
356     * @since 1.3
357     */
358    public void setToolTipGenerator(PieLabelGenerator generator) {
359        this.toolTipGenerator = generator;
360        fireChangeEvent(false);
361    }
362
363    /**
364     * Returns the dimensions for the plot.  For the pie chart, it is more 
365     * natural to specify the dimensions in terms of a radius and a depth, so
366     * we use those values to calculate the dimensions here.
367     * 
368     * @return The dimensions for the plot. 
369     */
370    @Override
371    public Dimension3D getDimensions() {
372        return new Dimension3D(this.radius * 2, this.depth, this.radius * 2);
373    }
374 
375    /**
376     * Returns the number of segments used when composing the 3D objects
377     * representing the pie chart.  The default value is {@code 40}.
378     * 
379     * @return The number of segments used to compose the pie chart. 
380     */
381    public int getSegmentCount() {
382        return this.segments;
383    }
384    
385    /**
386     * Sets the number of segments used when composing the pie chart and 
387     * sends a {@link Plot3DChangeEvent} to all registered listeners.  A higher
388     * number will result in a more rounded pie chart, but will take longer
389     * to render.
390     * 
391     * @param count  the count. 
392     */
393    public void setSegmentCount(int count) {
394        this.segments = count;
395        fireChangeEvent(true);
396    }
397    
398    /**
399     * Returns a list containing legend item info, typically one item for
400     * each series in the chart.  This is intended for use in the construction
401     * of a chart legend.
402     * 
403     * @return A list containing legend item info.
404     */
405    @Override @SuppressWarnings("unchecked")
406    public List<LegendItemInfo> getLegendInfo() {
407        List<LegendItemInfo> result = new ArrayList<LegendItemInfo>();
408        for (Comparable<?> key : (List<Comparable<?>>)
409                this.dataset.getKeys()) {
410            String label = this.legendLabelGenerator.generateLabel(dataset, 
411                    key);
412            LegendItemInfo info = new StandardLegendItemInfo(key, 
413                    label, this.sectionColorSource.getColor(key));
414            result.add(info);
415        }
416        return result;
417    }
418    
419    /**
420     * Adds 3D objects representing the current data for the plot to the 
421     * specified world.  After the world has been populated (or constructed) in
422     * this way, it is ready for rendering.  This method is called by the
423     * {@link Chart3D} class, you won't normally call it directly.
424     * 
425     * @param world  the world ({@code null} not permitted).
426     * @param xOffset  the x-offset.
427     * @param yOffset  the y-offset.
428     * @param zOffset  the z-offset.
429     */
430    @Override
431    @SuppressWarnings("unchecked")
432    public void compose(World world, double xOffset, double yOffset, 
433            double zOffset) {
434        double total = DataUtils.total(this.dataset);
435        double r = 0.0;
436        int count = this.dataset.getItemCount();
437        for (int i = 0; i < count; i++) {
438            Comparable<?> key = this.dataset.getKey(i);
439            Number n = (Number) this.dataset.getValue(i);
440            if (n != null) {
441                double angle = Math.PI * 2 * (n.doubleValue() / total);
442                Color c = this.sectionColorSource.getColor(
443                        this.dataset.getKey(i));
444                Object3D segment = Object3D.createPieSegment(this.radius, 0.0, 
445                        yOffset, this.depth, r, r + angle, 
446                        Math.PI / this.segments, c);
447                segment.setProperty(Object3D.ITEM_KEY, 
448                        new KeyedValuesItemKey(key));
449                world.add(segment);
450                r = r + angle;
451            }
452        }
453    }
454  
455    /**
456     * Returns a list of label faces for the plot.  These are non-visible 
457     * objects added to the 3D model of the pie chart to track the positions 
458     * for labels (which are added after the plot is projected and rendered).  
459     * <br><br>
460     * NOTE: This method is public so that it can be called by the 
461     * {@link Chart3D} class - you won't normally call it directly.
462     * 
463     * @param xOffset  the x-offset.
464     * @param yOffset  the y-offset.
465     * @param zOffset  the z-offset.
466     * 
467     * @return A list of label faces.
468     */
469    public List<Object3D> getLabelFaces(double xOffset, double yOffset, 
470            double zOffset) {
471        double total = DataUtils.total(this.dataset);
472        List<Object3D> result = new ArrayList<Object3D>();
473        // this adds the centre points
474        result.add(new Dot3D(0.0f, 0.0f, 0.0f, Color.RED));
475        result.add(new Dot3D(0.0f, (float) yOffset, 0.0f, Color.RED));
476        double r = 0.0;
477        int count = this.dataset.getItemCount();
478        for (int i = 0; i < count; i++) {
479            Number n = (Number) this.dataset.getValue(i);
480            double angle = 0.0;
481            if (n != null) {
482                angle = Math.PI * 2 * (n.doubleValue() / total);
483            }
484            result.addAll(Object3D.createPieLabelMarkers(this.radius * 1.2,
485                    0.0, yOffset - this.depth * 0.05, this.depth * 1.1, r, 
486                    r + angle));
487            r = r + angle;
488        }
489        return result;
490    }
491
492    @Override
493    public String generateToolTipText(ItemKey itemKey) {
494        if (!(itemKey instanceof KeyedValuesItemKey)) {
495            throw new IllegalArgumentException(
496                    "The itemKey must be a ValuesItemKey instance.");
497        }
498        KeyedValuesItemKey vik = (KeyedValuesItemKey) itemKey;
499        return this.toolTipGenerator.generateLabel(this.dataset, vik.getKey());
500    }
501
502    /**
503     * Receives a visitor.  This is a general purpose mechanism, but the main
504     * use is to apply chart style changes across all the elements of a 
505     * chart.
506     * 
507     * @param visitor  the visitor ({@code null} not permitted).
508     * 
509     * @since 1.2
510     */
511    @Override
512    public void receive(ChartElementVisitor visitor) { 
513        visitor.visit(this);
514    }
515
516    /**
517     * Tests this plot for equality with an arbitrary object.  Note that the
518     * plot's dataset is NOT considered in the equality test.
519     * 
520     * @param obj  the object ({@code null} not permitted).
521     * 
522     * @return A boolean. 
523     */
524    @Override
525    public boolean equals(Object obj) {
526        if (obj == this) {
527            return true;
528        }
529        if (!(obj instanceof PiePlot3D)) {
530            return false;
531        }
532        PiePlot3D that = (PiePlot3D) obj;
533        if (this.radius != that.radius) {
534            return false;
535        }
536        if (this.depth != that.depth) {
537            return false;
538        }
539        if (!this.sectionColorSource.equals(that.sectionColorSource)) {
540            return false;
541        }
542        if (!this.sectionLabelGenerator.equals(that.sectionLabelGenerator)) {
543            return false;
544        }
545        if (!this.sectionLabelFontSource.equals(that.sectionLabelFontSource)) {
546            return false;
547        }
548        if (!this.sectionLabelColorSource.equals(
549                that.sectionLabelColorSource)) {
550            return false;
551        }
552        if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) {
553            return false;
554        }
555        if (!this.toolTipGenerator.equals(that.toolTipGenerator)) {
556            return false;
557        }
558        if (this.segments != that.segments) {
559            return false;
560        }
561        return super.equals(obj);
562    }
563
564    @Override
565    public int hashCode() {
566        int hash = 5;
567        hash = 97 * hash + (int) (Double.doubleToLongBits(this.radius) 
568                ^ (Double.doubleToLongBits(this.radius) >>> 32));
569        hash = 97 * hash + (int) (Double.doubleToLongBits(this.depth) 
570                ^ (Double.doubleToLongBits(this.depth) >>> 32));
571        hash = 97 * hash + this.sectionColorSource.hashCode();
572        hash = 97 * hash + this.sectionLabelGenerator.hashCode();
573        hash = 97 * hash + this.sectionLabelFontSource.hashCode();
574        hash = 97 * hash + this.sectionLabelColorSource.hashCode();
575        hash = 97 * hash + this.segments;
576        return hash;
577    }
578
579}