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