001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2011, 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     * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025     * Other names may be trademarks of their respective owners.]
026     *
027     * --------------------
028     * XYShapeRenderer.java
029     * --------------------
030     * (C) Copyright 2008-2011 by Andreas Haumer, xS+S and Contributors.
031     *
032     * Original Author:  Martin Hoeller (x Software + Systeme  xS+S - Andreas
033     *                       Haumer);
034     * Contributor(s):   David Gilbert (for Object Refinery Limited);
035     *
036     * Changes:
037     * --------
038     * 17-Sep-2008 : Version 1, based on a contribution from Martin Hoeller with
039     *               amendments by David Gilbert (DG);
040     * 16-Feb-2010 : Added findZBounds() (patch 2952086) (MH);
041     * 19-Oct-2011 : Fixed NPE in findRangeBounds() (bug 3026341) (DG);
042     *
043     */
044    
045    package org.jfree.chart.renderer.xy;
046    
047    import java.awt.BasicStroke;
048    import java.awt.Color;
049    import java.awt.Graphics2D;
050    import java.awt.Paint;
051    import java.awt.Shape;
052    import java.awt.Stroke;
053    import java.awt.geom.Ellipse2D;
054    import java.awt.geom.Line2D;
055    import java.awt.geom.Rectangle2D;
056    import java.io.IOException;
057    import java.io.ObjectInputStream;
058    import java.io.ObjectOutputStream;
059    import java.io.Serializable;
060    
061    import org.jfree.chart.axis.ValueAxis;
062    import org.jfree.chart.entity.EntityCollection;
063    import org.jfree.chart.event.RendererChangeEvent;
064    import org.jfree.chart.plot.CrosshairState;
065    import org.jfree.chart.plot.PlotOrientation;
066    import org.jfree.chart.plot.PlotRenderingInfo;
067    import org.jfree.chart.plot.XYPlot;
068    import org.jfree.chart.renderer.LookupPaintScale;
069    import org.jfree.chart.renderer.PaintScale;
070    import org.jfree.data.Range;
071    import org.jfree.data.general.DatasetUtilities;
072    import org.jfree.data.xy.XYDataset;
073    import org.jfree.data.xy.XYZDataset;
074    import org.jfree.io.SerialUtilities;
075    import org.jfree.util.PublicCloneable;
076    import org.jfree.util.ShapeUtilities;
077    
078    /**
079     * A renderer that draws shapes at (x, y) coordinates and, if the dataset
080     * is an instance of {@link XYZDataset}, fills the shapes with a paint that
081     * is based on the z-value (the paint is obtained from a lookup table).  The
082     * renderer also allows for optional guidelines, horizontal and vertical lines
083     * connecting the shape to the edges of the plot.
084     * <br><br>
085     * The example shown here is generated by the
086     * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart
087     * demo collection:
088     * <br><br>
089     * <img src="../../../../../images/XYShapeRendererSample.png"
090     * alt="XYShapeRendererSample.png" />
091     * <br><br>
092     * This renderer has similarities to, but also differences from, the
093     * {@link XYLineAndShapeRenderer}.
094     *
095     * @since 1.0.11
096     */
097    public class XYShapeRenderer extends AbstractXYItemRenderer
098            implements XYItemRenderer, Cloneable, Serializable {
099    
100        /** Auto generated serial version id. */
101        private static final long serialVersionUID = 8320552104211173221L;
102    
103        /** The paint scale (never null). */
104        private PaintScale paintScale;
105    
106        /** A flag that controls whether or not the shape outlines are drawn. */
107        private boolean drawOutlines;
108    
109        /**
110         * A flag that controls whether or not the outline paint is used (if not,
111         * the regular paint is used).
112         */
113        private boolean useOutlinePaint;
114    
115        /**
116         * A flag that controls whether or not the fill paint is used (if not,
117         * the fill paint is used).
118         */
119        private boolean useFillPaint;
120    
121        /** Flag indicating if guide lines should be drawn for every item. */
122        private boolean guideLinesVisible;
123    
124        /** The paint used for drawing the guide lines (never null). */
125        private transient Paint guideLinePaint;
126    
127        /** The stroke used for drawing the guide lines (never null). */
128        private transient Stroke guideLineStroke;
129    
130        /**
131         * Creates a new <code>XYShapeRenderer</code> instance with default
132         * attributes.
133         */
134        public XYShapeRenderer() {
135            this.paintScale = new LookupPaintScale();
136            this.useFillPaint = false;
137            this.drawOutlines = false;
138            this.useOutlinePaint = true;
139            this.guideLinesVisible = false;
140            this.guideLinePaint = Color.darkGray;
141            this.guideLineStroke = new BasicStroke();
142            setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0));
143            setAutoPopulateSeriesShape(false);
144        }
145    
146        /**
147         * Returns the paint scale used by the renderer.
148         *
149         * @return The paint scale (never <code>null</code>).
150         *
151         * @see #setPaintScale(PaintScale)
152         */
153        public PaintScale getPaintScale() {
154            return this.paintScale;
155        }
156    
157        /**
158         * Sets the paint scale used by the renderer and sends a
159         * {@link RendererChangeEvent} to all registered listeners.
160         *
161         * @param scale  the scale (<code>null</code> not permitted).
162         *
163         * @see #getPaintScale()
164         */
165        public void setPaintScale(PaintScale scale) {
166            if (scale == null) {
167                throw new IllegalArgumentException("Null 'scale' argument.");
168            }
169            this.paintScale = scale;
170            notifyListeners(new RendererChangeEvent(this));
171        }
172    
173        /**
174         * Returns <code>true</code> if outlines should be drawn for shapes, and
175         * <code>false</code> otherwise.
176         *
177         * @return A boolean.
178         *
179         * @see #setDrawOutlines(boolean)
180         */
181        public boolean getDrawOutlines() {
182            return this.drawOutlines;
183        }
184    
185        /**
186         * Sets the flag that controls whether outlines are drawn for
187         * shapes, and sends a {@link RendererChangeEvent} to all registered
188         * listeners.
189         * <P>
190         * In some cases, shapes look better if they do NOT have an outline, but
191         * this flag allows you to set your own preference.
192         *
193         * @param flag  the flag.
194         *
195         * @see #getDrawOutlines()
196         */
197        public void setDrawOutlines(boolean flag) {
198            this.drawOutlines = flag;
199            fireChangeEvent();
200        }
201    
202        /**
203         * Returns <code>true</code> if the renderer should use the fill paint
204         * setting to fill shapes, and <code>false</code> if it should just
205         * use the regular paint.
206         * <p>
207         * Refer to <code>XYLineAndShapeRendererDemo2.java</code> to see the
208         * effect of this flag.
209         *
210         * @return A boolean.
211         *
212         * @see #setUseFillPaint(boolean)
213         * @see #getUseOutlinePaint()
214         */
215        public boolean getUseFillPaint() {
216            return this.useFillPaint;
217        }
218    
219        /**
220         * Sets the flag that controls whether the fill paint is used to fill
221         * shapes, and sends a {@link RendererChangeEvent} to all
222         * registered listeners.
223         *
224         * @param flag  the flag.
225         *
226         * @see #getUseFillPaint()
227         */
228        public void setUseFillPaint(boolean flag) {
229            this.useFillPaint = flag;
230            fireChangeEvent();
231        }
232    
233        /**
234         * Returns the flag that controls whether the outline paint is used for
235         * shape outlines.  If not, the regular series paint is used.
236         *
237         * @return A boolean.
238         *
239         * @see #setUseOutlinePaint(boolean)
240         */
241        public boolean getUseOutlinePaint() {
242            return this.useOutlinePaint;
243        }
244    
245        /**
246         * Sets the flag that controls whether the outline paint is used for shape
247         * outlines, and sends a {@link RendererChangeEvent} to all registered
248         * listeners.
249         *
250         * @param use  the flag.
251         *
252         * @see #getUseOutlinePaint()
253         */
254        public void setUseOutlinePaint(boolean use) {
255            this.useOutlinePaint = use;
256            fireChangeEvent();
257        }
258    
259        /**
260         * Returns a flag that controls whether or not guide lines are drawn for
261         * each data item (the lines are horizontal and vertical "crosshairs"
262         * linking the data point to the axes).
263         *
264         * @return A boolean.
265         *
266         * @see #setGuideLinesVisible(boolean)
267         */
268        public boolean isGuideLinesVisible() {
269            return this.guideLinesVisible;
270        }
271    
272        /**
273         * Sets the flag that controls whether or not guide lines are drawn for
274         * each data item and sends a {@link RendererChangeEvent} to all registered
275         * listeners.
276         *
277         * @param visible  the new flag value.
278         *
279         * @see #isGuideLinesVisible()
280         */
281        public void setGuideLinesVisible(boolean visible) {
282            this.guideLinesVisible = visible;
283            fireChangeEvent();
284        }
285    
286        /**
287         * Returns the paint used to draw the guide lines.
288         *
289         * @return The paint (never <code>null</code>).
290         *
291         * @see #setGuideLinePaint(Paint)
292         */
293        public Paint getGuideLinePaint() {
294            return this.guideLinePaint;
295        }
296    
297        /**
298         * Sets the paint used to draw the guide lines and sends a
299         * {@link RendererChangeEvent} to all registered listeners.
300         *
301         * @param paint  the paint (<code>null</code> not permitted).
302         *
303         * @see #getGuideLinePaint()
304         */
305        public void setGuideLinePaint(Paint paint) {
306            if (paint == null) {
307                throw new IllegalArgumentException("Null 'paint' argument.");
308            }
309            this.guideLinePaint = paint;
310            fireChangeEvent();
311        }
312    
313        /**
314         * Returns the stroke used to draw the guide lines.
315         *
316         * @return The stroke.
317         *
318         * @see #setGuideLineStroke(Stroke)
319         */
320        public Stroke getGuideLineStroke() {
321            return this.guideLineStroke;
322        }
323    
324        /**
325         * Sets the stroke used to draw the guide lines and sends a
326         * {@link RendererChangeEvent} to all registered listeners.
327         *
328         * @param stroke  the stroke (<code>null</code> not permitted).
329         *
330         * @see #getGuideLineStroke()
331         */
332        public void setGuideLineStroke(Stroke stroke) {
333            if (stroke == null) {
334                throw new IllegalArgumentException("Null 'stroke' argument.");
335            }
336            this.guideLineStroke = stroke;
337            fireChangeEvent();
338        }
339    
340        /**
341         * Returns the lower and upper bounds (range) of the x-values in the
342         * specified dataset.
343         *
344         * @param dataset  the dataset (<code>null</code> permitted).
345         *
346         * @return The range (<code>null</code> if the dataset is <code>null</code>
347         *         or empty).
348         */
349        public Range findDomainBounds(XYDataset dataset) {
350            if (dataset == null) {
351                return null;
352            }
353            Range r = DatasetUtilities.findDomainBounds(dataset, false);
354            if (r == null) {
355                return null;
356            }
357            double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2;
358            return new Range(r.getLowerBound() + offset,
359                             r.getUpperBound() + offset);
360        }
361    
362        /**
363         * Returns the range of values the renderer requires to display all the
364         * items from the specified dataset.
365         *
366         * @param dataset  the dataset (<code>null</code> permitted).
367         *
368         * @return The range (<code>null</code> if the dataset is <code>null</code>
369         *         or empty).
370         */
371        public Range findRangeBounds(XYDataset dataset) {
372            if (dataset == null) {
373                return null;
374            }
375            Range r = DatasetUtilities.findRangeBounds(dataset, false);
376            if (r == null) {
377                return null;
378            }
379            double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2;
380            return new Range(r.getLowerBound() + offset, r.getUpperBound()
381                    + offset);
382        }
383    
384        /**
385         * Return the range of z-values in the specified dataset.
386         *  
387         * @param dataset  the dataset (<code>null</code> permitted).
388         * 
389         * @return The range (<code>null</code> if the dataset is <code>null</code>
390         *         or empty).
391         */
392        public Range findZBounds(XYZDataset dataset) {
393            if (dataset != null) {
394                return DatasetUtilities.findZBounds(dataset);
395            }
396            else {
397                return null;
398            }
399        }
400    
401        /**
402         * Returns the number of passes required by this renderer.
403         *
404         * @return <code>2</code>.
405         */
406        public int getPassCount() {
407            return 2;
408        }
409    
410        /**
411         * Draws the block representing the specified item.
412         *
413         * @param g2  the graphics device.
414         * @param state  the state.
415         * @param dataArea  the data area.
416         * @param info  the plot rendering info.
417         * @param plot  the plot.
418         * @param domainAxis  the x-axis.
419         * @param rangeAxis  the y-axis.
420         * @param dataset  the dataset.
421         * @param series  the series index.
422         * @param item  the item index.
423         * @param crosshairState  the crosshair state.
424         * @param pass  the pass index.
425         */
426        public void drawItem(Graphics2D g2, XYItemRendererState state,
427                Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
428                ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
429                int series, int item, CrosshairState crosshairState, int pass) {
430    
431            Shape hotspot = null;
432            EntityCollection entities = null;
433            if (info != null) {
434                entities = info.getOwner().getEntityCollection();
435            }
436    
437            double x = dataset.getXValue(series, item);
438            double y = dataset.getYValue(series, item);
439            if (Double.isNaN(x) || Double.isNaN(y)) {
440                // can't draw anything
441                return;
442            }
443    
444            double transX = domainAxis.valueToJava2D(x, dataArea,
445                    plot.getDomainAxisEdge());
446            double transY = rangeAxis.valueToJava2D(y, dataArea,
447                    plot.getRangeAxisEdge());
448    
449            PlotOrientation orientation = plot.getOrientation();
450    
451            // draw optional guide lines
452            if ((pass == 0) && this.guideLinesVisible) {
453                g2.setStroke(this.guideLineStroke);
454                g2.setPaint(this.guideLinePaint);
455                if (orientation == PlotOrientation.HORIZONTAL) {
456                    g2.draw(new Line2D.Double(transY, dataArea.getMinY(), transY,
457                            dataArea.getMaxY()));
458                    g2.draw(new Line2D.Double(dataArea.getMinX(), transX,
459                            dataArea.getMaxX(), transX));
460                }
461                else {
462                    g2.draw(new Line2D.Double(transX, dataArea.getMinY(), transX,
463                            dataArea.getMaxY()));
464                    g2.draw(new Line2D.Double(dataArea.getMinX(), transY,
465                            dataArea.getMaxX(), transY));
466                }
467            }
468            else if (pass == 1) {
469                Shape shape = getItemShape(series, item);
470                if (orientation == PlotOrientation.HORIZONTAL) {
471                    shape = ShapeUtilities.createTranslatedShape(shape, transY,
472                            transX);
473                }
474                else if (orientation == PlotOrientation.VERTICAL) {
475                    shape = ShapeUtilities.createTranslatedShape(shape, transX,
476                            transY);
477                }
478                hotspot = shape;
479                if (shape.intersects(dataArea)) {
480                    //if (getItemShapeFilled(series, item)) {
481                        g2.setPaint(getPaint(dataset, series, item));
482                        g2.fill(shape);
483                   //}
484                    if (this.drawOutlines) {
485                        if (getUseOutlinePaint()) {
486                            g2.setPaint(getItemOutlinePaint(series, item));
487                        }
488                        else {
489                            g2.setPaint(getItemPaint(series, item));
490                        }
491                        g2.setStroke(getItemOutlineStroke(series, item));
492                        g2.draw(shape);
493                    }
494                }
495    
496                // add an entity for the item...
497                if (entities != null) {
498                    addEntity(entities, hotspot, dataset, series, item, transX,
499                            transY);
500                }
501            }
502        }
503    
504        /**
505         * Get the paint for a given series and item from a dataset.
506         *
507         * @param dataset  the dataset..
508         * @param series  the series index.
509         * @param item  the item index.
510         *
511         * @return The paint.
512         */
513        protected Paint getPaint(XYDataset dataset, int series, int item) {
514            Paint p = null;
515            if (dataset instanceof XYZDataset) {
516                double z = ((XYZDataset) dataset).getZValue(series, item);
517                p = this.paintScale.getPaint(z);
518            }
519            else {
520                if (this.useFillPaint) {
521                    p = getItemFillPaint(series, item);
522                }
523                else {
524                    p = getItemPaint(series, item);
525                }
526            }
527            return p;
528        }
529    
530        /**
531         * Tests this instance for equality with an arbitrary object.  This method
532         * returns <code>true</code> if and only if:
533         * <ul>
534         * <li><code>obj</code> is an instance of <code>XYShapeRenderer</code> (not
535         *     <code>null</code>);</li>
536         * <li><code>obj</code> has the same field values as this
537         *     <code>XYShapeRenderer</code>;</li>
538         * </ul>
539         *
540         * @param obj  the object (<code>null</code> permitted).
541         *
542         * @return A boolean.
543         */
544        public boolean equals(Object obj) {
545            if (obj == this) {
546                return true;
547            }
548            if (!(obj instanceof XYShapeRenderer)) {
549                return false;
550            }
551            XYShapeRenderer that = (XYShapeRenderer) obj;
552            if (!this.paintScale.equals(that.paintScale)) {
553                return false;
554            }
555            if (this.drawOutlines != that.drawOutlines) {
556                return false;
557            }
558            if (this.useOutlinePaint != that.useOutlinePaint) {
559                return false;
560            }
561            if (this.useFillPaint != that.useFillPaint) {
562                return false;
563            }
564            if (this.guideLinesVisible != that.guideLinesVisible) {
565                return false;
566            }
567            if (!this.guideLinePaint.equals(that.guideLinePaint)) {
568                return false;
569            }
570            if (!this.guideLineStroke.equals(that.guideLineStroke)) {
571                return false;
572            }
573            return super.equals(obj);
574        }
575    
576        /**
577         * Returns a clone of this renderer.
578         *
579         * @return A clone of this renderer.
580         *
581         * @throws CloneNotSupportedException if there is a problem creating the
582         *     clone.
583         */
584        public Object clone() throws CloneNotSupportedException {
585            XYShapeRenderer clone = (XYShapeRenderer) super.clone();
586            if (this.paintScale instanceof PublicCloneable) {
587                PublicCloneable pc = (PublicCloneable) this.paintScale;
588                clone.paintScale = (PaintScale) pc.clone();
589            }
590            return clone;
591        }
592    
593        /**
594         * Provides serialization support.
595         *
596         * @param stream  the input stream.
597         *
598         * @throws IOException  if there is an I/O error.
599         * @throws ClassNotFoundException  if there is a classpath problem.
600         */
601        private void readObject(ObjectInputStream stream)
602                throws IOException, ClassNotFoundException {
603            stream.defaultReadObject();
604            this.guideLinePaint = SerialUtilities.readPaint(stream);
605            this.guideLineStroke = SerialUtilities.readStroke(stream);
606        }
607    
608        /**
609         * Provides serialization support.
610         *
611         * @param stream  the output stream.
612         *
613         * @throws IOException  if there is an I/O error.
614         */
615        private void writeObject(ObjectOutputStream stream) throws IOException {
616            stream.defaultWriteObject();
617            SerialUtilities.writePaint(this.guideLinePaint, stream);
618            SerialUtilities.writeStroke(this.guideLineStroke, stream);
619        }
620    
621    }