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