001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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-2013, 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 *
051 */
052
053package org.jfree.chart.plot;
054
055import java.awt.BasicStroke;
056import java.awt.Color;
057import java.awt.Graphics2D;
058import java.awt.Paint;
059import java.awt.Shape;
060import java.awt.Stroke;
061import java.awt.geom.Arc2D;
062import java.awt.geom.GeneralPath;
063import java.awt.geom.Line2D;
064import java.awt.geom.Rectangle2D;
065import java.io.IOException;
066import java.io.ObjectInputStream;
067import java.io.ObjectOutputStream;
068import java.io.Serializable;
069
070import org.jfree.chart.entity.EntityCollection;
071import org.jfree.chart.entity.PieSectionEntity;
072import org.jfree.chart.event.PlotChangeEvent;
073import org.jfree.chart.labels.PieToolTipGenerator;
074import org.jfree.chart.urls.PieURLGenerator;
075import org.jfree.chart.util.ParamChecks;
076import org.jfree.data.general.PieDataset;
077import org.jfree.io.SerialUtilities;
078import org.jfree.ui.RectangleInsets;
079import org.jfree.util.ObjectUtilities;
080import org.jfree.util.PaintUtilities;
081import org.jfree.util.Rotation;
082import org.jfree.util.ShapeUtilities;
083import org.jfree.util.UnitType;
084
085/**
086 * A customised pie plot that leaves a hole in the middle.
087 */
088public class RingPlot extends PiePlot implements Cloneable, Serializable {
089
090    /** For serialization. */
091    private static final long serialVersionUID = 1556064784129676620L;
092
093    /**
094     * A flag that controls whether or not separators are drawn between the
095     * sections of the chart.
096     */
097    private boolean separatorsVisible;
098
099    /** The stroke used to draw separators. */
100    private transient Stroke separatorStroke;
101
102    /** The paint used to draw separators. */
103    private transient Paint separatorPaint;
104
105    /**
106     * The length of the inner separator extension (as a percentage of the
107     * depth of the sections).
108     */
109    private double innerSeparatorExtension;
110
111    /**
112     * The length of the outer separator extension (as a percentage of the
113     * depth of the sections).
114     */
115    private double outerSeparatorExtension;
116
117    /**
118     * The depth of the section as a percentage of the diameter.
119     */
120    private double sectionDepth;
121
122    /**
123     * Creates a new plot with a <code>null</code> dataset.
124     */
125    public RingPlot() {
126        this(null);
127    }
128
129    /**
130     * Creates a new plot for the specified dataset.
131     *
132     * @param dataset  the dataset (<code>null</code> permitted).
133     */
134    public RingPlot(PieDataset dataset) {
135        super(dataset);
136        this.separatorsVisible = true;
137        this.separatorStroke = new BasicStroke(0.5f);
138        this.separatorPaint = Color.gray;
139        this.innerSeparatorExtension = 0.20;  // twenty percent
140        this.outerSeparatorExtension = 0.20;  // twenty percent
141        this.sectionDepth = 0.20; // 20%
142    }
143
144    /**
145     * Returns a flag that indicates whether or not separators are drawn between
146     * the sections in the chart.
147     *
148     * @return A boolean.
149     *
150     * @see #setSeparatorsVisible(boolean)
151     */
152    public boolean getSeparatorsVisible() {
153        return this.separatorsVisible;
154    }
155
156    /**
157     * Sets the flag that controls whether or not separators are drawn between
158     * the sections in the chart, and sends a {@link PlotChangeEvent} to all
159     * registered listeners.
160     *
161     * @param visible  the flag.
162     *
163     * @see #getSeparatorsVisible()
164     */
165    public void setSeparatorsVisible(boolean visible) {
166        this.separatorsVisible = visible;
167        fireChangeEvent();
168    }
169
170    /**
171     * Returns the separator stroke.
172     *
173     * @return The stroke (never <code>null</code>).
174     *
175     * @see #setSeparatorStroke(Stroke)
176     */
177    public Stroke getSeparatorStroke() {
178        return this.separatorStroke;
179    }
180
181    /**
182     * Sets the stroke used to draw the separator between sections and sends
183     * a {@link PlotChangeEvent} to all registered listeners.
184     *
185     * @param stroke  the stroke (<code>null</code> not permitted).
186     *
187     * @see #getSeparatorStroke()
188     */
189    public void setSeparatorStroke(Stroke stroke) {
190        ParamChecks.nullNotPermitted(stroke, "stroke");
191        this.separatorStroke = stroke;
192        fireChangeEvent();
193    }
194
195    /**
196     * Returns the separator paint.
197     *
198     * @return The paint (never <code>null</code>).
199     *
200     * @see #setSeparatorPaint(Paint)
201     */
202    public Paint getSeparatorPaint() {
203        return this.separatorPaint;
204    }
205
206    /**
207     * Sets the paint used to draw the separator between sections and sends a
208     * {@link PlotChangeEvent} to all registered listeners.
209     *
210     * @param paint  the paint (<code>null</code> not permitted).
211     *
212     * @see #getSeparatorPaint()
213     */
214    public void setSeparatorPaint(Paint paint) {
215        ParamChecks.nullNotPermitted(paint, "paint");
216        this.separatorPaint = paint;
217        fireChangeEvent();
218    }
219
220    /**
221     * Returns the length of the inner extension of the separator line that
222     * is drawn between sections, expressed as a percentage of the depth of
223     * the section.
224     *
225     * @return The inner separator extension (as a percentage).
226     *
227     * @see #setInnerSeparatorExtension(double)
228     */
229    public double getInnerSeparatorExtension() {
230        return this.innerSeparatorExtension;
231    }
232
233    /**
234     * Sets the length of the inner extension of the separator line that is
235     * drawn between sections, as a percentage of the depth of the
236     * sections, and sends a {@link PlotChangeEvent} to all registered
237     * listeners.
238     *
239     * @param percent  the percentage.
240     *
241     * @see #getInnerSeparatorExtension()
242     * @see #setOuterSeparatorExtension(double)
243     */
244    public void setInnerSeparatorExtension(double percent) {
245        this.innerSeparatorExtension = percent;
246        fireChangeEvent();
247    }
248
249    /**
250     * Returns the length of the outer extension of the separator line that
251     * is drawn between sections, expressed as a percentage of the depth of
252     * the section.
253     *
254     * @return The outer separator extension (as a percentage).
255     *
256     * @see #setOuterSeparatorExtension(double)
257     */
258    public double getOuterSeparatorExtension() {
259        return this.outerSeparatorExtension;
260    }
261
262    /**
263     * Sets the length of the outer extension of the separator line that is
264     * drawn between sections, as a percentage of the depth of the
265     * sections, and sends a {@link PlotChangeEvent} to all registered
266     * listeners.
267     *
268     * @param percent  the percentage.
269     *
270     * @see #getOuterSeparatorExtension()
271     */
272    public void setOuterSeparatorExtension(double percent) {
273        this.outerSeparatorExtension = percent;
274        fireChangeEvent();
275    }
276
277    /**
278     * Returns the depth of each section, expressed as a percentage of the
279     * plot radius.
280     *
281     * @return The depth of each section.
282     *
283     * @see #setSectionDepth(double)
284     * @since 1.0.3
285     */
286    public double getSectionDepth() {
287        return this.sectionDepth;
288    }
289
290    /**
291     * The section depth is given as percentage of the plot radius.
292     * Specifying 1.0 results in a straightforward pie chart.
293     *
294     * @param sectionDepth  the section depth.
295     *
296     * @see #getSectionDepth()
297     * @since 1.0.3
298     */
299    public void setSectionDepth(double sectionDepth) {
300        this.sectionDepth = sectionDepth;
301        fireChangeEvent();
302    }
303
304    /**
305     * Initialises the plot state (which will store the total of all dataset
306     * values, among other things).  This method is called once at the
307     * beginning of each drawing.
308     *
309     * @param g2  the graphics device.
310     * @param plotArea  the plot area (<code>null</code> not permitted).
311     * @param plot  the plot.
312     * @param index  the secondary index (<code>null</code> for primary
313     *               renderer).
314     * @param info  collects chart rendering information for return to caller.
315     *
316     * @return A state object (maintains state information relevant to one
317     *         chart drawing).
318     */
319    @Override
320    public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
321            PiePlot plot, Integer index, PlotRenderingInfo info) {
322
323        PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
324        state.setPassesRequired(3);
325        return state;
326
327    }
328
329    /**
330     * Draws a single data item.
331     *
332     * @param g2  the graphics device (<code>null</code> not permitted).
333     * @param section  the section index.
334     * @param dataArea  the data plot area.
335     * @param state  state information for one chart.
336     * @param currentPass  the current pass index.
337     */
338    @Override
339    protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea,
340            PiePlotState state, int currentPass) {
341
342        PieDataset dataset = getDataset();
343        Number n = dataset.getValue(section);
344        if (n == null) {
345            return;
346        }
347        double value = n.doubleValue();
348        double angle1 = 0.0;
349        double angle2 = 0.0;
350
351        Rotation direction = getDirection();
352        if (direction == Rotation.CLOCKWISE) {
353            angle1 = state.getLatestAngle();
354            angle2 = angle1 - value / state.getTotal() * 360.0;
355        }
356        else if (direction == Rotation.ANTICLOCKWISE) {
357            angle1 = state.getLatestAngle();
358            angle2 = angle1 + value / state.getTotal() * 360.0;
359        }
360        else {
361            throw new IllegalStateException("Rotation type not recognised.");
362        }
363
364        double angle = (angle2 - angle1);
365        if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
366            Comparable key = getSectionKey(section);
367            double ep = 0.0;
368            double mep = getMaximumExplodePercent();
369            if (mep > 0.0) {
370                ep = getExplodePercent(key) / mep;
371            }
372            Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
373                    state.getExplodedPieArea(), angle1, angle, ep);
374            Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
375                    Arc2D.OPEN);
376
377            // create the bounds for the inner arc
378            double depth = this.sectionDepth / 2.0;
379            RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
380                depth, depth, depth, depth);
381            Rectangle2D innerArcBounds = new Rectangle2D.Double();
382            innerArcBounds.setRect(arcBounds);
383            s.trim(innerArcBounds);
384            // calculate inner arc in reverse direction, for later
385            // GeneralPath construction
386            Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
387                    + angle, -angle, Arc2D.OPEN);
388            GeneralPath path = new GeneralPath();
389            path.moveTo((float) arc.getStartPoint().getX(),
390                    (float) arc.getStartPoint().getY());
391            path.append(arc.getPathIterator(null), false);
392            path.append(arc2.getPathIterator(null), true);
393            path.closePath();
394
395            Line2D separator = new Line2D.Double(arc2.getEndPoint(),
396                    arc.getStartPoint());
397
398            if (currentPass == 0) {
399                Paint shadowPaint = getShadowPaint();
400                double shadowXOffset = getShadowXOffset();
401                double shadowYOffset = getShadowYOffset();
402                if (shadowPaint != null && getShadowGenerator() == null) {
403                    Shape shadowArc = ShapeUtilities.createTranslatedShape(
404                            path, (float) shadowXOffset, (float) shadowYOffset);
405                    g2.setPaint(shadowPaint);
406                    g2.fill(shadowArc);
407                }
408            }
409            else if (currentPass == 1) {
410                Paint paint = lookupSectionPaint(key);
411                g2.setPaint(paint);
412                g2.fill(path);
413                Paint outlinePaint = lookupSectionOutlinePaint(key);
414                Stroke outlineStroke = lookupSectionOutlineStroke(key);
415                if (getSectionOutlinesVisible() && outlinePaint != null 
416                        && outlineStroke != null) {
417                    g2.setPaint(outlinePaint);
418                    g2.setStroke(outlineStroke);
419                    g2.draw(path);
420                }
421
422                // add an entity for the pie section
423                if (state.getInfo() != null) {
424                    EntityCollection entities = state.getEntityCollection();
425                    if (entities != null) {
426                        String tip = null;
427                        PieToolTipGenerator toolTipGenerator
428                                = getToolTipGenerator();
429                        if (toolTipGenerator != null) {
430                            tip = toolTipGenerator.generateToolTip(dataset,
431                                    key);
432                        }
433                        String url = null;
434                        PieURLGenerator urlGenerator = getURLGenerator();
435                        if (urlGenerator != null) {
436                            url = urlGenerator.generateURL(dataset, key,
437                                    getPieIndex());
438                        }
439                        PieSectionEntity entity = new PieSectionEntity(path,
440                                dataset, getPieIndex(), section, key, tip,
441                                url);
442                        entities.add(entity);
443                    }
444                }
445            }
446            else if (currentPass == 2) {
447                if (this.separatorsVisible) {
448                    Line2D extendedSeparator = extendLine(separator,
449                        this.innerSeparatorExtension,
450                        this.outerSeparatorExtension);
451                    g2.setStroke(this.separatorStroke);
452                    g2.setPaint(this.separatorPaint);
453                    g2.draw(extendedSeparator);
454                }
455            }
456        }
457        state.setLatestAngle(angle2);
458    }
459
460    /**
461     * This method overrides the default value for cases where the ring plot
462     * is very thin.  This fixes bug 2121818.
463     *
464     * @return The label link depth, as a percentage of the plot's radius.
465     */
466    @Override
467    protected double getLabelLinkDepth() {
468        return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
469    }
470
471    /**
472     * Tests this plot for equality with an arbitrary object.
473     *
474     * @param obj  the object to test against (<code>null</code> permitted).
475     *
476     * @return A boolean.
477     */
478    @Override
479    public boolean equals(Object obj) {
480        if (this == obj) {
481            return true;
482        }
483        if (!(obj instanceof RingPlot)) {
484            return false;
485        }
486        RingPlot that = (RingPlot) obj;
487        if (this.separatorsVisible != that.separatorsVisible) {
488            return false;
489        }
490        if (!ObjectUtilities.equal(this.separatorStroke,
491                that.separatorStroke)) {
492            return false;
493        }
494        if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
495            return false;
496        }
497        if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
498            return false;
499        }
500        if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
501            return false;
502        }
503        if (this.sectionDepth != that.sectionDepth) {
504            return false;
505        }
506        return super.equals(obj);
507    }
508
509    /**
510     * Creates a new line by extending an existing line.
511     *
512     * @param line  the line (<code>null</code> not permitted).
513     * @param startPercent  the amount to extend the line at the start point
514     *                      end.
515     * @param endPercent  the amount to extend the line at the end point end.
516     *
517     * @return A new line.
518     */
519    private Line2D extendLine(Line2D line, double startPercent,
520                              double endPercent) {
521        ParamChecks.nullNotPermitted(line, "line");
522        double x1 = line.getX1();
523        double x2 = line.getX2();
524        double deltaX = x2 - x1;
525        double y1 = line.getY1();
526        double y2 = line.getY2();
527        double deltaY = y2 - y1;
528        x1 = x1 - (startPercent * deltaX);
529        y1 = y1 - (startPercent * deltaY);
530        x2 = x2 + (endPercent * deltaX);
531        y2 = y2 + (endPercent * deltaY);
532        return new Line2D.Double(x1, y1, x2, y2);
533    }
534
535    /**
536     * Provides serialization support.
537     *
538     * @param stream  the output stream.
539     *
540     * @throws IOException  if there is an I/O error.
541     */
542    private void writeObject(ObjectOutputStream stream) throws IOException {
543        stream.defaultWriteObject();
544        SerialUtilities.writeStroke(this.separatorStroke, stream);
545        SerialUtilities.writePaint(this.separatorPaint, stream);
546    }
547
548    /**
549     * Provides serialization support.
550     *
551     * @param stream  the input stream.
552     *
553     * @throws IOException  if there is an I/O error.
554     * @throws ClassNotFoundException  if there is a classpath problem.
555     */
556    private void readObject(ObjectInputStream stream)
557        throws IOException, ClassNotFoundException {
558        stream.defaultReadObject();
559        this.separatorStroke = SerialUtilities.readStroke(stream);
560        this.separatorPaint = SerialUtilities.readPaint(stream);
561    }
562
563}