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