001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * -------------
028     * RingPlot.java
029     * -------------
030     * (C) Copyright 2004-2008, 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     *
048     */
049    
050    package org.jfree.chart.plot;
051    
052    import java.awt.BasicStroke;
053    import java.awt.Color;
054    import java.awt.Graphics2D;
055    import java.awt.Paint;
056    import java.awt.Shape;
057    import java.awt.Stroke;
058    import java.awt.geom.Arc2D;
059    import java.awt.geom.GeneralPath;
060    import java.awt.geom.Line2D;
061    import java.awt.geom.Rectangle2D;
062    import java.io.IOException;
063    import java.io.ObjectInputStream;
064    import java.io.ObjectOutputStream;
065    import java.io.Serializable;
066    
067    import org.jfree.chart.entity.EntityCollection;
068    import org.jfree.chart.entity.PieSectionEntity;
069    import org.jfree.chart.event.PlotChangeEvent;
070    import org.jfree.chart.labels.PieToolTipGenerator;
071    import org.jfree.chart.urls.PieURLGenerator;
072    import org.jfree.data.general.PieDataset;
073    import org.jfree.io.SerialUtilities;
074    import org.jfree.ui.RectangleInsets;
075    import org.jfree.util.ObjectUtilities;
076    import org.jfree.util.PaintUtilities;
077    import org.jfree.util.Rotation;
078    import org.jfree.util.ShapeUtilities;
079    import org.jfree.util.UnitType;
080    
081    /**
082     * A customised pie plot that leaves a hole in the middle.
083     */
084    public class RingPlot extends PiePlot implements Cloneable, Serializable {
085    
086        /** For serialization. */
087        private static final long serialVersionUID = 1556064784129676620L;
088    
089        /**
090         * A flag that controls whether or not separators are drawn between the
091         * sections of the chart.
092         */
093        private boolean separatorsVisible;
094    
095        /** The stroke used to draw separators. */
096        private transient Stroke separatorStroke;
097    
098        /** The paint used to draw separators. */
099        private transient Paint separatorPaint;
100    
101        /**
102         * The length of the inner separator extension (as a percentage of the
103         * depth of the sections).
104         */
105        private double innerSeparatorExtension;
106    
107        /**
108         * The length of the outer separator extension (as a percentage of the
109         * depth of the sections).
110         */
111        private double outerSeparatorExtension;
112    
113        /**
114         * The depth of the section as a percentage of the diameter.
115         */
116        private double sectionDepth;
117    
118        /**
119         * Creates a new plot with a <code>null</code> dataset.
120         */
121        public RingPlot() {
122            this(null);
123        }
124    
125        /**
126         * Creates a new plot for the specified dataset.
127         *
128         * @param dataset  the dataset (<code>null</code> permitted).
129         */
130        public RingPlot(PieDataset dataset) {
131            super(dataset);
132            this.separatorsVisible = true;
133            this.separatorStroke = new BasicStroke(0.5f);
134            this.separatorPaint = Color.gray;
135            this.innerSeparatorExtension = 0.20;  // twenty percent
136            this.outerSeparatorExtension = 0.20;  // twenty percent
137            this.sectionDepth = 0.20; // 20%
138        }
139    
140        /**
141         * Returns a flag that indicates whether or not separators are drawn between
142         * the sections in the chart.
143         *
144         * @return A boolean.
145         *
146         * @see #setSeparatorsVisible(boolean)
147         */
148        public boolean getSeparatorsVisible() {
149            return this.separatorsVisible;
150        }
151    
152        /**
153         * Sets the flag that controls whether or not separators are drawn between
154         * the sections in the chart, and sends a {@link PlotChangeEvent} to all
155         * registered listeners.
156         *
157         * @param visible  the flag.
158         *
159         * @see #getSeparatorsVisible()
160         */
161        public void setSeparatorsVisible(boolean visible) {
162            this.separatorsVisible = visible;
163            fireChangeEvent();
164        }
165    
166        /**
167         * Returns the separator stroke.
168         *
169         * @return The stroke (never <code>null</code>).
170         *
171         * @see #setSeparatorStroke(Stroke)
172         */
173        public Stroke getSeparatorStroke() {
174            return this.separatorStroke;
175        }
176    
177        /**
178         * Sets the stroke used to draw the separator between sections and sends
179         * a {@link PlotChangeEvent} to all registered listeners.
180         *
181         * @param stroke  the stroke (<code>null</code> not permitted).
182         *
183         * @see #getSeparatorStroke()
184         */
185        public void setSeparatorStroke(Stroke stroke) {
186            if (stroke == null) {
187                throw new IllegalArgumentException("Null 'stroke' argument.");
188            }
189            this.separatorStroke = stroke;
190            fireChangeEvent();
191        }
192    
193        /**
194         * Returns the separator paint.
195         *
196         * @return The paint (never <code>null</code>).
197         *
198         * @see #setSeparatorPaint(Paint)
199         */
200        public Paint getSeparatorPaint() {
201            return this.separatorPaint;
202        }
203    
204        /**
205         * Sets the paint used to draw the separator between sections and sends a
206         * {@link PlotChangeEvent} to all registered listeners.
207         *
208         * @param paint  the paint (<code>null</code> not permitted).
209         *
210         * @see #getSeparatorPaint()
211         */
212        public void setSeparatorPaint(Paint paint) {
213            if (paint == null) {
214                throw new IllegalArgumentException("Null 'paint' argument.");
215            }
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        public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea,
320                PiePlot plot, Integer index, PlotRenderingInfo info) {
321    
322            PiePlotState state = super.initialise(g2, plotArea, plot, index, info);
323            state.setPassesRequired(3);
324            return state;
325    
326        }
327    
328        /**
329         * Draws a single data item.
330         *
331         * @param g2  the graphics device (<code>null</code> not permitted).
332         * @param section  the section index.
333         * @param dataArea  the data plot area.
334         * @param state  state information for one chart.
335         * @param currentPass  the current pass index.
336         */
337        protected void drawItem(Graphics2D g2,
338                                int section,
339                                Rectangle2D dataArea,
340                                PiePlotState state,
341                                int currentPass) {
342    
343            PieDataset dataset = getDataset();
344            Number n = dataset.getValue(section);
345            if (n == null) {
346                return;
347            }
348            double value = n.doubleValue();
349            double angle1 = 0.0;
350            double angle2 = 0.0;
351    
352            Rotation direction = getDirection();
353            if (direction == Rotation.CLOCKWISE) {
354                angle1 = state.getLatestAngle();
355                angle2 = angle1 - value / state.getTotal() * 360.0;
356            }
357            else if (direction == Rotation.ANTICLOCKWISE) {
358                angle1 = state.getLatestAngle();
359                angle2 = angle1 + value / state.getTotal() * 360.0;
360            }
361            else {
362                throw new IllegalStateException("Rotation type not recognised.");
363            }
364    
365            double angle = (angle2 - angle1);
366            if (Math.abs(angle) > getMinimumArcAngleToDraw()) {
367                Comparable key = getSectionKey(section);
368                double ep = 0.0;
369                double mep = getMaximumExplodePercent();
370                if (mep > 0.0) {
371                    ep = getExplodePercent(key) / mep;
372                }
373                Rectangle2D arcBounds = getArcBounds(state.getPieArea(),
374                        state.getExplodedPieArea(), angle1, angle, ep);
375                Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle,
376                        Arc2D.OPEN);
377    
378                // create the bounds for the inner arc
379                double depth = this.sectionDepth / 2.0;
380                RectangleInsets s = new RectangleInsets(UnitType.RELATIVE,
381                    depth, depth, depth, depth);
382                Rectangle2D innerArcBounds = new Rectangle2D.Double();
383                innerArcBounds.setRect(arcBounds);
384                s.trim(innerArcBounds);
385                // calculate inner arc in reverse direction, for later
386                // GeneralPath construction
387                Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1
388                        + angle, -angle, Arc2D.OPEN);
389                GeneralPath path = new GeneralPath();
390                path.moveTo((float) arc.getStartPoint().getX(),
391                        (float) arc.getStartPoint().getY());
392                path.append(arc.getPathIterator(null), false);
393                path.append(arc2.getPathIterator(null), true);
394                path.closePath();
395    
396                Line2D separator = new Line2D.Double(arc2.getEndPoint(),
397                        arc.getStartPoint());
398    
399                if (currentPass == 0) {
400                    Paint shadowPaint = getShadowPaint();
401                    double shadowXOffset = getShadowXOffset();
402                    double shadowYOffset = getShadowYOffset();
403                    if (shadowPaint != null) {
404                        Shape shadowArc = ShapeUtilities.createTranslatedShape(
405                                path, (float) shadowXOffset, (float) shadowYOffset);
406                        g2.setPaint(shadowPaint);
407                        g2.fill(shadowArc);
408                    }
409                }
410                else if (currentPass == 1) {
411                    Paint paint = lookupSectionPaint(key);
412                    g2.setPaint(paint);
413                    g2.fill(path);
414                    Paint outlinePaint = lookupSectionOutlinePaint(key);
415                    Stroke outlineStroke = lookupSectionOutlineStroke(key);
416                    if (outlinePaint != null && 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        protected double getLabelLinkDepth() {
467            return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2);
468        }
469    
470        /**
471         * Tests this plot for equality with an arbitrary object.
472         *
473         * @param obj  the object to test against (<code>null</code> permitted).
474         *
475         * @return A boolean.
476         */
477        public boolean equals(Object obj) {
478            if (this == obj) {
479                return true;
480            }
481            if (!(obj instanceof RingPlot)) {
482                return false;
483            }
484            RingPlot that = (RingPlot) obj;
485            if (this.separatorsVisible != that.separatorsVisible) {
486                return false;
487            }
488            if (!ObjectUtilities.equal(this.separatorStroke,
489                    that.separatorStroke)) {
490                return false;
491            }
492            if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) {
493                return false;
494            }
495            if (this.innerSeparatorExtension != that.innerSeparatorExtension) {
496                return false;
497            }
498            if (this.outerSeparatorExtension != that.outerSeparatorExtension) {
499                return false;
500            }
501            if (this.sectionDepth != that.sectionDepth) {
502                return false;
503            }
504            return super.equals(obj);
505        }
506    
507        /**
508         * Creates a new line by extending an existing line.
509         *
510         * @param line  the line (<code>null</code> not permitted).
511         * @param startPercent  the amount to extend the line at the start point
512         *                      end.
513         * @param endPercent  the amount to extend the line at the end point end.
514         *
515         * @return A new line.
516         */
517        private Line2D extendLine(Line2D line, double startPercent,
518                                  double endPercent) {
519            if (line == null) {
520                throw new IllegalArgumentException("Null 'line' argument.");
521            }
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    }