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 * OfflineRenderingChartPanel.java
029 * -------------------------------
030 * (C) Copyright 2000-present, by Yuri Blankenstein and Contributors.
031 *
032 * Original Author:  Yuri Blankenstein;
033 */
034
035package org.jfree.chart;
036
037import java.awt.AlphaComposite;
038import java.awt.Container;
039import java.awt.Cursor;
040import java.awt.Dimension;
041import java.awt.Graphics2D;
042import java.awt.GraphicsConfiguration;
043import java.awt.Rectangle;
044import java.awt.Transparency;
045import java.awt.geom.Point2D;
046import java.awt.image.BufferedImage;
047
048import javax.swing.SwingWorker;
049
050import org.jfree.chart.entity.EntityCollection;
051import org.jfree.chart.plot.PlotRenderingInfo;
052
053/**
054 * A {@link ChartPanel} that applies offline rendering, for better performance
055 * when navigating (i.e. panning / zooming) {@link JFreeChart charts} with lots
056 * of data.
057 * <P>
058 * This chart panel uses a {@link SwingWorker} to perform the actual
059 * {@link JFreeChart} rendering. While rendering, a {@link Cursor#WAIT_CURSOR
060 * wait cursor} is visible and the current buffered image of the chart will be
061 * scaled and drawn to the screen. When - while rendering - another
062 * {@link #setRefreshBuffer(boolean) refresh} is requested, this will be either
063 * postponed until the current rendering is done or ignored when another refresh
064 * is requested.
065 */
066public class OfflineRenderingChartPanel extends ChartPanel {
067    private static final long serialVersionUID = -724633596883320084L;
068
069    /**
070     * Using enum state pattern to control the 'offline' rendering
071     */
072    protected enum State {
073        IDLE {
074            @Override
075            protected State renderOffline(OfflineRenderingChartPanel panel,
076                    OfflineChartRenderer renderer) {
077                // Start rendering offline
078                renderer.execute();
079                panel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
080                return RENDERING;
081            }
082
083            @Override
084            protected State offlineRenderingDone(
085                    OfflineRenderingChartPanel panel,
086                    OfflineChartRenderer renderer) {
087                throw new IllegalStateException(
088                        "offlineRenderingDone not expected in IDLE state");
089            }
090        },
091        RENDERING {
092            @Override
093            protected State renderOffline(OfflineRenderingChartPanel panel,
094                    OfflineChartRenderer renderer) {
095                // We're already rendering, we'll start this renderer when we're
096                // finished. If another rendering is requested, this one will be
097                // ignored, see RE_RENDERING_PENDING. This gains a lot of speed
098                // as not all requested (intermediate) renderings are executed
099                // for large plots.
100                panel.pendingOfflineRenderer = renderer;
101                return RE_RENDERING_PENDING;
102            }
103
104            @Override
105            protected State offlineRenderingDone(
106                    OfflineRenderingChartPanel panel,
107                    OfflineChartRenderer renderer) {
108                // Offline rendering done, prepare the buffer and info for the
109                // next repaint and request it.
110                panel.currentChartBuffer = renderer.buffer;
111                panel.currentChartRenderingInfo = renderer.info;
112                panel.repaint();
113                panel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
114                return IDLE;
115            }
116        },
117        RE_RENDERING_PENDING {
118            @Override
119            protected State renderOffline(OfflineRenderingChartPanel panel,
120                    OfflineChartRenderer renderer) {
121                // We're already rendering, we'll start this renderer when we're
122                // finished.
123                panel.pendingOfflineRenderer = renderer;
124                return RE_RENDERING_PENDING;
125            }
126
127            @Override
128            protected State offlineRenderingDone(
129                    OfflineRenderingChartPanel panel,
130                    OfflineChartRenderer renderer) {
131                // Store the intermediate result, but do not actively repaint
132                // as this could trigger another RE_RENDERING_PENDING if i.e.
133                // the buffer-image-size of the pending renderer differs from
134                // the current buffer-image-size.
135                panel.currentChartBuffer = renderer.buffer;
136                panel.currentChartRenderingInfo = renderer.info;
137                // Immediately start rendering again to update the chart to the
138                // latest requested state.
139                panel.pendingOfflineRenderer.execute();
140                panel.pendingOfflineRenderer = null;
141                return RENDERING;
142            }
143        };
144
145        protected abstract State renderOffline(
146                final OfflineRenderingChartPanel panel,
147                final OfflineChartRenderer renderer);
148
149        protected abstract State offlineRenderingDone(
150                final OfflineRenderingChartPanel panel,
151                final OfflineChartRenderer renderer);
152    }
153
154    /** A buffer for the rendered chart. */
155    private transient BufferedImage currentChartBuffer = null;
156    private transient ChartRenderingInfo currentChartRenderingInfo = null;
157
158    /** A pending rendering for the chart. */
159    private transient OfflineChartRenderer pendingOfflineRenderer = null;
160
161    private State state = State.IDLE;
162
163    /**
164     * Constructs a double buffered JFreeChart panel that displays the specified
165     * chart.
166     *
167     * @param chart the chart.
168     */
169    public OfflineRenderingChartPanel(JFreeChart chart) {
170        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MINIMUM_DRAW_WIDTH,
171                DEFAULT_MINIMUM_DRAW_HEIGHT, DEFAULT_MAXIMUM_DRAW_WIDTH,
172                DEFAULT_MAXIMUM_DRAW_HEIGHT, true, // properties
173                true, // save
174                true, // print
175                true, // zoom
176                true // tooltips
177        );
178
179    }
180
181    /**
182     * Constructs a double buffered JFreeChart panel.
183     *
184     * @param chart      the chart.
185     * @param properties a flag indicating whether or not the chart property
186     *                   editor should be available via the popup menu.
187     * @param save       a flag indicating whether or not save options should be
188     *                   available via the popup menu.
189     * @param print      a flag indicating whether or not the print option
190     *                   should be available via the popup menu.
191     * @param zoom       a flag indicating whether or not zoom options should be
192     *                   added to the popup menu.
193     * @param tooltips   a flag indicating whether or not tooltips should be
194     *                   enabled for the chart.
195     */
196    public OfflineRenderingChartPanel(JFreeChart chart, boolean properties,
197            boolean save, boolean print, boolean zoom, boolean tooltips) {
198
199        this(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_MINIMUM_DRAW_WIDTH,
200                DEFAULT_MINIMUM_DRAW_HEIGHT, DEFAULT_MAXIMUM_DRAW_WIDTH,
201                DEFAULT_MAXIMUM_DRAW_HEIGHT, properties, save, print, zoom,
202                tooltips);
203
204    }
205
206    /**
207     * Constructs a double buffered JFreeChart panel.
208     *
209     * @param chart             the chart.
210     * @param width             the preferred width of the panel.
211     * @param height            the preferred height of the panel.
212     * @param minimumDrawWidth  the minimum drawing width.
213     * @param minimumDrawHeight the minimum drawing height.
214     * @param maximumDrawWidth  the maximum drawing width.
215     * @param maximumDrawHeight the maximum drawing height.
216     * @param properties        a flag indicating whether or not the chart
217     *                          property editor should be available via the
218     *                          popup menu.
219     * @param save              a flag indicating whether or not save options
220     *                          should be available via the popup menu.
221     * @param print             a flag indicating whether or not the print
222     *                          option should be available via the popup menu.
223     * @param zoom              a flag indicating whether or not zoom options
224     *                          should be added to the popup menu.
225     * @param tooltips          a flag indicating whether or not tooltips should
226     *                          be enabled for the chart.
227     */
228    public OfflineRenderingChartPanel(JFreeChart chart, int width, int height,
229            int minimumDrawWidth, int minimumDrawHeight, int maximumDrawWidth,
230            int maximumDrawHeight, boolean properties, boolean save,
231            boolean print, boolean zoom, boolean tooltips) {
232
233        this(chart, width, height, minimumDrawWidth, minimumDrawHeight,
234                maximumDrawWidth, maximumDrawHeight, properties, true, save,
235                print, zoom, tooltips);
236    }
237
238    /**
239     * Constructs a double buffered JFreeChart panel.
240     *
241     * @param chart             the chart.
242     * @param width             the preferred width of the panel.
243     * @param height            the preferred height of the panel.
244     * @param minimumDrawWidth  the minimum drawing width.
245     * @param minimumDrawHeight the minimum drawing height.
246     * @param maximumDrawWidth  the maximum drawing width.
247     * @param maximumDrawHeight the maximum drawing height.
248     * @param properties        a flag indicating whether or not the chart
249     *                          property editor should be available via the
250     *                          popup menu.
251     * @param copy              a flag indicating whether or not a copy option
252     *                          should be available via the popup menu.
253     * @param save              a flag indicating whether or not save options
254     *                          should be available via the popup menu.
255     * @param print             a flag indicating whether or not the print
256     *                          option should be available via the popup menu.
257     * @param zoom              a flag indicating whether or not zoom options
258     *                          should be added to the popup menu.
259     * @param tooltips          a flag indicating whether or not tooltips should
260     *                          be enabled for the chart.
261     */
262    public OfflineRenderingChartPanel(JFreeChart chart, int width, int height,
263            int minimumDrawWidth, int minimumDrawHeight, int maximumDrawWidth,
264            int maximumDrawHeight, boolean properties, boolean copy,
265            boolean save, boolean print, boolean zoom, boolean tooltips) {
266        super(chart, width, height, minimumDrawWidth, minimumDrawHeight,
267                maximumDrawWidth, maximumDrawHeight, true, properties, copy,
268                save, print, zoom, tooltips);
269    }
270
271    @Override
272    protected BufferedImage paintChartToBuffer(Graphics2D g2,
273            Dimension bufferSize, Dimension chartSize, Point2D anchor,
274            ChartRenderingInfo info) {
275        synchronized (state) {
276            if (this.currentChartBuffer == null) {
277                // Rendering the first time, prepare an empty buffer and
278                // start rendering, no need for an additional state
279                this.currentChartBuffer = createChartBuffer(g2, bufferSize);
280                clearChartBuffer(currentChartBuffer);
281                setRefreshBuffer(true);
282            } else if ((this.currentChartBuffer.getWidth() != bufferSize.width)
283                    || (this.currentChartBuffer
284                            .getHeight() != bufferSize.height)) {
285                setRefreshBuffer(true);
286            }
287
288            // do we need to redraw the buffer?
289            if (getRefreshBuffer()) {
290                setRefreshBuffer(false); // clear the flag
291
292                // Rendering is done offline, hence it requires a fresh buffer
293                // and rendering info
294                BufferedImage rendererBuffer = createChartBuffer(g2,
295                        bufferSize);
296                ChartRenderingInfo rendererInfo = info;
297                if (rendererInfo != null) {
298                    // As the chart will be re-rendered, the current chart
299                    // entities cannot be trusted
300                    final EntityCollection entityCollection = 
301                            rendererInfo.getEntityCollection();
302                    if (entityCollection != null) {
303                        entityCollection.clear();
304                    }
305
306                    // Offline rendering requires its own instance of
307                    // ChartRenderingInfo, using clone if possible
308                    try {
309                        rendererInfo = rendererInfo.clone();
310                    } catch (CloneNotSupportedException e) {
311                        // Not expected
312                        e.printStackTrace();
313                        rendererInfo = new ChartRenderingInfo();
314                    }
315                }
316
317                OfflineChartRenderer offlineRenderer = new OfflineChartRenderer(
318                        getChart(), rendererBuffer, chartSize, anchor,
319                        rendererInfo);
320                state = state.renderOffline(this, offlineRenderer);
321            }
322
323            // Copy the rendered ChartRenderingInfo into the passed info
324            // argument and mark that we have done so.
325            copyChartRenderingInfo(this.currentChartRenderingInfo, info);
326            this.currentChartRenderingInfo = info;
327
328            return this.currentChartBuffer;
329        }
330    }
331
332    private class OfflineChartRenderer extends SwingWorker<Object, Object> {
333        private final JFreeChart chart;
334        private final BufferedImage buffer;
335        private final Dimension chartSize;
336        private final Point2D anchor;
337        private final ChartRenderingInfo info;
338
339        public OfflineChartRenderer(JFreeChart chart, BufferedImage image,
340                Dimension chartSize, Point2D anchor, ChartRenderingInfo info) {
341            this.chart = chart;
342            this.buffer = image;
343            this.chartSize = chartSize;
344            this.anchor = anchor;
345            this.info = info;
346        }
347
348        @Override
349        protected Object doInBackground() throws Exception {
350            clearChartBuffer(buffer);
351
352            Graphics2D bufferG2 = buffer.createGraphics();
353            if ((this.buffer.getWidth() != this.chartSize.width)
354                    || (this.buffer.getHeight() != this.chartSize.height)) {
355                // Scale the chart to fit the buffer
356                bufferG2.scale(
357                        this.buffer.getWidth() / this.chartSize.getWidth(),
358                        this.buffer.getHeight() / this.chartSize.getHeight());
359            }
360            Rectangle chartArea = new Rectangle(this.chartSize);
361
362            this.chart.draw(bufferG2, chartArea, this.anchor, this.info);
363            bufferG2.dispose();
364
365            // Return type is not used
366            return null;
367        }
368
369        @Override
370        protected void done() {
371            synchronized (state) {
372                state = state.offlineRenderingDone(
373                        OfflineRenderingChartPanel.this, this);
374            }
375        }
376    }
377
378    @Override
379    public void setCursor(Cursor cursor) {
380        super.setCursor(cursor);
381
382        // Buggy mouse cursor: setting both behaves as expected
383        Container root = getTopLevelAncestor();
384        if (null != root) {
385            root.setCursor(cursor);
386        }
387    }
388
389    private static void copyChartRenderingInfo(ChartRenderingInfo source,
390            ChartRenderingInfo target) {
391        if (source == null || target == null || source == target) {
392            // Nothing to do
393            return;
394        }
395        target.clear();
396        target.setChartArea(source.getChartArea());
397        target.setEntityCollection(source.getEntityCollection());
398        copyPlotRenderingInfo(source.getPlotInfo(), target.getPlotInfo());
399    }
400
401    private static void copyPlotRenderingInfo(PlotRenderingInfo source,
402            PlotRenderingInfo target) {
403        target.setDataArea(source.getDataArea());
404        target.setPlotArea(source.getPlotArea());
405        for (int i = 0; i < target.getSubplotCount(); i++) {
406            PlotRenderingInfo subSource = source.getSubplotInfo(i);
407            PlotRenderingInfo subTarget = new PlotRenderingInfo(
408                    target.getOwner());
409            copyPlotRenderingInfo(subSource, subTarget);
410            target.addSubplotInfo(subTarget);
411        }
412    }
413
414    private static BufferedImage createChartBuffer(Graphics2D g2,
415            Dimension bufferSize) {
416        GraphicsConfiguration gc = g2.getDeviceConfiguration();
417        return gc.createCompatibleImage(bufferSize.width, bufferSize.height,
418                Transparency.TRANSLUCENT);
419    }
420
421    private static void clearChartBuffer(BufferedImage buffer) {
422        Graphics2D bufferG2 = buffer.createGraphics();
423        // make the background of the buffer clear and transparent
424        bufferG2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
425        bufferG2.fill(new Rectangle(buffer.getWidth(), buffer.getHeight()));
426        bufferG2.dispose();
427    }
428}