001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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     * LineRenderer3D.java
029     * -------------------
030     * (C) Copyright 2004-2008, by Tobias Selb and Contributors.
031     *
032     * Original Author:  Tobias Selb (http://www.uepselon.com);
033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
034     *
035     * Changes
036     * -------
037     * 15-Oct-2004 : Version 1 (TS);
038     * 05-Nov-2004 : Modified drawItem() signature (DG);
039     * 11-Nov-2004 : Now uses ShapeUtilities class to translate shapes (DG);
040     * 26-Jan-2005 : Update for changes in super class (DG);
041     * 13-Apr-2005 : Check item visibility in drawItem() method (DG);
042     * 09-Jun-2005 : Use addItemEntity() in drawItem() method (DG);
043     * 10-Jun-2005 : Fixed capitalisation of setXOffset() and setYOffset() (DG);
044     * ------------- JFREECHART 1.0.x ---------------------------------------------
045     * 01-Dec-2006 : Fixed equals() and serialization (DG);
046     * 17-Jan-2007 : Fixed bug in drawDomainGridline() method and added
047     *               argument check to setWallPaint() (DG);
048     * 03-Apr-2007 : Fixed bugs in drawBackground() method (DG);
049     * 16-Oct-2007 : Fixed bug in range marker drawing (DG);
050     *
051     */
052    
053    package org.jfree.chart.renderer.category;
054    
055    import java.awt.AlphaComposite;
056    import java.awt.Color;
057    import java.awt.Composite;
058    import java.awt.Graphics2D;
059    import java.awt.Image;
060    import java.awt.Paint;
061    import java.awt.Shape;
062    import java.awt.Stroke;
063    import java.awt.geom.GeneralPath;
064    import java.awt.geom.Line2D;
065    import java.awt.geom.Rectangle2D;
066    import java.io.IOException;
067    import java.io.ObjectInputStream;
068    import java.io.ObjectOutputStream;
069    import java.io.Serializable;
070    
071    import org.jfree.chart.Effect3D;
072    import org.jfree.chart.axis.CategoryAxis;
073    import org.jfree.chart.axis.ValueAxis;
074    import org.jfree.chart.entity.EntityCollection;
075    import org.jfree.chart.event.RendererChangeEvent;
076    import org.jfree.chart.plot.CategoryPlot;
077    import org.jfree.chart.plot.Marker;
078    import org.jfree.chart.plot.PlotOrientation;
079    import org.jfree.chart.plot.ValueMarker;
080    import org.jfree.data.Range;
081    import org.jfree.data.category.CategoryDataset;
082    import org.jfree.io.SerialUtilities;
083    import org.jfree.util.PaintUtilities;
084    import org.jfree.util.ShapeUtilities;
085    
086    /**
087     * A line renderer with a 3D effect.
088     */
089    public class LineRenderer3D extends LineAndShapeRenderer
090                                implements Effect3D, Serializable {
091    
092        /** For serialization. */
093        private static final long serialVersionUID = 5467931468380928736L;
094    
095        /** The default x-offset for the 3D effect. */
096        public static final double DEFAULT_X_OFFSET = 12.0;
097    
098        /** The default y-offset for the 3D effect. */
099        public static final double DEFAULT_Y_OFFSET = 8.0;
100    
101        /** The default wall paint. */
102        public static final Paint DEFAULT_WALL_PAINT = new Color(0xDD, 0xDD, 0xDD);
103    
104        /** The size of x-offset for the 3D effect. */
105        private double xOffset;
106    
107        /** The size of y-offset for the 3D effect. */
108        private double yOffset;
109    
110        /** The paint used to shade the left and lower 3D wall. */
111        private transient Paint wallPaint;
112    
113        /**
114         * Creates a new renderer.
115         */
116        public LineRenderer3D() {
117            super(true, false);  //Create a line renderer only
118            this.xOffset = DEFAULT_X_OFFSET;
119            this.yOffset = DEFAULT_Y_OFFSET;
120            this.wallPaint = DEFAULT_WALL_PAINT;
121        }
122    
123        /**
124         * Returns the x-offset for the 3D effect.
125         *
126         * @return The x-offset.
127         *
128         * @see #setXOffset(double)
129         * @see #getYOffset()
130         */
131        public double getXOffset() {
132            return this.xOffset;
133        }
134    
135        /**
136         * Returns the y-offset for the 3D effect.
137         *
138         * @return The y-offset.
139         *
140         * @see #setYOffset(double)
141         * @see #getXOffset()
142         */
143        public double getYOffset() {
144            return this.yOffset;
145        }
146    
147        /**
148         * Sets the x-offset and sends a {@link RendererChangeEvent} to all
149         * registered listeners.
150         *
151         * @param xOffset  the x-offset.
152         *
153         * @see #getXOffset()
154         */
155        public void setXOffset(double xOffset) {
156            this.xOffset = xOffset;
157            fireChangeEvent();
158        }
159    
160        /**
161         * Sets the y-offset and sends a {@link RendererChangeEvent} to all
162         * registered listeners.
163         *
164         * @param yOffset  the y-offset.
165         *
166         * @see #getYOffset()
167         */
168        public void setYOffset(double yOffset) {
169            this.yOffset = yOffset;
170            fireChangeEvent();
171        }
172    
173        /**
174         * Returns the paint used to highlight the left and bottom wall in the plot
175         * background.
176         *
177         * @return The paint.
178         *
179         * @see #setWallPaint(Paint)
180         */
181        public Paint getWallPaint() {
182            return this.wallPaint;
183        }
184    
185        /**
186         * Sets the paint used to hightlight the left and bottom walls in the plot
187         * background, and sends a {@link RendererChangeEvent} to all
188         * registered listeners.
189         *
190         * @param paint  the paint (<code>null</code> not permitted).
191         *
192         * @see #getWallPaint()
193         */
194        public void setWallPaint(Paint paint) {
195            if (paint == null) {
196                throw new IllegalArgumentException("Null 'paint' argument.");
197            }
198            this.wallPaint = paint;
199            fireChangeEvent();
200        }
201    
202        /**
203         * Draws the background for the plot.
204         *
205         * @param g2  the graphics device.
206         * @param plot  the plot.
207         * @param dataArea  the area inside the axes.
208         */
209        public void drawBackground(Graphics2D g2, CategoryPlot plot,
210                                   Rectangle2D dataArea) {
211    
212            float x0 = (float) dataArea.getX();
213            float x1 = x0 + (float) Math.abs(this.xOffset);
214            float x3 = (float) dataArea.getMaxX();
215            float x2 = x3 - (float) Math.abs(this.xOffset);
216    
217            float y0 = (float) dataArea.getMaxY();
218            float y1 = y0 - (float) Math.abs(this.yOffset);
219            float y3 = (float) dataArea.getMinY();
220            float y2 = y3 + (float) Math.abs(this.yOffset);
221    
222            GeneralPath clip = new GeneralPath();
223            clip.moveTo(x0, y0);
224            clip.lineTo(x0, y2);
225            clip.lineTo(x1, y3);
226            clip.lineTo(x3, y3);
227            clip.lineTo(x3, y1);
228            clip.lineTo(x2, y0);
229            clip.closePath();
230    
231            Composite originalComposite = g2.getComposite();
232            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
233                    plot.getBackgroundAlpha()));
234    
235            // fill background...
236            Paint backgroundPaint = plot.getBackgroundPaint();
237            if (backgroundPaint != null) {
238                g2.setPaint(backgroundPaint);
239                g2.fill(clip);
240            }
241    
242            GeneralPath leftWall = new GeneralPath();
243            leftWall.moveTo(x0, y0);
244            leftWall.lineTo(x0, y2);
245            leftWall.lineTo(x1, y3);
246            leftWall.lineTo(x1, y1);
247            leftWall.closePath();
248            g2.setPaint(getWallPaint());
249            g2.fill(leftWall);
250    
251            GeneralPath bottomWall = new GeneralPath();
252            bottomWall.moveTo(x0, y0);
253            bottomWall.lineTo(x1, y1);
254            bottomWall.lineTo(x3, y1);
255            bottomWall.lineTo(x2, y0);
256            bottomWall.closePath();
257            g2.setPaint(getWallPaint());
258            g2.fill(bottomWall);
259    
260            // higlight the background corners...
261            g2.setPaint(Color.lightGray);
262            Line2D corner = new Line2D.Double(x0, y0, x1, y1);
263            g2.draw(corner);
264            corner.setLine(x1, y1, x1, y3);
265            g2.draw(corner);
266            corner.setLine(x1, y1, x3, y1);
267            g2.draw(corner);
268    
269            // draw background image, if there is one...
270            Image backgroundImage = plot.getBackgroundImage();
271            if (backgroundImage != null) {
272                Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX()
273                        + getXOffset(), dataArea.getY(),
274                        dataArea.getWidth() - getXOffset(),
275                        dataArea.getHeight() - getYOffset());
276                plot.drawBackgroundImage(g2, adjusted);
277            }
278    
279            g2.setComposite(originalComposite);
280    
281        }
282    
283        /**
284         * Draws the outline for the plot.
285         *
286         * @param g2  the graphics device.
287         * @param plot  the plot.
288         * @param dataArea  the area inside the axes.
289         */
290        public void drawOutline(Graphics2D g2, CategoryPlot plot,
291                                Rectangle2D dataArea) {
292    
293            float x0 = (float) dataArea.getX();
294            float x1 = x0 + (float) Math.abs(this.xOffset);
295            float x3 = (float) dataArea.getMaxX();
296            float x2 = x3 - (float) Math.abs(this.xOffset);
297    
298            float y0 = (float) dataArea.getMaxY();
299            float y1 = y0 - (float) Math.abs(this.yOffset);
300            float y3 = (float) dataArea.getMinY();
301            float y2 = y3 + (float) Math.abs(this.yOffset);
302    
303            GeneralPath clip = new GeneralPath();
304            clip.moveTo(x0, y0);
305            clip.lineTo(x0, y2);
306            clip.lineTo(x1, y3);
307            clip.lineTo(x3, y3);
308            clip.lineTo(x3, y1);
309            clip.lineTo(x2, y0);
310            clip.closePath();
311    
312            // put an outline around the data area...
313            Stroke outlineStroke = plot.getOutlineStroke();
314            Paint outlinePaint = plot.getOutlinePaint();
315            if ((outlineStroke != null) && (outlinePaint != null)) {
316                g2.setStroke(outlineStroke);
317                g2.setPaint(outlinePaint);
318                g2.draw(clip);
319            }
320    
321        }
322    
323        /**
324         * Draws a grid line against the domain axis.
325         *
326         * @param g2  the graphics device.
327         * @param plot  the plot.
328         * @param dataArea  the area for plotting data (not yet adjusted for any
329         *                  3D effect).
330         * @param value  the Java2D value at which the grid line should be drawn.
331         *
332         */
333        public void drawDomainGridline(Graphics2D g2,
334                                       CategoryPlot plot,
335                                       Rectangle2D dataArea,
336                                       double value) {
337    
338            Line2D line1 = null;
339            Line2D line2 = null;
340            PlotOrientation orientation = plot.getOrientation();
341            if (orientation == PlotOrientation.HORIZONTAL) {
342                double y0 = value;
343                double y1 = value - getYOffset();
344                double x0 = dataArea.getMinX();
345                double x1 = x0 + getXOffset();
346                double x2 = dataArea.getMaxX();
347                line1 = new Line2D.Double(x0, y0, x1, y1);
348                line2 = new Line2D.Double(x1, y1, x2, y1);
349            }
350            else if (orientation == PlotOrientation.VERTICAL) {
351                double x0 = value;
352                double x1 = value + getXOffset();
353                double y0 = dataArea.getMaxY();
354                double y1 = y0 - getYOffset();
355                double y2 = dataArea.getMinY();
356                line1 = new Line2D.Double(x0, y0, x1, y1);
357                line2 = new Line2D.Double(x1, y1, x1, y2);
358            }
359            g2.setPaint(plot.getDomainGridlinePaint());
360            g2.setStroke(plot.getDomainGridlineStroke());
361            g2.draw(line1);
362            g2.draw(line2);
363    
364        }
365    
366        /**
367         * Draws a grid line against the range axis.
368         *
369         * @param g2  the graphics device.
370         * @param plot  the plot.
371         * @param axis  the value axis.
372         * @param dataArea  the area for plotting data (not yet adjusted for any
373         *                  3D effect).
374         * @param value  the value at which the grid line should be drawn.
375         *
376         */
377        public void drawRangeGridline(Graphics2D g2,
378                                      CategoryPlot plot,
379                                      ValueAxis axis,
380                                      Rectangle2D dataArea,
381                                      double value) {
382    
383            Range range = axis.getRange();
384    
385            if (!range.contains(value)) {
386                return;
387            }
388    
389            Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
390                    dataArea.getY() + getYOffset(),
391                    dataArea.getWidth() - getXOffset(),
392                    dataArea.getHeight() - getYOffset());
393    
394            Line2D line1 = null;
395            Line2D line2 = null;
396            PlotOrientation orientation = plot.getOrientation();
397            if (orientation == PlotOrientation.HORIZONTAL) {
398                double x0 = axis.valueToJava2D(value, adjusted,
399                        plot.getRangeAxisEdge());
400                double x1 = x0 + getXOffset();
401                double y0 = dataArea.getMaxY();
402                double y1 = y0 - getYOffset();
403                double y2 = dataArea.getMinY();
404                line1 = new Line2D.Double(x0, y0, x1, y1);
405                line2 = new Line2D.Double(x1, y1, x1, y2);
406            }
407            else if (orientation == PlotOrientation.VERTICAL) {
408                double y0 = axis.valueToJava2D(value, adjusted,
409                        plot.getRangeAxisEdge());
410                double y1 = y0 - getYOffset();
411                double x0 = dataArea.getMinX();
412                double x1 = x0 + getXOffset();
413                double x2 = dataArea.getMaxX();
414                line1 = new Line2D.Double(x0, y0, x1, y1);
415                line2 = new Line2D.Double(x1, y1, x2, y1);
416            }
417            g2.setPaint(plot.getRangeGridlinePaint());
418            g2.setStroke(plot.getRangeGridlineStroke());
419            g2.draw(line1);
420            g2.draw(line2);
421    
422        }
423    
424        /**
425         * Draws a range marker.
426         *
427         * @param g2  the graphics device.
428         * @param plot  the plot.
429         * @param axis  the value axis.
430         * @param marker  the marker.
431         * @param dataArea  the area for plotting data (not including 3D effect).
432         */
433        public void drawRangeMarker(Graphics2D g2,
434                                    CategoryPlot plot,
435                                    ValueAxis axis,
436                                    Marker marker,
437                                    Rectangle2D dataArea) {
438    
439            Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
440                    dataArea.getY() + getYOffset(),
441                    dataArea.getWidth() - getXOffset(),
442                    dataArea.getHeight() - getYOffset());
443    
444            if (marker instanceof ValueMarker) {
445                ValueMarker vm = (ValueMarker) marker;
446                double value = vm.getValue();
447                Range range = axis.getRange();
448                if (!range.contains(value)) {
449                    return;
450                }
451    
452                GeneralPath path = null;
453                PlotOrientation orientation = plot.getOrientation();
454                if (orientation == PlotOrientation.HORIZONTAL) {
455                    float x = (float) axis.valueToJava2D(value, adjusted,
456                            plot.getRangeAxisEdge());
457                    float y = (float) adjusted.getMaxY();
458                    path = new GeneralPath();
459                    path.moveTo(x, y);
460                    path.lineTo((float) (x + getXOffset()),
461                            y - (float) getYOffset());
462                    path.lineTo((float) (x + getXOffset()),
463                            (float) (adjusted.getMinY() - getYOffset()));
464                    path.lineTo(x, (float) adjusted.getMinY());
465                    path.closePath();
466                }
467                else if (orientation == PlotOrientation.VERTICAL) {
468                    float y = (float) axis.valueToJava2D(value, adjusted,
469                            plot.getRangeAxisEdge());
470                    float x = (float) dataArea.getX();
471                    path = new GeneralPath();
472                    path.moveTo(x, y);
473                    path.lineTo(x + (float) this.xOffset, y - (float) this.yOffset);
474                    path.lineTo((float) (adjusted.getMaxX() + this.xOffset),
475                            y - (float) this.yOffset);
476                    path.lineTo((float) (adjusted.getMaxX()), y);
477                    path.closePath();
478                }
479                g2.setPaint(marker.getPaint());
480                g2.fill(path);
481                g2.setPaint(marker.getOutlinePaint());
482                g2.draw(path);
483            }
484            else {
485                super.drawRangeMarker(g2, plot, axis, marker, adjusted);
486                // TODO: draw the interval marker with a 3D effect
487            }
488        }
489    
490       /**
491         * Draw a single data item.
492         *
493         * @param g2  the graphics device.
494         * @param state  the renderer state.
495         * @param dataArea  the area in which the data is drawn.
496         * @param plot  the plot.
497         * @param domainAxis  the domain axis.
498         * @param rangeAxis  the range axis.
499         * @param dataset  the dataset.
500         * @param row  the row index (zero-based).
501         * @param column  the column index (zero-based).
502         * @param pass  the pass index.
503         */
504        public void drawItem(Graphics2D g2,
505                             CategoryItemRendererState state,
506                             Rectangle2D dataArea,
507                             CategoryPlot plot,
508                             CategoryAxis domainAxis,
509                             ValueAxis rangeAxis,
510                             CategoryDataset dataset,
511                             int row,
512                             int column,
513                             int pass) {
514    
515            if (!getItemVisible(row, column)) {
516                return;
517            }
518    
519            // nothing is drawn for null...
520            Number v = dataset.getValue(row, column);
521            if (v == null) {
522                return;
523            }
524    
525            Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
526                    dataArea.getY() + getYOffset(),
527                    dataArea.getWidth() - getXOffset(),
528                    dataArea.getHeight() - getYOffset());
529    
530            PlotOrientation orientation = plot.getOrientation();
531    
532            // current data point...
533            double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
534                    adjusted, plot.getDomainAxisEdge());
535            double value = v.doubleValue();
536            double y1 = rangeAxis.valueToJava2D(value, adjusted,
537                    plot.getRangeAxisEdge());
538    
539            Shape shape = getItemShape(row, column);
540            if (orientation == PlotOrientation.HORIZONTAL) {
541                shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
542            }
543            else if (orientation == PlotOrientation.VERTICAL) {
544                shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
545            }
546    
547            if (getItemLineVisible(row, column)) {
548                if (column != 0) {
549    
550                    Number previousValue = dataset.getValue(row, column - 1);
551                    if (previousValue != null) {
552    
553                        // previous data point...
554                        double previous = previousValue.doubleValue();
555                        double x0 = domainAxis.getCategoryMiddle(column - 1,
556                                getColumnCount(), adjusted,
557                                plot.getDomainAxisEdge());
558                        double y0 = rangeAxis.valueToJava2D(previous, adjusted,
559                                plot.getRangeAxisEdge());
560    
561                        double x2 = x0 + getXOffset();
562                        double y2 = y0 - getYOffset();
563                        double x3 = x1 + getXOffset();
564                        double y3 = y1 - getYOffset();
565    
566                        GeneralPath clip = new GeneralPath();
567    
568                        if (orientation == PlotOrientation.HORIZONTAL) {
569                            clip.moveTo((float) y0, (float) x0);
570                            clip.lineTo((float) y1, (float) x1);
571                            clip.lineTo((float) y3, (float) x3);
572                            clip.lineTo((float) y2, (float) x2);
573                            clip.lineTo((float) y0, (float) x0);
574                            clip.closePath();
575                        }
576                        else if (orientation == PlotOrientation.VERTICAL) {
577                            clip.moveTo((float) x0, (float) y0);
578                            clip.lineTo((float) x1, (float) y1);
579                            clip.lineTo((float) x3, (float) y3);
580                            clip.lineTo((float) x2, (float) y2);
581                            clip.lineTo((float) x0, (float) y0);
582                            clip.closePath();
583                        }
584    
585                        g2.setPaint(getItemPaint(row, column));
586                        g2.fill(clip);
587                        g2.setStroke(getItemOutlineStroke(row, column));
588                        g2.setPaint(getItemOutlinePaint(row, column));
589                        g2.draw(clip);
590                    }
591                }
592            }
593    
594            // draw the item label if there is one...
595            if (isItemLabelVisible(row, column)) {
596                drawItemLabel(g2, orientation, dataset, row, column, x1, y1,
597                        (value < 0.0));
598            }
599    
600            // add an item entity, if this information is being collected
601            EntityCollection entities = state.getEntityCollection();
602            if (entities != null) {
603                addItemEntity(entities, dataset, row, column, shape);
604            }
605    
606        }
607    
608        /**
609         * Checks this renderer for equality with an arbitrary object.
610         *
611         * @param obj  the object (<code>null</code> permitted).
612         *
613         * @return A boolean.
614         */
615        public boolean equals(Object obj) {
616            if (obj == this) {
617                return true;
618            }
619            if (!(obj instanceof LineRenderer3D)) {
620                return false;
621            }
622            LineRenderer3D that = (LineRenderer3D) obj;
623            if (this.xOffset != that.xOffset) {
624                return false;
625            }
626            if (this.yOffset != that.yOffset) {
627                return false;
628            }
629            if (!PaintUtilities.equal(this.wallPaint, that.wallPaint)) {
630                return false;
631            }
632            return super.equals(obj);
633        }
634    
635        /**
636         * Provides serialization support.
637         *
638         * @param stream  the output stream.
639         *
640         * @throws IOException  if there is an I/O error.
641         */
642        private void writeObject(ObjectOutputStream stream) throws IOException {
643            stream.defaultWriteObject();
644            SerialUtilities.writePaint(this.wallPaint, stream);
645        }
646    
647        /**
648         * Provides serialization support.
649         *
650         * @param stream  the input stream.
651         *
652         * @throws IOException  if there is an I/O error.
653         * @throws ClassNotFoundException  if there is a classpath problem.
654         */
655        private void readObject(ObjectInputStream stream)
656                throws IOException, ClassNotFoundException {
657            stream.defaultReadObject();
658            this.wallPaint = SerialUtilities.readPaint(stream);
659        }
660    
661    }