001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -------------
028 * RingPlot.java
029 * -------------
030 * (C) Copyright 2004-2014, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limtied);
033 * Contributor(s):   Christoph Beck (bug 2121818);
034 *
035 * Changes
036 * -------
037 * 08-Nov-2004 : Version 1 (DG);
038 * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
039 * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
040 *               GradientPaint (DG);
041 * ------------- JFREECHART 1.0.x ---------------------------------------------
042 * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
043 * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
044 * 12-Oct-2006 : Added configurable section depth (DG);
045 * 14-Feb-2007 : Added notification in setSectionDepth() method (DG);
046 * 23-Sep-2008 : Fix for bug 2121818 by Christoph Beck (DG);
047 * 13-Jul-2009 : Added support for shadow generator (DG);
048 * 11-Oct-2011 : Check sectionOutlineVisible - bug 3237879 (DG);
049 * 02-Jul-2013 : Use ParamChecks (DG);
050 * 28-Feb-2014 : Add center text feature (DG);
051 *
052 */
053
054package org.jfree.chart.plot;
055
056import java.awt.BasicStroke;
057import java.awt.Color;
058import java.awt.Font;
059import java.awt.Graphics2D;
060import java.awt.Paint;
061import java.awt.Shape;
062import java.awt.Stroke;
063import java.awt.geom.Arc2D;
064import java.awt.geom.GeneralPath;
065import java.awt.geom.Line2D;
066import java.awt.geom.Rectangle2D;
067import java.io.IOException;
068import java.io.ObjectInputStream;
069import java.io.ObjectOutputStream;
070import java.io.Serializable;
071import java.text.DecimalFormat;
072import java.text.Format;
073
074import org.jfree.chart.entity.EntityCollection;
075import org.jfree.chart.entity.PieSectionEntity;
076import org.jfree.chart.labels.PieToolTipGenerator;
077import org.jfree.chart.urls.PieURLGenerator;
078import org.jfree.chart.util.LineUtilities;
079import org.jfree.chart.util.ParamChecks;
080import org.jfree.data.general.PieDataset;
081import org.jfree.io.SerialUtilities;
082import org.jfree.text.TextUtilities;
083import org.jfree.ui.RectangleInsets;
084import org.jfree.ui.TextAnchor;
085import org.jfree.util.ObjectUtilities;
086import org.jfree.util.PaintUtilities;
087import org.jfree.util.Rotation;
088import org.jfree.util.ShapeUtilities;
089import org.jfree.util.UnitType;
090
091/**
092 * A customised pie plot that leaves a hole in the middle.
093 */
094public class RingPlot extends PiePlot implements Cloneable, Serializable {
095
096    /** For serialization. */
097    private static final long serialVersionUID = 1556064784129676620L;
098
099    /** The center text mode. */
100    private CenterTextMode centerTextMode = CenterTextMode.NONE;
101    
102    /** 
103     * Text to display in the middle of the chart (used for 
104     * CenterTextMode.FIXED). 
105     */
106    private String centerText;
107    
108    /**
109     * The formatter used when displaying the first data value from the
110     * dataset (CenterTextMode.VALUE).
111     */
112    private Format centerTextFormatter = new DecimalFormat("0.00");
113    
114    /** The font used to display the center text. */
115    private Font centerTextFont;
116    
117    /** The color used to display the center text. */
118    private Color centerTextColor;
119    
120    /**
121     * A flag that controls whether or not separators are drawn between the
122     * sections of the chart.
123     */
124    private boolean separatorsVisible;
125
126    /** The stroke used to draw separators. */
127    private transient Stroke separatorStroke;
128
129    /** The paint used to draw separators. */
130    private transient Paint separatorPaint;
131
132    /**
133     * The length of the inner separator extension (as a percentage of the
134     * depth of the sections).
135     */
136    private double innerSeparatorExtension;
137
138    /**
139     * The length of the outer separator extension (as a percentage of the
140     * depth of the sections).
141     */
142    private double outerSeparatorExtension;
143
144    /**
145     * The depth of the section as a percentage of the diameter.
146     */
147    private double sectionDepth;
148
149    /**
150     * Creates a new plot with a <code>null</code> dataset.
151     */
152    public RingPlot() {
153        this(null);
154    }
155
156    /**
157     * Creates a new plot for the specified dataset.
158     *
159     * @param dataset  the dataset (<code>null</code> permitted).
160     */
161    public RingPlot(PieDataset dataset) {
162        super(dataset);
163        this.centerTextMode = CenterTextMode.NONE;
164        this.centerText = null;
165        this.centerTextFormatter = new DecimalFormat("0.00");
166        this.centerTextFont = DEFAULT_LABEL_FONT;
167        this.centerTextColor = Color.BLACK;
168        this.separatorsVisible = true;
169        this.separatorStroke = new BasicStroke(0.5f);
170        this.separatorPaint = Color.gray;
171        this.innerSeparatorExtension = 0.20;  // 20%
172        this.outerSeparatorExtension = 0.20;  // 20%
173        this.sectionDepth = 0.20; // 20%
174    }
175
176    /**
177     * Returns the mode for displaying text in the center of the plot.  The
178     * default value is {@link CenterTextMode#NONE} therefore no text
179     * will be displayed by default.
180     * 
181     * @return The mode (never <code>null</code>).
182     * 
183     * @since 1.0.18
184     */
185    public CenterTextMode getCenterTextMode() {
186        return this.centerTextMode;
187    }
188    
189    /**
190     * Sets the mode for displaying text in the center of the plot and sends 
191     * a change event to all registered listeners.  For
192     * {@link CenterTextMode#FIXED}, the display text will come from the 
193     * <code>centerText</code> attribute (see {@link #getCenterText()}).
194     * For {@link CenterTextMode#VALUE}, the center text will be the value from
195     * the first section in the dataset.
196     * 
197     * @param mode  the mode (<code>null</code> not permitted).
198     * 
199     * @since 1.0.18
200     */
201    public void setCenterTextMode(CenterTextMode mode) {
202        ParamChecks.nullNotPermitted(mode, "mode");
203        this.centerTextMode = mode;
204        fireChangeEvent();
205    }
206    
207    /**
208     * Returns the text to display in the center of the plot when the mode
209     * is {@link CenterTextMode#FIXED}.
210     * 
211     * @return The text (possibly <code>null</code>).
212     * 
213     * @since 1.0.18.
214     */
215    public String getCenterText() {
216        return this.centerText;
217    }
218    
219    /**
220     * Sets the text to display in the center of the plot and sends a
221     * change event to all registered listeners.  If the text is set to 
222     * <code>null</code>, no text will be displayed.
223     * 
224     * @param text  the text (<code>null</code> permitted).
225     * 
226     * @since 1.0.18
227     */
228    public void setCenterText(String text) {
229        this.centerText = text;
230        fireChangeEvent();
231    }
232    
233    /**
234     * Returns the formatter used to format the center text value for the mode
235     * {@link CenterTextMode#VALUE}.  The default value is 
236     * <code>DecimalFormat("0.00");</code>.
237     * 
238     * @return The formatter (never <code>null</code>).
239     * 
240     * @since 1.0.18
241     */
242    public Format getCenterTextFormatter() {
243        return this.centerTextFormatter;
244    }
245    
246    /**
247     * Sets the formatter used to format the center text value and sends a
248     * change event to all registered listeners.
249     * 
250     * @param formatter  the formatter (<code>null</code> not permitted).
251     * 
252     * @since 1.0.18
253     */
254    public void setCenterTextFormatter(Format formatter) {
255        ParamChecks.nullNotPermitted(formatter, "formatter");
256        this.centerTextFormatter = formatter;
257    }
258    
259    /**
260     * Returns the font used to display the center text.  The default value
261     * is {@link PiePlot#DEFAULT_LABEL_FONT}.
262     * 
263     * @return The font (never <code>null</code>).
264     * 
265     * @since 1.0.18
266     */
267    public Font getCenterTextFont() {
268        return this.centerTextFont;
269    }
270    
271    /**
272     * Sets the font used to display the center text and sends a change event
273     * to all registered listeners.
274     * 
275     * @param font  the font (<code>null</code> not permitted).
276     * 
277     * @since 1.0.18
278     */
279    public void setCenterTextFont(Font font) {
280        ParamChecks.nullNotPermitted(font, "font");
281        this.centerTextFont = font;
282        fireChangeEvent();
283    }
284    
285    /**
286     * Returns the color for the center text.  The default value is
287     * <code>Color.BLACK</code>.
288     * 
289     * @return The color (never <code>null</code>). 
290     * 
291     * @since 1.0.18
292     */
293    public Color getCenterTextColor() {
294        return this.centerTextColor;
295    }
296    
297    /**
298     * Sets the color for the center text and sends a change event to all 
299     * registered listeners.
300     * 
301     * @param color  the color (<code>null</code> not permitted).
302     * 
303     * @since 1.0.18
304     */
305    public void setCenterTextColor(Color color) {
306        ParamChecks.nullNotPermitted(color, "color");
307        this.centerTextColor = color;
308        fireChangeEvent();
309    }
310    
311    /**
312     * Returns a flag that indicates whether or not separators are drawn between
313     * the sections in the chart.
314     *
315     * @return A boolean.
316     *
317     * @see #setSeparatorsVisible(boolean)
318     */
319    public boolean getSeparatorsVisible() {
320        return this.separatorsVisible;
321    }
322
323    /**
324     * Sets the flag that controls whether or not separators are drawn between
325     * the sections in the chart, and sends a change event to all registered 
326     * listeners.
327     *
328     * @param visible  the flag.
329     *
330     * @see #getSeparatorsVisible()
331     */
332    public void setSeparatorsVisible(boolean visible) {
333        this.separatorsVisible = visible;
334        fireChangeEvent();
335    }
336
337    /**
338     * Returns the separator stroke.
339     *
340     * @return The stroke (never <code>null</code>).
341     *
342     * @see #setSeparatorStroke(Stroke)
343     */
344    public Stroke getSeparatorStroke() {
345        return this.separatorStroke;
346    }
347
348    /**
349     * Sets the stroke used to draw the separator between sections and sends
350     * a change event to all registered listeners.
351     *
352     * @param stroke  the stroke (<code>null</code> not permitted).
353     *
354     * @see #getSeparatorStroke()
355     */
356    public void setSeparatorStroke(Stroke stroke) {
357        ParamChecks.nullNotPermitted(stroke, "stroke");
358        this.separatorStroke = stroke;
359        fireChangeEvent();
360    }
361
362    /**
363     * Returns the separator paint.
364     *
365     * @return The paint (never <code>null</code>).
366     *
367     * @see #setSeparatorPaint(Paint)
368     */
369    public Paint getSeparatorPaint() {
370        return this.separatorPaint;
371    }
372
373    /**
374     * Sets the paint used to draw the separator between sections and sends a
375     * change event to all registered listeners.
376     *
377     * @param paint  the paint (<code>null</code> not permitted).
378     *
379     * @see #getSeparatorPaint()
380     */
381    public void setSeparatorPaint(Paint paint) {
382        ParamChecks.nullNotPermitted(paint, "paint");
383        this.separatorPaint = paint;
384        fireChangeEvent();
385    }
386
387    /**
388     * Returns the length of the inner extension of the separator line that
389     * is drawn between sections, expressed as a percentage of the depth of
390     * the section.
391     *
392     * @return The inner separator extension (as a percentage).
393     *
394     * @see #setInnerSeparatorExtension(double)
395     */
396    public double getInnerSeparatorExtension() {
397        return this.innerSeparatorExtension;
398    }
399
400    /**
401     * Sets the length of the inner extension of the separator line that is
402     * drawn between sections, as a percentage of the depth of the
403     * sections, and sends a change event to all registered listeners.
404     *
405     * @param percent  the percentage.
406     *
407     * @see #getInnerSeparatorExtension()
408     * @see #setOuterSeparatorExtension(double)
409     */
410    public void setInnerSeparatorExtension(double percent) {
411        this.innerSeparatorExtension = percent;
412        fireChangeEvent();
413    }
414
415    /**
416     * Returns the length of the outer extension of the separator line that
417     * is drawn between sections, expressed as a percentage of the depth of
418     * the section.
419     *
420     * @return The outer separator extension (as a percentage).
421     *
422     * @see #setOuterSeparatorExtension(double)
423     */
424    public double getOuterSeparatorExtension() {
425        return this.outerSeparatorExtension;
426    }
427
428    /**
429     * Sets the length of the outer extension of the separator line that is
430     * drawn between sections, as a percentage of the depth of the
431     * sections, and sends a change event to all registered listeners.
432     *
433     * @param percent  the percentage.
434     *
435     * @see #getOuterSeparatorExtension()
436     */
437    public void setOuterSeparatorExtension(double percent) {
438        this.outerSeparatorExtension = percent;
439        fireChangeEvent();
440    }
441
442    /**
443     * Returns the depth of each section, expressed as a percentage of the
444     * plot radius.
445     *
446     * @return The depth of each section.
447     *
448     * @see #setSectionDepth(double)
449     * @since 1.0.3
450     */
451    public double getSectionDepth() {
452        return this.sectionDepth;
453    }
454
455    /**
456     * The section depth is given as percentage of the plot radius.
457     * Specifying 1.0 results in a straightforward pie chart.
458     *
459     * @param sectionDepth  the section depth.
460     *
461     * @see #getSectionDepth()
462     * @since 1.0.3
463     */
464    public void setSectionDepth(double sectionDepth) {
465        this.sectionDepth = sectionDepth;
466        fireChangeEvent();
467    }
468
469    /**
470     * Initialises the plot state (which will store the total of all dataset
471     * values, among other things).  This method is called once at the
472     * beginning of each drawing.
473     *
474     * @param g2  the graphics device.
475     * @param plotArea  the plot area (<code>null</code> not permitted).
476     * @param plot  the plot.
477     * @param index  the secondary index (<code>null</code> for primary
478     *               renderer).
479     * @param info  collects chart rendering information for return to caller.
480     *
481     * @return A state object (maintains state information relevant to one
482     *         chart drawing).
483     */
484    @Override
485    public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
486            PiePlot plot, Integer index, PlotRenderingInfo info) {
487        PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
488        state.setPassesRequired(3);
489        return state;
490    }
491
492    /**
493     * Draws a single data item.
494     *
495     * @param g2  the graphics device (<code>null</code> not permitted).
496     * @param section  the section index.
497     * @param dataArea  the data plot area.
498     * @param state  state information for one chart.
499     * @param currentPass  the current pass index.
500     */
501    @Override
502    protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea,
503            PiePlotState state, int currentPass) {
504
505        PieDataset dataset = getDataset();
506        Number n = dataset.getValue(section);
507        if (n == null) {
508            return;
509        }
510        double value = n.doubleValue();
511        double angle1 = 0.0;
512        double angle2 = 0.0;
513
514        Rotation direction = getDirection();
515        if (direction == Rotation.CLOCKWISE) {
516            angle1 = state.getLatestAngle();
517            angle2 = angle1 - value / state.getTotal() * 360.0;
518        }
519        else if (direction == Rotation.ANTICLOCKWISE) {
520            angle1 = state.getLatestAngle();
521            angle2 = angle1 + value / state.getTotal() * 360.0;
522        }
523        else {
524            throw new IllegalStateException("Rotation type not recognised.");
525        }
526
527        double angle = (angle2 - angle1);
528        if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
529            Comparable key = getSectionKey(section);
530            double ep = 0.0;
531            double mep = getMaximumExplodePercent();
532            if (mep > 0.0) {
533                ep = getExplodePercent(key) / mep;
534            }
535            Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
536                    state.getExplodedPieArea(), angle1, angle, ep);
537            Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
538                    Arc2D.OPEN);
539
540            // create the bounds for the inner arc
541            double depth = this.sectionDepth / 2.0;
542            RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
543                depth, depth, depth, depth);
544            Rectangle2D innerArcBounds = new Rectangle2D.Double();
545            innerArcBounds.setRect(arcBounds);
546            s.trim(innerArcBounds);
547            // calculate inner arc in reverse direction, for later
548            // GeneralPath construction
549            Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
550                    + angle, -angle, Arc2D.OPEN);
551            GeneralPath path = new GeneralPath();
552            path.moveTo((float) arc.getStartPoint().getX(),
553                    (float) arc.getStartPoint().getY());
554            path.append(arc.getPathIterator(null), false);
555            path.append(arc2.getPathIterator(null), true);
556            path.closePath();
557
558            Line2D separator = new Line2D.Double(arc2.getEndPoint(),
559                    arc.getStartPoint());
560
561            if (currentPass == 0) {
562                Paint shadowPaint = getShadowPaint();
563                double shadowXOffset = getShadowXOffset();
564                double shadowYOffset = getShadowYOffset();
565                if (shadowPaint != null && getShadowGenerator() == null) {
566                    Shape shadowArc = ShapeUtilities.createTranslatedShape(
567                            path, (float) shadowXOffset, (float) shadowYOffset);
568                    g2.setPaint(shadowPaint);
569                    g2.fill(shadowArc);
570                }
571            }
572            else if (currentPass == 1) {
573                Paint paint = lookupSectionPaint(key);
574                g2.setPaint(paint);
575                g2.fill(path);
576                Paint outlinePaint = lookupSectionOutlinePaint(key);
577                Stroke outlineStroke = lookupSectionOutlineStroke(key);
578                if (getSectionOutlinesVisible() && outlinePaint != null 
579                        && outlineStroke != null) {
580                    g2.setPaint(outlinePaint);
581                    g2.setStroke(outlineStroke);
582                    g2.draw(path);
583                }
584                
585                if (section == 0) {
586                    String nstr = null;
587                    if (this.centerTextMode.equals(CenterTextMode.VALUE)) {
588                        nstr = this.centerTextFormatter.format(n);
589                    } else if (this.centerTextMode.equals(CenterTextMode.FIXED)) {
590                        nstr = this.centerText;
591                    }
592                    if (nstr != null) {
593                        g2.setFont(this.centerTextFont);
594                        g2.setPaint(this.centerTextColor);
595                        TextUtilities.drawAlignedString(nstr, g2, 
596                            (float) dataArea.getCenterX(), 
597                            (float) dataArea.getCenterY(),  
598                            TextAnchor.CENTER);                        
599                    }
600                }
601
602                // add an entity for the pie section
603                if (state.getInfo() != null) {
604                    EntityCollection entities = state.getEntityCollection();
605                    if (entities != null) {
606                        String tip = null;
607                        PieToolTipGenerator toolTipGenerator
608                                = getToolTipGenerator();
609                        if (toolTipGenerator != null) {
610                            tip = toolTipGenerator.generateToolTip(dataset,
611                                    key);
612                        }
613                        String url = null;
614                        PieURLGenerator urlGenerator = getURLGenerator();
615                        if (urlGenerator != null) {
616                            url = urlGenerator.generateURL(dataset, key,
617                                    getPieIndex());
618                        }
619                        PieSectionEntity entity = new PieSectionEntity(path,
620                                dataset, getPieIndex(), section, key, tip,
621                                url);
622                        entities.add(entity);
623                    }
624                }
625            }
626            else if (currentPass == 2) {
627                if (this.separatorsVisible) {
628                    Line2D extendedSeparator = LineUtilities.extendLine(
629                            separator, this.innerSeparatorExtension,
630                            this.outerSeparatorExtension);
631                    g2.setStroke(this.separatorStroke);
632                    g2.setPaint(this.separatorPaint);
633                    g2.draw(extendedSeparator);
634                }
635            }
636        }
637        state.setLatestAngle(angle2);
638    }
639
640    /**
641     * This method overrides the default value for cases where the ring plot
642     * is very thin.  This fixes bug 2121818.
643     *
644     * @return The label link depth, as a percentage of the plot's radius.
645     */
646    @Override
647    protected double getLabelLinkDepth() {
648        return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
649    }
650
651    /**
652     * Tests this plot for equality with an arbitrary object.
653     *
654     * @param obj  the object to test against (<code>null</code> permitted).
655     *
656     * @return A boolean.
657     */
658    @Override
659    public boolean equals(Object obj) {
660        if (this == obj) {
661            return true;
662        }
663        if (!(obj instanceof RingPlot)) {
664            return false;
665        }
666        RingPlot that = (RingPlot) obj;
667        if (!this.centerTextMode.equals(that.centerTextMode)) {
668            return false;
669        }
670        if (!ObjectUtilities.equal(this.centerText, that.centerText)) {
671            return false;
672        }
673        if (!this.centerTextFormatter.equals(that.centerTextFormatter)) {
674            return false;
675        }
676        if (!this.centerTextFont.equals(that.centerTextFont)) {
677            return false;
678        }
679        if (!this.centerTextColor.equals(that.centerTextColor)) {
680            return false;
681        }
682        if (this.separatorsVisible != that.separatorsVisible) {
683            return false;
684        }
685        if (!ObjectUtilities.equal(this.separatorStroke,
686                that.separatorStroke)) {
687            return false;
688        }
689        if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
690            return false;
691        }
692        if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
693            return false;
694        }
695        if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
696            return false;
697        }
698        if (this.sectionDepth != that.sectionDepth) {
699            return false;
700        }
701        return super.equals(obj);
702    }
703
704    /**
705     * Provides serialization support.
706     *
707     * @param stream  the output stream.
708     *
709     * @throws IOException  if there is an I/O error.
710     */
711    private void writeObject(ObjectOutputStream stream) throws IOException {
712        stream.defaultWriteObject();
713        SerialUtilities.writeStroke(this.separatorStroke, stream);
714        SerialUtilities.writePaint(this.separatorPaint, stream);
715    }
716
717    /**
718     * Provides serialization support.
719     *
720     * @param stream  the input stream.
721     *
722     * @throws IOException  if there is an I/O error.
723     * @throws ClassNotFoundException  if there is a classpath problem.
724     */
725    private void readObject(ObjectInputStream stream)
726        throws IOException, ClassNotFoundException {
727        stream.defaultReadObject();
728        this.separatorStroke = SerialUtilities.readStroke(stream);
729        this.separatorPaint = SerialUtilities.readPaint(stream);
730    }
731
732}