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