/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2008-2011, 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.Graphics2D; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.geotools.map.MapContent; import org.geotools.renderer.GTRenderer; /** * A single threaded, non-queueing {@code RenderingExecutor}. 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> * taskId = executor.submit(areaToDraw, graphicsToDrawInto); * if (taskId == RenderingExecutor.TASK_REJECTED) { * ... * } * </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( 10 ); // 10 milliseconds * </code></pre> * * @author Michael Bedward * @since 2.7 * @source $URL$ * @version $Id$ * * @see RenderingExecutorListener */ public class SingleTaskRenderingExecutor implements 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; private static class DaemonThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } } private RenderingTask task; private Future<RenderingTask.Status> taskFuture; private RenderingExecutorListener listener; private ScheduledFuture<?> watcher; private AtomicBoolean notifiedStart; /** * Creates a new executor. */ public SingleTaskRenderingExecutor() { taskExecutor = Executors.newSingleThreadExecutor(); pollingInterval = DEFAULT_POLLING_INTERVAL; cancelLatch = new CountDownLatch(0); notifiedStart = new AtomicBoolean(); watchExecutor = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory()); startPolling(); } /** * {@inheritDoc} */ public long getPollingInterval() { return pollingInterval; } /** * {@inheritDoc} */ public void setPollingInterval(long interval) { if (interval > 0 && interval != pollingInterval) { pollingInterval = interval; restartPolling(); } } /** * {@inheritDoc} * If no rendering task is presently running this new task will be accepted, * otherwise it will be rejected (ie. there is no task queue). */ public synchronized long submit(MapContent mapContent, GTRenderer renderer, Graphics2D graphics, RenderingExecutorListener listener) { long rtnValue = RenderingExecutor.TASK_REJECTED; if (taskExecutor.isShutdown()) { throw new IllegalStateException("Calling submit after the executor has been shutdown"); } if (mapContent == null) { throw new IllegalArgumentException("mapContent must not be null"); } if (graphics == null) { throw new IllegalArgumentException("graphics must not be null"); } if (mapContent.getViewport().isEmpty()) { throw new IllegalArgumentException("The viewport must not be empty"); } if (listener == null) { throw new IllegalArgumentException("listener must not be null"); } if (!hasUnfinishedTask()) { try { // wait for any cancelled task to finish its shutdown cancelLatch.await(); } catch (InterruptedException ex) { throw new RuntimeException(ex); } task = new RenderingTask(mapContent, renderer, graphics); this.listener = listener; notifiedStart.set(false); taskFuture = taskExecutor.submit(task); rtnValue = task.getId(); } return rtnValue; } /** * {@inheritDoc} * Since this task can only ever have a single task running, * and no tasks queued, this method simply checks if the running * task has the specified ID value and, if so, cancels it. */ public synchronized void cancel(long taskId) { if (hasUnfinishedTask(taskId)) { task.cancel(); cancelLatch = new CountDownLatch(1); } } /** * {@inheritDoc} * Since this task can only ever have a single task running, and * no tasks queued, this method simply checks for a running task * and, if one exists, cancels it. */ public synchronized void cancelAll() { if (hasUnfinishedTask()) { task.cancel(); cancelLatch = new CountDownLatch(1); } } /** * {@inheritDoc} */ public void shutdown() { if (taskExecutor != null && !taskExecutor.isShutdown()) { taskExecutor.shutdown(); watchExecutor.shutdown(); } } /** * {@inheritDoc} */ public boolean isShutdown() { return taskExecutor.isShutdown(); } /** * Checks if the executor is holding a specific task that is either running * or yet to start running. * * @param taskId the task ID value * * @return {@code true} if the given task exists and is unfinished */ private boolean hasUnfinishedTask(long taskId) { return (task != null && task.getId() == taskId && taskFuture != null && !taskFuture.isDone()); } /** * Checks if the executor is holding a task that is either running * or yet to start running. * * @return {@code true} if a task exists and is unfinished */ private boolean hasUnfinishedTask() { return (task != null && taskFuture != null && !taskFuture.isDone()); } private void notifyStarted(boolean force) { if (!notifiedStart.get() && (force || task.isRunning())) { RenderingExecutorEvent event = new RenderingExecutorEvent(this, task.getId()); listener.onRenderingStarted(event); notifiedStart.set(true); } } private void pollTaskResult() { if (taskFuture == null) { return; } else if (!taskFuture.isDone()) { notifyStarted(false); return; } // call again in case the task was so quick we missed the start notifyStarted(true); RenderingTask.Status result = null; long taskId = task.getId(); try { result = taskFuture.get(); } catch (Exception ex) { throw new IllegalStateException("When getting rendering result", ex); } taskFuture = null; /* * 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 caused apps * to freeze occasionally. */ cancelLatch.countDown(); RenderingExecutorEvent event = new RenderingExecutorEvent(this, taskId); switch (result) { case CANCELLED: listener.onRenderingCancelled(event); break; case COMPLETED: listener.onRenderingCompleted(event); break; case FAILED: listener.onRenderingFailed(event); break; } } private void startPolling() { watcher = watchExecutor.scheduleAtFixedRate(new Runnable() { public void run() { pollTaskResult(); } }, pollingInterval, pollingInterval, TimeUnit.MILLISECONDS); } private void restartPolling() { stopPolling(); startPolling(); } private void stopPolling() { if (watcher != null && !watcher.isDone()) { watcher.cancel(false); } } }