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.7 2007/01/17 14:16:11 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     * 
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            notifyListeners(new RendererChangeEvent(this));
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            notifyListeners(new RendererChangeEvent(this));
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            notifyListeners(new RendererChangeEvent(this));
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            // fill background...
232            Paint backgroundPaint = plot.getBackgroundPaint();
233            if (backgroundPaint != null) {
234                g2.setPaint(backgroundPaint);
235                g2.fill(clip);
236            }
237    
238            GeneralPath leftWall = new GeneralPath();
239            leftWall.moveTo(x0, y0);
240            leftWall.lineTo(x0, y2);
241            leftWall.lineTo(x1, y3);
242            leftWall.lineTo(x1, y1);
243            leftWall.closePath();
244            g2.setPaint(getWallPaint());
245            g2.fill(leftWall);
246    
247            GeneralPath bottomWall = new GeneralPath();
248            bottomWall.moveTo(x0, y0);
249            bottomWall.lineTo(x1, y1);
250            bottomWall.lineTo(x3, y1);
251            bottomWall.lineTo(x2, y0);
252            bottomWall.closePath();
253            g2.setPaint(getWallPaint());
254            g2.fill(bottomWall);
255    
256            // higlight the background corners...
257            g2.setPaint(Color.lightGray);
258            Line2D corner = new Line2D.Double(x0, y0, x1, y1);
259            g2.draw(corner);
260            corner.setLine(x1, y1, x1, y3);
261            g2.draw(corner);
262            corner.setLine(x1, y1, x3, y1);
263            g2.draw(corner);
264    
265            // draw background image, if there is one...
266            Image backgroundImage = plot.getBackgroundImage();
267            if (backgroundImage != null) {
268                Composite originalComposite = g2.getComposite();
269                g2.setComposite(AlphaComposite.getInstance(
270                        AlphaComposite.SRC, plot.getBackgroundAlpha()));
271                g2.drawImage(backgroundImage, (int) x1, (int) y3, 
272                        (int) (x3 - x1 + 1), (int) (y1 - y3 + 1), null);
273                g2.setComposite(originalComposite);
274            }
275    
276        }
277    
278        /**
279         * Draws the outline for the plot.
280         *
281         * @param g2  the graphics device.
282         * @param plot  the plot.
283         * @param dataArea  the area inside the axes.
284         */
285        public void drawOutline(Graphics2D g2, CategoryPlot plot, 
286                                Rectangle2D dataArea) {
287    
288            float x0 = (float) dataArea.getX();
289            float x1 = x0 + (float) Math.abs(this.xOffset);
290            float x3 = (float) dataArea.getMaxX();
291            float x2 = x3 - (float) Math.abs(this.xOffset);
292    
293            float y0 = (float) dataArea.getMaxY();
294            float y1 = y0 - (float) Math.abs(this.yOffset);
295            float y3 = (float) dataArea.getMinY();
296            float y2 = y3 + (float) Math.abs(this.yOffset);
297    
298            GeneralPath clip = new GeneralPath();
299            clip.moveTo(x0, y0);
300            clip.lineTo(x0, y2);
301            clip.lineTo(x1, y3);
302            clip.lineTo(x3, y3);
303            clip.lineTo(x3, y1);
304            clip.lineTo(x2, y0);
305            clip.closePath();
306    
307            // put an outline around the data area...
308            Stroke outlineStroke = plot.getOutlineStroke();
309            Paint outlinePaint = plot.getOutlinePaint();
310            if ((outlineStroke != null) && (outlinePaint != null)) {
311                g2.setStroke(outlineStroke);
312                g2.setPaint(outlinePaint);
313                g2.draw(clip);
314            }
315    
316        }
317    
318        /**
319         * Draws a grid line against the domain axis.
320         *
321         * @param g2  the graphics device.
322         * @param plot  the plot.
323         * @param dataArea  the area for plotting data (not yet adjusted for any 
324         *                  3D effect).
325         * @param value  the Java2D value at which the grid line should be drawn.
326         *
327         */
328        public void drawDomainGridline(Graphics2D g2,
329                                       CategoryPlot plot,
330                                       Rectangle2D dataArea,
331                                       double value) {
332    
333            Line2D line1 = null;
334            Line2D line2 = null;
335            PlotOrientation orientation = plot.getOrientation();
336            if (orientation == PlotOrientation.HORIZONTAL) {
337                double y0 = value;
338                double y1 = value - getYOffset();
339                double x0 = dataArea.getMinX();
340                double x1 = x0 + getXOffset();
341                double x2 = dataArea.getMaxX();
342                line1 = new Line2D.Double(x0, y0, x1, y1);
343                line2 = new Line2D.Double(x1, y1, x2, y1);
344            }
345            else if (orientation == PlotOrientation.VERTICAL) {
346                double x0 = value;
347                double x1 = value + getXOffset();
348                double y0 = dataArea.getMaxY();
349                double y1 = y0 - getYOffset();
350                double y2 = dataArea.getMinY();
351                line1 = new Line2D.Double(x0, y0, x1, y1);
352                line2 = new Line2D.Double(x1, y1, x1, y2);
353            }
354            g2.setPaint(plot.getDomainGridlinePaint());
355            g2.setStroke(plot.getDomainGridlineStroke());
356            g2.draw(line1);
357            g2.draw(line2);
358    
359        }
360    
361        /**
362         * Draws a grid line against the range axis.
363         *
364         * @param g2  the graphics device.
365         * @param plot  the plot.
366         * @param axis  the value axis.
367         * @param dataArea  the area for plotting data (not yet adjusted for any 
368         *                  3D effect).
369         * @param value  the value at which the grid line should be drawn.
370         *
371         */
372        public void drawRangeGridline(Graphics2D g2,
373                                      CategoryPlot plot,
374                                      ValueAxis axis,
375                                      Rectangle2D dataArea,
376                                      double value) {
377    
378            Range range = axis.getRange();
379    
380            if (!range.contains(value)) {
381                return;
382            }
383    
384            Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
385                    dataArea.getY() + getYOffset(),
386                    dataArea.getWidth() - getXOffset(),
387                    dataArea.getHeight() - getYOffset());
388    
389            Line2D line1 = null;
390            Line2D line2 = null;
391            PlotOrientation orientation = plot.getOrientation();
392            if (orientation == PlotOrientation.HORIZONTAL) {
393                double x0 = axis.valueToJava2D(value, adjusted, 
394                        plot.getRangeAxisEdge());
395                double x1 = x0 + getXOffset();
396                double y0 = dataArea.getMaxY();
397                double y1 = y0 - getYOffset();
398                double y2 = dataArea.getMinY();
399                line1 = new Line2D.Double(x0, y0, x1, y1);
400                line2 = new Line2D.Double(x1, y1, x1, y2);
401            }
402            else if (orientation == PlotOrientation.VERTICAL) {
403                double y0 = axis.valueToJava2D(value, adjusted,
404                        plot.getRangeAxisEdge());
405                double y1 = y0 - getYOffset();
406                double x0 = dataArea.getMinX();
407                double x1 = x0 + getXOffset();
408                double x2 = dataArea.getMaxX();
409                line1 = new Line2D.Double(x0, y0, x1, y1);
410                line2 = new Line2D.Double(x1, y1, x2, y1);
411            }
412            g2.setPaint(plot.getRangeGridlinePaint());
413            g2.setStroke(plot.getRangeGridlineStroke());
414            g2.draw(line1);
415            g2.draw(line2);
416    
417        }
418    
419        /**
420         * Draws a range marker.
421         *
422         * @param g2  the graphics device.
423         * @param plot  the plot.
424         * @param axis  the value axis.
425         * @param marker  the marker.
426         * @param dataArea  the area for plotting data (not including 3D effect).
427         */
428        public void drawRangeMarker(Graphics2D g2,
429                                    CategoryPlot plot,
430                                    ValueAxis axis,
431                                    Marker marker,
432                                    Rectangle2D dataArea) {
433    
434            if (marker instanceof ValueMarker) {
435                ValueMarker vm = (ValueMarker) marker;
436                double value = vm.getValue();
437                Range range = axis.getRange();
438                if (!range.contains(value)) {
439                    return;
440                }
441    
442                Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(), 
443                        dataArea.getY() + getYOffset(), 
444                        dataArea.getWidth() - getXOffset(), 
445                        dataArea.getHeight() - getYOffset());
446    
447                GeneralPath path = null;
448                PlotOrientation orientation = plot.getOrientation();
449                if (orientation == PlotOrientation.HORIZONTAL) {
450                    float x = (float) axis.valueToJava2D(value, adjusted, 
451                            plot.getRangeAxisEdge());
452                    float y = (float) adjusted.getMaxY();
453                    path = new GeneralPath();
454                    path.moveTo(x, y);
455                    path.lineTo((float) (x + getXOffset()), 
456                            y - (float) getYOffset());
457                    path.lineTo((float) (x + getXOffset()), 
458                            (float) (adjusted.getMinY() - getYOffset()));
459                    path.lineTo(x, (float) adjusted.getMinY());
460                    path.closePath();
461                }
462                else if (orientation == PlotOrientation.VERTICAL) {
463                    float y = (float) axis.valueToJava2D(value, adjusted, 
464                            plot.getRangeAxisEdge());
465                    float x = (float) dataArea.getX();
466                    path = new GeneralPath();
467                    path.moveTo(x, y);
468                    path.lineTo(x + (float) this.xOffset, y - (float) this.yOffset);
469                    path.lineTo((float) (adjusted.getMaxX() + this.xOffset), 
470                            y - (float) this.yOffset);
471                    path.lineTo((float) (adjusted.getMaxX()), y);
472                    path.closePath();
473                }
474                g2.setPaint(marker.getPaint());
475                g2.fill(path);
476                g2.setPaint(marker.getOutlinePaint());
477                g2.draw(path);
478            }
479        }
480       
481       /**
482         * Draw a single data item.
483         *
484         * @param g2  the graphics device.
485         * @param state  the renderer state.
486         * @param dataArea  the area in which the data is drawn.
487         * @param plot  the plot.
488         * @param domainAxis  the domain axis.
489         * @param rangeAxis  the range axis.
490         * @param dataset  the dataset.
491         * @param row  the row index (zero-based).
492         * @param column  the column index (zero-based).
493         * @param pass  the pass index.
494         */
495        public void drawItem(Graphics2D g2,
496                             CategoryItemRendererState state,
497                             Rectangle2D dataArea,
498                             CategoryPlot plot,
499                             CategoryAxis domainAxis,
500                             ValueAxis rangeAxis,
501                             CategoryDataset dataset,
502                             int row,
503                             int column,
504                             int pass) {
505    
506            if (!getItemVisible(row, column)) {
507                return;   
508            }
509            
510            // nothing is drawn for null...
511            Number v = dataset.getValue(row, column);
512            if (v == null) {
513                return;
514            }
515           
516            Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(),
517                    dataArea.getY() + getYOffset(), 
518                    dataArea.getWidth() - getXOffset(),
519                    dataArea.getHeight() - getYOffset());
520           
521            PlotOrientation orientation = plot.getOrientation();
522    
523            // current data point...
524            double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
525                    adjusted, plot.getDomainAxisEdge());
526            double value = v.doubleValue();
527            double y1 = rangeAxis.valueToJava2D(value, adjusted, 
528                    plot.getRangeAxisEdge());
529    
530            Shape shape = getItemShape(row, column);
531            if (orientation == PlotOrientation.HORIZONTAL) {
532                shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
533            }
534            else if (orientation == PlotOrientation.VERTICAL) {
535                shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
536            }
537           
538            if (getItemLineVisible(row, column)) {
539                if (column != 0) {
540    
541                    Number previousValue = dataset.getValue(row, column - 1);
542                    if (previousValue != null) {
543    
544                        // previous data point...
545                        double previous = previousValue.doubleValue();
546                        double x0 = domainAxis.getCategoryMiddle(column - 1, 
547                                getColumnCount(), adjusted, 
548                                plot.getDomainAxisEdge());
549                        double y0 = rangeAxis.valueToJava2D(previous, adjusted, 
550                                plot.getRangeAxisEdge());
551    
552                        double x2 = x0 + getXOffset();
553                        double y2 = y0 - getYOffset();
554                        double x3 = x1 + getXOffset();
555                        double y3 = y1 - getYOffset();
556                       
557                        GeneralPath clip = new GeneralPath();
558                       
559                        if (orientation == PlotOrientation.HORIZONTAL) {
560                            clip.moveTo((float) y0, (float) x0);
561                            clip.lineTo((float) y1, (float) x1);
562                            clip.lineTo((float) y3, (float) x3);
563                            clip.lineTo((float) y2, (float) x2);
564                            clip.lineTo((float) y0, (float) x0);
565                            clip.closePath();
566                        }
567                        else if (orientation == PlotOrientation.VERTICAL) {
568                            clip.moveTo((float) x0, (float) y0);
569                            clip.lineTo((float) x1, (float) y1);
570                            clip.lineTo((float) x3, (float) y3);
571                            clip.lineTo((float) x2, (float) y2);
572                            clip.lineTo((float) x0, (float) y0);
573                            clip.closePath();
574                        }
575                       
576                        g2.setPaint(getItemPaint(row, column));
577                        g2.fill(clip);
578                        g2.setStroke(getItemOutlineStroke(row, column));
579                        g2.setPaint(getItemOutlinePaint(row, column));
580                        g2.draw(clip);
581                    }
582                }
583            }
584    
585            // draw the item label if there is one...
586            if (isItemLabelVisible(row, column)) {
587                drawItemLabel(g2, orientation, dataset, row, column, x1, y1, 
588                        (value < 0.0));
589            }
590    
591            // add an item entity, if this information is being collected
592            EntityCollection entities = state.getEntityCollection();
593            if (entities != null) {
594                addItemEntity(entities, dataset, row, column, shape);
595            }
596    
597        }
598        
599        /**
600         * Checks this renderer for equality with an arbitrary object.
601         * 
602         * @param obj  the object (<code>null</code> permitted).
603         * 
604         * @return A boolean.
605         */
606        public boolean equals(Object obj) {
607            if (obj == this) {
608                return true;
609            }
610            if (!(obj instanceof LineRenderer3D)) {
611                return false;
612            }
613            LineRenderer3D that = (LineRenderer3D) obj;
614            if (this.xOffset != that.xOffset) {
615                return false;
616            }
617            if (this.yOffset != that.yOffset) {
618                return false;
619            }
620            if (!PaintUtilities.equal(this.wallPaint, that.wallPaint)) {
621                return false;
622            }
623            return super.equals(obj);
624        }
625        
626        /**
627         * Provides serialization support.
628         *
629         * @param stream  the output stream.
630         *
631         * @throws IOException  if there is an I/O error.
632         */
633        private void writeObject(ObjectOutputStream stream) throws IOException {
634            stream.defaultWriteObject();
635            SerialUtilities.writePaint(this.wallPaint, stream);
636        }
637    
638        /**
639         * Provides serialization support.
640         *
641         * @param stream  the input stream.
642         *
643         * @throws IOException  if there is an I/O error.
644         * @throws ClassNotFoundException  if there is a classpath problem.
645         */
646        private void readObject(ObjectInputStream stream)
647                throws IOException, ClassNotFoundException {
648            stream.defaultReadObject();
649            this.wallPaint = SerialUtilities.readPaint(stream);
650        }
651    
652    }