/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.swing; import java.awt.AlphaComposite; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.renderer.GTRenderer; import org.geotools.renderer.RenderListener; import org.opengis.feature.simple.SimpleFeature; /** * This class is used by {@code JMapPane} to handle the scheduling and running of * rendering tasks on a background thread. It functions as a single thread, non- * queueing executor, ie. only one rendering task can run at any given time and, * while it is running, any other submitted tasks will be rejected. * <p> * Whether a rendering task is accepted or rejected can be tested on submission: * <pre><code> * ReferencedEnvelope areaToDraw = ... * Graphics2D graphicsToDrawInto = ... * boolean accepted = renderingExecutor.submit(areaToDraw, graphicsToDrawInto); * </code></pre> * * The status of the executor can also be checked at any time like this: * <pre><code> * boolean busy = renderingExecutor.isRunning(); * </code></pre> * * While a rendering task is running it is regularly polled to see if it has completed * and, if so, whether it finished normally, was cancelled or failed. The interval between * polling can be adjusted which might be useful to tune the executor for particular * applications: * <pre><code> * RenderingExecutor re = new RenderingExecutor( mapPane ); * re.setPollingInterval( 150 ); // 150 milliseconds * </code></pre> * * @author Michael Bedward * @since 2.7 * @source $URL$ * @version $Id$ * * @see JMapPane */ public class RenderingExecutor { private final JMapPane mapPane; private final ExecutorService taskExecutor; private final ScheduledExecutorService watchExecutor; /** The default interval (milliseconds) for polling the result of a rendering task */ public static final long DEFAULT_POLLING_INTERVAL = 20L; private long pollingInterval; /* * This latch is used to avoid a race between the cancellation of * a current task and the submittal of a new task */ private CountDownLatch cancelLatch; /** * Constants to indicate the result of a rendering task */ public enum TaskResult { PENDING, COMPLETED, CANCELLED, FAILED; } private long numFeatures; /** * A rendering task */ private class Task implements Callable<TaskResult>, RenderListener { private final ReferencedEnvelope envelope; private final Rectangle paintArea; private final Graphics2D graphics; private boolean cancelled; private boolean failed; /** * Constructor. Creates a new rendering task * * @param envelope map area to render (world coordinates) * @param paintArea drawing area (image or display coordinates) * @param graphics graphics object used to draw into the image or display */ public Task(final ReferencedEnvelope envelope, final Rectangle paintArea, final Graphics2D graphics) { this.envelope = envelope; this.paintArea = paintArea; this.graphics = graphics; this.cancelled = false; failed = false; } /** * Called by the executor to run this rendering task. * * @return result of the task: completed, cancelled or failed * @throws Exception */ public TaskResult call() throws Exception { if (!cancelled) { GTRenderer renderer = mapPane.getRenderer(); try { renderer.addRenderListener(this); Composite composite = graphics.getComposite(); //graphics.setComposite(AlphaComposite.Src); //graphics.setBackground(Color.WHITE); graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f)); graphics.fill(paintArea); graphics.setComposite(composite); numFeatures = 0; renderer.paint(graphics, mapPane.getVisibleRect(), envelope, mapPane.getWorldToScreenTransform()); } finally { renderer.removeRenderListener(this); } } if (cancelled) { return TaskResult.CANCELLED; } else if (failed) { return TaskResult.FAILED; } else { return TaskResult.COMPLETED; } } /** * Cancel the rendering task if it is running. If called before * being run the task will be abandoned. */ public synchronized void cancel() { if (isRunning()) { cancelled = true; mapPane.getRenderer().stopRendering(); } } /** * Called by the renderer when each feature is drawn. * * @param feature the feature just drawn */ public void featureRenderer(SimpleFeature feature) { // @todo update a progress listener numFeatures++ ; } /** * Called by the renderer on error * * @param e cause of the error */ public void errorOccurred(Exception e) { failed = true; } } private AtomicBoolean taskRunning; private Task task; private Future<TaskResult> taskResult; private ScheduledFuture<?> watcher; /** * Constructor. Creates a new executor to service the specified map pane. * * @param mapPane the map pane to be serviced */ public RenderingExecutor(final JMapPane mapPane) { taskRunning = new AtomicBoolean(false); this.mapPane = mapPane; taskExecutor = Executors.newSingleThreadExecutor(); watchExecutor = Executors.newSingleThreadScheduledExecutor(); pollingInterval = DEFAULT_POLLING_INTERVAL; cancelLatch = new CountDownLatch(0); } /** * Get the interval for polling the result of a rendering task * * @return polling interval in milliseconds */ public long getPollingInterval() { return pollingInterval; } /** * Set the interval for polling the result of a rendering task * * @param interval interval in milliseconds (values {@code <=} 0 are ignored) */ public void setPollingInterval(long interval) { if (interval > 0) { pollingInterval = interval; } } /** * Submit a new rendering task. If no rendering task is presently running * this new task will be accepted; otherwise it will be rejected (ie. there * is no task queue). * * @param envelope the map area (world coordinates) to be rendered * @param graphics the graphics object to draw on * * @return true if the rendering task was accepted; false if it was * rejected */ public synchronized boolean submit(ReferencedEnvelope envelope, Rectangle paintArea, Graphics2D graphics) { if (!isRunning() || cancelLatch.getCount() > 0) { try { // wait for any cancelled task to finish its shutdown cancelLatch.await(); } catch (InterruptedException ex) { return false; } task = new Task(envelope, paintArea, graphics); taskRunning.set(true); taskResult = taskExecutor.submit(task); watcher = watchExecutor.scheduleAtFixedRate(new Runnable() { public void run() { pollTaskResult(); } }, DEFAULT_POLLING_INTERVAL, DEFAULT_POLLING_INTERVAL, TimeUnit.MILLISECONDS); return true; } return false; } /** * Cancel the current rendering task if one is running */ public synchronized void cancelTask() { if (isRunning()) { task.cancel(); cancelLatch = new CountDownLatch(1); } } private void pollTaskResult() { if (!taskResult.isDone()) { return; } TaskResult result = TaskResult.PENDING; try { result = taskResult.get(); } catch (Exception ex) { throw new IllegalStateException("When getting rendering result", ex); } watcher.cancel(false); taskRunning.set(false); /* * We zero the cancel latch here because it's possible that the job * completed (or failed) before it could be cancelled. When this statement * was only executed for the CANCELLED case (below) it led to * apps somtimes freezing. */ cancelLatch.countDown(); switch (result) { case CANCELLED: mapPane.onRenderingCancelled(); break; case COMPLETED: mapPane.onRenderingCompleted(); break; case FAILED: mapPane.onRenderingFailed(); break; } } public synchronized boolean isRunning() { return taskRunning.get(); } @Override protected void finalize() throws Throwable { if (this.isRunning()) { taskExecutor.shutdownNow(); watchExecutor.shutdownNow(); } } }