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