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