/* * 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.List; import java.util.concurrent.CancellationException; import java.util.concurrent.CopyOnWriteArrayList; 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.AtomicLong; import org.geotools.map.MapContent; import org.geotools.map.MapViewport; import org.geotools.renderer.GTRenderer; /** * The default implementation of {@code RenderingExecutor} which is used by * {@linkplain JMapPane} and {@linkplain JLayeredMapPane}. It runs no more than * one rendering task at any given time, although that task may involve multiple * threads (e.g. each layer of a map being rendered into separate destinations. * While a task is running any other submitted tasks are 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> * executor.setPollingInterval( 10 ); // 10 milliseconds * </code></pre> * * @author Michael Bedward * @since 2.7 * * @source $URL$ * @version $Id$ * * @see RenderingExecutorListener */ public class DefaultRenderingExecutor implements RenderingExecutor { private final AtomicLong NEXT_ID = new AtomicLong(1); private final ExecutorService taskExecutor; private final ScheduledExecutorService watchExecutor; private ScheduledFuture<?> watcher; private CountDownLatch tasksLatch = new CountDownLatch(0); /** The default interval (milliseconds) for polling the result of a rendering task */ public static final long DEFAULT_POLLING_INTERVAL = 20L; private long pollingInterval; private static class DaemonThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } } private static class TaskInfo { final long id; final RenderingTask task; final MapContent mapContent; final Future<Boolean> future; final RenderingExecutorListener listener; boolean polledDone; TaskInfo(long id, RenderingTask task, MapContent mapContent, Future<Boolean> future, RenderingExecutorListener listener) { this.id = id; this.task = task; this.mapContent = mapContent; this.future = future; this.listener = listener; this.polledDone = false; } } private List<TaskInfo> currentTasks; /** * Creates a new executor. */ public DefaultRenderingExecutor() { currentTasks = new CopyOnWriteArrayList<TaskInfo>(); taskExecutor = Executors.newCachedThreadPool(); pollingInterval = DEFAULT_POLLING_INTERVAL; watchExecutor = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory()); startPolling(); } /** * {@inheritDoc} */ @Override public long getPollingInterval() { return pollingInterval; } /** * {@inheritDoc} */ @Override 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). */ @Override 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 (tasksLatch.getCount() == 0) { tasksLatch = new CountDownLatch(1); long id = NEXT_ID.getAndIncrement(); RenderingExecutorEvent event = new RenderingExecutorEvent(this, id); listener.onRenderingStarted(event); RenderingTask task = new RenderingTask(mapContent, graphics, renderer); Future<Boolean> future = taskExecutor.submit(task); currentTasks.add( new TaskInfo(id, task, mapContent, future, listener) ); rtnValue = id; } return rtnValue; } @Override public long submit(MapContent mapContent, List<RenderingOperands> operands, 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 (mapContent.getViewport().isEmpty()) { throw new IllegalArgumentException("The viewport must not be empty"); } if (operands == null || operands.isEmpty()) { throw new IllegalArgumentException("operands list must not be null or empty"); } if (listener == null) { throw new IllegalArgumentException("listener must not be null"); } if (tasksLatch.getCount() == 0) { tasksLatch = new CountDownLatch(operands.size()); long id = NEXT_ID.getAndIncrement(); RenderingExecutorEvent event = new RenderingExecutorEvent(this, id); listener.onRenderingStarted(event); // Clone the viewport and mark it as not editable to prevent // the temporary MapContents created below from changing it MapViewport vp = new MapViewport(mapContent.getViewport()); vp.setEditable(false); for (RenderingOperands op : operands) { MapContent mc = new SingleLayerMapContent(op.getLayer()); mc.setViewport(vp); op.getRenderer().setMapContent(mc); RenderingTask task = new RenderingTask(mapContent, op.getGraphics(), op.getRenderer()); Future<Boolean> future = taskExecutor.submit(task); currentTasks.add( new TaskInfo(id, task, mc, future, listener) ); } rtnValue = id; } return rtnValue; } /** * {@inheritDoc} */ @Override public synchronized void cancel(long taskId) { if (!currentTasks.isEmpty() && currentTasks.get(0).id == taskId) { cancelAll(); } } /** * {@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. */ @Override public synchronized void cancelAll() { for (TaskInfo info : currentTasks) { info.task.cancel(); } } /** * {@inheritDoc} */ @Override public void shutdown() { if (taskExecutor != null && !taskExecutor.isShutdown()) { taskExecutor.shutdown(); watchExecutor.shutdown(); } } /** * {@inheritDoc} */ @Override public boolean isShutdown() { return taskExecutor.isShutdown(); } private void pollTaskResult() { for (TaskInfo info : currentTasks) { if (!info.polledDone && info.future.isDone()) { info.polledDone = true; Boolean result = null; try { result = info.future.get(); } catch (CancellationException ex) { result = false; } catch (Exception ex) { throw new IllegalStateException("When getting rendering result", ex); } RenderingExecutorEvent event = new RenderingExecutorEvent(this, info.id); if (!result) { info.listener.onRenderingFailed(event); } else { tasksLatch.countDown(); if (tasksLatch.getCount() == 0) { info.listener.onRenderingCompleted(event); currentTasks.clear(); break; } } } } } private void startPolling() { watcher = watchExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { pollTaskResult(); } }, pollingInterval, pollingInterval, TimeUnit.MILLISECONDS); } private void restartPolling() { stopPolling(); startPolling(); } private void stopPolling() { if (watcher != null && !watcher.isDone()) { watcher.cancel(false); } } }