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