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