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