001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ------------------
028 * GanttRenderer.java
029 * ------------------
030 * (C) Copyright 2003-2008, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes
036 * -------
037 * 16-Sep-2003 : Version 1 (DG);
038 * 23-Sep-2003 : Fixed Checkstyle issues (DG);
039 * 21-Oct-2003 : Bar width moved into CategoryItemRendererState (DG);
040 * 03-Feb-2004 : Added get/set methods for attributes (DG);
041 * 12-Aug-2004 : Fixed rendering problem with maxBarWidth attribute (DG);
042 * 05-Nov-2004 : Modified drawItem() signature (DG);
043 * 20-Apr-2005 : Renamed CategoryLabelGenerator
044 *               --> CategoryItemLabelGenerator (DG);
045 * 01-Dec-2005 : Fix for bug 1369954, drawBarOutline flag ignored (DG);
046 * ------------- JFREECHART 1.0.x --------------------------------------------
047 * 17-Jan-2006 : Set includeBaseInRange flag to false (DG);
048 * 20-Mar-2007 : Implemented equals() and fixed serialization (DG);
049 * 24-Jun-2008 : Added new barPainter mechanism (DG);
050 * 26-Jun-2008 : Added crosshair support (DG);
051 *
052 */
053
054package org.jfree.chart.renderer.category;
055
056import java.awt.Color;
057import java.awt.Graphics2D;
058import java.awt.Paint;
059import java.awt.Stroke;
060import java.awt.geom.Rectangle2D;
061import java.io.IOException;
062import java.io.ObjectInputStream;
063import java.io.ObjectOutputStream;
064import java.io.Serializable;
065
066import org.jfree.chart.axis.CategoryAxis;
067import org.jfree.chart.axis.ValueAxis;
068import org.jfree.chart.entity.EntityCollection;
069import org.jfree.chart.event.RendererChangeEvent;
070import org.jfree.chart.labels.CategoryItemLabelGenerator;
071import org.jfree.chart.plot.CategoryPlot;
072import org.jfree.chart.plot.PlotOrientation;
073import org.jfree.data.category.CategoryDataset;
074import org.jfree.data.gantt.GanttCategoryDataset;
075import org.jfree.io.SerialUtilities;
076import org.jfree.ui.RectangleEdge;
077import org.jfree.util.PaintUtilities;
078
079/**
080 * A renderer for simple Gantt charts.  The example shown
081 * here is generated by the <code>GanttDemo1.java</code> program
082 * included in the JFreeChart Demo Collection:
083 * <br><br>
084 * <img src="../../../../../images/GanttRendererSample.png"
085 * alt="GanttRendererSample.png" />
086 */
087public class GanttRenderer extends IntervalBarRenderer
088        implements Serializable {
089
090    /** For serialization. */
091    private static final long serialVersionUID = -4010349116350119512L;
092
093    /** The paint for displaying the percentage complete. */
094    private transient Paint completePaint;
095
096    /** The paint for displaying the incomplete part of a task. */
097    private transient Paint incompletePaint;
098
099    /**
100     * Controls the starting edge of the progress indicator (expressed as a
101     * percentage of the overall bar width).
102     */
103    private double startPercent;
104
105    /**
106     * Controls the ending edge of the progress indicator (expressed as a
107     * percentage of the overall bar width).
108     */
109    private double endPercent;
110
111    /**
112     * Creates a new renderer.
113     */
114    public GanttRenderer() {
115        super();
116        setIncludeBaseInRange(false);
117        this.completePaint = Color.green;
118        this.incompletePaint = Color.red;
119        this.startPercent = 0.35;
120        this.endPercent = 0.65;
121    }
122
123    /**
124     * Returns the paint used to show the percentage complete.
125     *
126     * @return The paint (never <code>null</code>.
127     *
128     * @see #setCompletePaint(Paint)
129     */
130    public Paint getCompletePaint() {
131        return this.completePaint;
132    }
133
134    /**
135     * Sets the paint used to show the percentage complete and sends a
136     * {@link RendererChangeEvent} to all registered listeners.
137     *
138     * @param paint  the paint (<code>null</code> not permitted).
139     *
140     * @see #getCompletePaint()
141     */
142    public void setCompletePaint(Paint paint) {
143        if (paint == null) {
144            throw new IllegalArgumentException("Null 'paint' argument.");
145        }
146        this.completePaint = paint;
147        fireChangeEvent();
148    }
149
150    /**
151     * Returns the paint used to show the percentage incomplete.
152     *
153     * @return The paint (never <code>null</code>).
154     *
155     * @see #setCompletePaint(Paint)
156     */
157    public Paint getIncompletePaint() {
158        return this.incompletePaint;
159    }
160
161    /**
162     * Sets the paint used to show the percentage incomplete and sends a
163     * {@link RendererChangeEvent} to all registered listeners.
164     *
165     * @param paint  the paint (<code>null</code> not permitted).
166     *
167     * @see #getIncompletePaint()
168     */
169    public void setIncompletePaint(Paint paint) {
170        if (paint == null) {
171            throw new IllegalArgumentException("Null 'paint' argument.");
172        }
173        this.incompletePaint = paint;
174        fireChangeEvent();
175    }
176
177    /**
178     * Returns the position of the start of the progress indicator, as a
179     * percentage of the bar width.
180     *
181     * @return The start percent.
182     *
183     * @see #setStartPercent(double)
184     */
185    public double getStartPercent() {
186        return this.startPercent;
187    }
188
189    /**
190     * Sets the position of the start of the progress indicator, as a
191     * percentage of the bar width, and sends a {@link RendererChangeEvent} to
192     * all registered listeners.
193     *
194     * @param percent  the percent.
195     *
196     * @see #getStartPercent()
197     */
198    public void setStartPercent(double percent) {
199        this.startPercent = percent;
200        fireChangeEvent();
201    }
202
203    /**
204     * Returns the position of the end of the progress indicator, as a
205     * percentage of the bar width.
206     *
207     * @return The end percent.
208     *
209     * @see #setEndPercent(double)
210     */
211    public double getEndPercent() {
212        return this.endPercent;
213    }
214
215    /**
216     * Sets the position of the end of the progress indicator, as a percentage
217     * of the bar width, and sends a {@link RendererChangeEvent} to all
218     * registered listeners.
219     *
220     * @param percent  the percent.
221     *
222     * @see #getEndPercent()
223     */
224    public void setEndPercent(double percent) {
225        this.endPercent = percent;
226        fireChangeEvent();
227    }
228
229    /**
230     * Draws the bar for a single (series, category) data item.
231     *
232     * @param g2  the graphics device.
233     * @param state  the renderer state.
234     * @param dataArea  the data area.
235     * @param plot  the plot.
236     * @param domainAxis  the domain axis.
237     * @param rangeAxis  the range axis.
238     * @param dataset  the dataset.
239     * @param row  the row index (zero-based).
240     * @param column  the column index (zero-based).
241     * @param pass  the pass index.
242     */
243    public void drawItem(Graphics2D g2,
244                         CategoryItemRendererState state,
245                         Rectangle2D dataArea,
246                         CategoryPlot plot,
247                         CategoryAxis domainAxis,
248                         ValueAxis rangeAxis,
249                         CategoryDataset dataset,
250                         int row,
251                         int column,
252                         int pass) {
253
254         if (dataset instanceof GanttCategoryDataset) {
255             GanttCategoryDataset gcd = (GanttCategoryDataset) dataset;
256             drawTasks(g2, state, dataArea, plot, domainAxis, rangeAxis, gcd,
257                     row, column);
258         }
259         else {  // let the superclass handle it...
260             super.drawItem(g2, state, dataArea, plot, domainAxis, rangeAxis,
261                     dataset, row, column, pass);
262         }
263
264     }
265
266    /**
267     * Draws the tasks/subtasks for one item.
268     *
269     * @param g2  the graphics device.
270     * @param state  the renderer state.
271     * @param dataArea  the data plot area.
272     * @param plot  the plot.
273     * @param domainAxis  the domain axis.
274     * @param rangeAxis  the range axis.
275     * @param dataset  the data.
276     * @param row  the row index (zero-based).
277     * @param column  the column index (zero-based).
278     */
279    protected void drawTasks(Graphics2D g2,
280                             CategoryItemRendererState state,
281                             Rectangle2D dataArea,
282                             CategoryPlot plot,
283                             CategoryAxis domainAxis,
284                             ValueAxis rangeAxis,
285                             GanttCategoryDataset dataset,
286                             int row,
287                             int column) {
288
289        int count = dataset.getSubIntervalCount(row, column);
290        if (count == 0) {
291            drawTask(g2, state, dataArea, plot, domainAxis, rangeAxis,
292                    dataset, row, column);
293        }
294
295        PlotOrientation orientation = plot.getOrientation();
296        for (int subinterval = 0; subinterval < count; subinterval++) {
297
298            RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
299
300            // value 0
301            Number value0 = dataset.getStartValue(row, column, subinterval);
302            if (value0 == null) {
303                return;
304            }
305            double translatedValue0 = rangeAxis.valueToJava2D(
306                    value0.doubleValue(), dataArea, rangeAxisLocation);
307
308            // value 1
309            Number value1 = dataset.getEndValue(row, column, subinterval);
310            if (value1 == null) {
311                return;
312            }
313            double translatedValue1 = rangeAxis.valueToJava2D(
314                    value1.doubleValue(), dataArea, rangeAxisLocation);
315
316            if (translatedValue1 < translatedValue0) {
317                double temp = translatedValue1;
318                translatedValue1 = translatedValue0;
319                translatedValue0 = temp;
320            }
321
322            double rectStart = calculateBarW0(plot, plot.getOrientation(),
323                    dataArea, domainAxis, state, row, column);
324            double rectLength = Math.abs(translatedValue1 - translatedValue0);
325            double rectBreadth = state.getBarWidth();
326
327            // DRAW THE BARS...
328            Rectangle2D bar = null;
329            RectangleEdge barBase = null;
330            if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
331                bar = new Rectangle2D.Double(translatedValue0, rectStart,
332                        rectLength, rectBreadth);
333                barBase = RectangleEdge.LEFT;
334            }
335            else if (plot.getOrientation() == PlotOrientation.VERTICAL) {
336                bar = new Rectangle2D.Double(rectStart, translatedValue0,
337                        rectBreadth, rectLength);
338                barBase = RectangleEdge.BOTTOM;
339            }
340
341            Rectangle2D completeBar = null;
342            Rectangle2D incompleteBar = null;
343            Number percent = dataset.getPercentComplete(row, column,
344                    subinterval);
345            double start = getStartPercent();
346            double end = getEndPercent();
347            if (percent != null) {
348                double p = percent.doubleValue();
349                if (orientation == PlotOrientation.HORIZONTAL) {
350                    completeBar = new Rectangle2D.Double(translatedValue0,
351                            rectStart + start * rectBreadth, rectLength * p,
352                            rectBreadth * (end - start));
353                    incompleteBar = new Rectangle2D.Double(translatedValue0
354                            + rectLength * p, rectStart + start * rectBreadth,
355                            rectLength * (1 - p), rectBreadth * (end - start));
356                }
357                else if (orientation == PlotOrientation.VERTICAL) {
358                    completeBar = new Rectangle2D.Double(rectStart + start
359                            * rectBreadth, translatedValue0 + rectLength
360                            * (1 - p), rectBreadth * (end - start),
361                            rectLength * p);
362                    incompleteBar = new Rectangle2D.Double(rectStart + start
363                            * rectBreadth, translatedValue0, rectBreadth
364                            * (end - start), rectLength * (1 - p));
365                }
366
367            }
368
369            if (getShadowsVisible()) {
370                getBarPainter().paintBarShadow(g2, this, row, column, bar,
371                        barBase, true);
372            }
373            getBarPainter().paintBar(g2, this, row, column, bar, barBase);
374
375            if (completeBar != null) {
376                g2.setPaint(getCompletePaint());
377                g2.fill(completeBar);
378            }
379            if (incompleteBar != null) {
380                g2.setPaint(getIncompletePaint());
381                g2.fill(incompleteBar);
382            }
383            if (isDrawBarOutline()
384                    && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
385                g2.setStroke(getItemStroke(row, column));
386                g2.setPaint(getItemOutlinePaint(row, column));
387                g2.draw(bar);
388            }
389
390            if (subinterval == count - 1) {
391                // submit the current data point as a crosshair candidate
392                int datasetIndex = plot.indexOf(dataset);
393                Comparable columnKey = dataset.getColumnKey(column);
394                Comparable rowKey = dataset.getRowKey(row);
395                double xx = domainAxis.getCategorySeriesMiddle(columnKey,
396                        rowKey, dataset, getItemMargin(), dataArea,
397                        plot.getDomainAxisEdge());
398                updateCrosshairValues(state.getCrosshairState(),
399                        dataset.getRowKey(row), dataset.getColumnKey(column),
400                        value1.doubleValue(), datasetIndex, xx,
401                        translatedValue1, orientation);
402
403            }
404            // collect entity and tool tip information...
405            if (state.getInfo() != null) {
406                EntityCollection entities = state.getEntityCollection();
407                if (entities != null) {
408                    addItemEntity(entities, dataset, row, column, bar);
409                }
410            }
411        }
412    }
413
414    /**
415     * Draws a single task.
416     *
417     * @param g2  the graphics device.
418     * @param state  the renderer state.
419     * @param dataArea  the data plot area.
420     * @param plot  the plot.
421     * @param domainAxis  the domain axis.
422     * @param rangeAxis  the range axis.
423     * @param dataset  the data.
424     * @param row  the row index (zero-based).
425     * @param column  the column index (zero-based).
426     */
427    protected void drawTask(Graphics2D g2,
428                            CategoryItemRendererState state,
429                            Rectangle2D dataArea,
430                            CategoryPlot plot,
431                            CategoryAxis domainAxis,
432                            ValueAxis rangeAxis,
433                            GanttCategoryDataset dataset,
434                            int row,
435                            int column) {
436
437        PlotOrientation orientation = plot.getOrientation();
438        RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
439
440        // Y0
441        Number value0 = dataset.getEndValue(row, column);
442        if (value0 == null) {
443            return;
444        }
445        double java2dValue0 = rangeAxis.valueToJava2D(value0.doubleValue(),
446                dataArea, rangeAxisLocation);
447
448        // Y1
449        Number value1 = dataset.getStartValue(row, column);
450        if (value1 == null) {
451            return;
452        }
453        double java2dValue1 = rangeAxis.valueToJava2D(value1.doubleValue(),
454                dataArea, rangeAxisLocation);
455
456        if (java2dValue1 < java2dValue0) {
457            double temp = java2dValue1;
458            java2dValue1 = java2dValue0;
459            java2dValue0 = temp;
460            Number tempNum = value1;
461            value1 = value0;
462            value0 = tempNum;
463        }
464
465        double rectStart = calculateBarW0(plot, orientation, dataArea,
466                domainAxis, state, row, column);
467        double rectBreadth = state.getBarWidth();
468        double rectLength = Math.abs(java2dValue1 - java2dValue0);
469
470        Rectangle2D bar = null;
471        RectangleEdge barBase = null;
472        if (orientation == PlotOrientation.HORIZONTAL) {
473            bar = new Rectangle2D.Double(java2dValue0, rectStart, rectLength,
474                    rectBreadth);
475            barBase = RectangleEdge.LEFT;
476        }
477        else if (orientation == PlotOrientation.VERTICAL) {
478            bar = new Rectangle2D.Double(rectStart, java2dValue1, rectBreadth,
479                    rectLength);
480            barBase = RectangleEdge.BOTTOM;
481        }
482
483        Rectangle2D completeBar = null;
484        Rectangle2D incompleteBar = null;
485        Number percent = dataset.getPercentComplete(row, column);
486        double start = getStartPercent();
487        double end = getEndPercent();
488        if (percent != null) {
489            double p = percent.doubleValue();
490            if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
491                completeBar = new Rectangle2D.Double(java2dValue0,
492                        rectStart + start * rectBreadth, rectLength * p,
493                        rectBreadth * (end - start));
494                incompleteBar = new Rectangle2D.Double(java2dValue0
495                        + rectLength * p, rectStart + start * rectBreadth,
496                        rectLength * (1 - p), rectBreadth * (end - start));
497            }
498            else if (plot.getOrientation() == PlotOrientation.VERTICAL) {
499                completeBar = new Rectangle2D.Double(rectStart + start
500                        * rectBreadth, java2dValue1 + rectLength * (1 - p),
501                        rectBreadth * (end - start), rectLength * p);
502                incompleteBar = new Rectangle2D.Double(rectStart + start
503                        * rectBreadth, java2dValue1, rectBreadth * (end
504                        - start), rectLength * (1 - p));
505            }
506
507        }
508
509        if (getShadowsVisible()) {
510            getBarPainter().paintBarShadow(g2, this, row, column, bar,
511                    barBase, true);
512        }
513        getBarPainter().paintBar(g2, this, row, column, bar, barBase);
514
515        if (completeBar != null) {
516            g2.setPaint(getCompletePaint());
517            g2.fill(completeBar);
518        }
519        if (incompleteBar != null) {
520            g2.setPaint(getIncompletePaint());
521            g2.fill(incompleteBar);
522        }
523
524        // draw the outline...
525        if (isDrawBarOutline()
526                && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
527            Stroke stroke = getItemOutlineStroke(row, column);
528            Paint paint = getItemOutlinePaint(row, column);
529            if (stroke != null && paint != null) {
530                g2.setStroke(stroke);
531                g2.setPaint(paint);
532                g2.draw(bar);
533            }
534        }
535
536        CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
537                column);
538        if (generator != null && isItemLabelVisible(row, column)) {
539            drawItemLabel(g2, dataset, row, column, plot, generator, bar,
540                    false);
541        }
542
543        // submit the current data point as a crosshair candidate
544        int datasetIndex = plot.indexOf(dataset);
545        Comparable columnKey = dataset.getColumnKey(column);
546        Comparable rowKey = dataset.getRowKey(row);
547        double xx = domainAxis.getCategorySeriesMiddle(columnKey, rowKey,
548                dataset, getItemMargin(), dataArea, plot.getDomainAxisEdge());
549        updateCrosshairValues(state.getCrosshairState(),
550                dataset.getRowKey(row), dataset.getColumnKey(column),
551                value1.doubleValue(), datasetIndex, xx, java2dValue1,
552                orientation);
553
554        // collect entity and tool tip information...
555        EntityCollection entities = state.getEntityCollection();
556        if (entities != null) {
557            addItemEntity(entities, dataset, row, column, bar);
558        }
559    }
560
561    /**
562     * Returns the Java2D coordinate for the middle of the specified data item.
563     *
564     * @param rowKey  the row key.
565     * @param columnKey  the column key.
566     * @param dataset  the dataset.
567     * @param axis  the axis.
568     * @param area  the drawing area.
569     * @param edge  the edge along which the axis lies.
570     *
571     * @return The Java2D coordinate.
572     *
573     * @since 1.0.11
574     */
575    public double getItemMiddle(Comparable rowKey, Comparable columnKey,
576            CategoryDataset dataset, CategoryAxis axis, Rectangle2D area,
577            RectangleEdge edge) {
578        return axis.getCategorySeriesMiddle(columnKey, rowKey, dataset,
579                getItemMargin(), area, edge);
580    }
581
582    /**
583     * Tests this renderer for equality with an arbitrary object.
584     *
585     * @param obj  the object (<code>null</code> permitted).
586     *
587     * @return A boolean.
588     */
589    public boolean equals(Object obj) {
590        if (obj == this) {
591            return true;
592        }
593        if (!(obj instanceof GanttRenderer)) {
594            return false;
595        }
596        GanttRenderer that = (GanttRenderer) obj;
597        if (!PaintUtilities.equal(this.completePaint, that.completePaint)) {
598            return false;
599        }
600        if (!PaintUtilities.equal(this.incompletePaint, that.incompletePaint)) {
601            return false;
602        }
603        if (this.startPercent != that.startPercent) {
604            return false;
605        }
606        if (this.endPercent != that.endPercent) {
607            return false;
608        }
609        return super.equals(obj);
610    }
611
612    /**
613     * Provides serialization support.
614     *
615     * @param stream  the output stream.
616     *
617     * @throws IOException  if there is an I/O error.
618     */
619    private void writeObject(ObjectOutputStream stream) throws IOException {
620        stream.defaultWriteObject();
621        SerialUtilities.writePaint(this.completePaint, stream);
622        SerialUtilities.writePaint(this.incompletePaint, stream);
623    }
624
625    /**
626     * Provides serialization support.
627     *
628     * @param stream  the input stream.
629     *
630     * @throws IOException  if there is an I/O error.
631     * @throws ClassNotFoundException  if there is a classpath problem.
632     */
633    private void readObject(ObjectInputStream stream)
634        throws IOException, ClassNotFoundException {
635        stream.defaultReadObject();
636        this.completePaint = SerialUtilities.readPaint(stream);
637        this.incompletePaint = SerialUtilities.readPaint(stream);
638    }
639
640}