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