/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 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.Color;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyBoundsAdapter;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.MapContent;
import org.geotools.map.Layer;
import org.geotools.map.MapViewport;
import org.geotools.map.event.MapBoundsEvent;
import org.geotools.map.event.MapBoundsListener;
import org.geotools.map.event.MapLayerEvent;
import org.geotools.map.event.MapLayerListEvent;
import org.geotools.map.event.MapLayerListListener;
import org.geotools.renderer.lite.LabelCache;
import org.geotools.swing.event.DefaultMapMouseEventDispatcher;
import org.geotools.swing.event.MapMouseEventDispatcher;
import org.geotools.swing.event.MapMouseListener;
import org.geotools.swing.event.MapPaneEvent;
import org.geotools.swing.event.MapPaneKeyHandler;
import org.geotools.swing.event.MapPaneListener;
import org.geotools.swing.tool.CursorTool;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* Base class for Swing map panes. It extends Swing's {@code JPanel} class and
* handles window sizing and repainting as well as redirecting mouse events.
* It also provides basic implementations of all interface methods. Sub-classes
* must implement {@linkplain #drawLayers(boolean)} and override
* {@linkplain JPanel#paintComponent(java.awt.Graphics)}.
*
* @author Michael Bedward
* @since 8.0
*
* @source $URL$
* @version $Id$
*/
public abstract class AbstractMapPane extends JPanel
implements MapPane, RenderingExecutorListener, MapLayerListListener, MapBoundsListener {
/**
* Default delay (500 milliseconds) before the map will be redrawn when
* resizing the pane or moving the displayed image. This avoids flickering
* and redundant rendering.
*/
public static final int DEFAULT_PAINT_DELAY = 500;
/**
* Default background color (white).
*/
public static final Color DEFAULT_BACKGROUND_COLOR = Color.WHITE;
protected final ScheduledExecutorService paneTaskExecutor;
protected Future<?> resizedFuture;
protected int paintDelay;
protected final AtomicBoolean acceptRepaintRequests;
/* Fields used for map panning */
protected final AtomicBoolean baseImageMoved;
protected Future<?> imageMovedFuture;
protected final Point imageOrigin;
protected final Lock drawingLock;
protected final ReadWriteLock paramsLock;
protected final Set<MapPaneListener> listeners = new HashSet<MapPaneListener>();
protected final MouseDragBox dragBox;
/*
* If the user sets the display area before the pane is shown on
* screen we store the requested envelope with this field and refer
* to it when the pane is shown.
*/
protected ReferencedEnvelope pendingDisplayArea;
/*
* This field is used to cache the full extent of the combined map
* layers.
*/
protected ReferencedEnvelope fullExtent;
protected MapContent mapContent;
protected RenderingExecutor renderingExecutor;
protected KeyListener keyHandler;
protected MapMouseEventDispatcher mouseEventDispatcher;
protected LabelCache labelCache;
protected AtomicBoolean clearLabelCache;
protected CursorTool currentCursorTool;
public AbstractMapPane(MapContent content, RenderingExecutor executor) {
setBackground(DEFAULT_BACKGROUND_COLOR);
setFocusable(true);
drawingLock = new ReentrantLock();
paramsLock = new ReentrantReadWriteLock();
paneTaskExecutor = Executors.newSingleThreadScheduledExecutor();
paintDelay = DEFAULT_PAINT_DELAY;
acceptRepaintRequests = new AtomicBoolean(true);
clearLabelCache = new AtomicBoolean(true);
baseImageMoved = new AtomicBoolean();
imageOrigin = new Point(0, 0);
dragBox = new MouseDragBox(this);
mouseEventDispatcher = new DefaultMapMouseEventDispatcher(this);
addMouseListener(dragBox);
addMouseMotionListener(dragBox);
addMouseListener(mouseEventDispatcher);
addMouseMotionListener(mouseEventDispatcher);
addMouseWheelListener(mouseEventDispatcher);
/*
* Listen for mouse entered events to (re-)set the
* current tool cursor, otherwise the cursor seems to
* default to the standard cursor sometimes (at least
* on OSX)
*/
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
if (currentCursorTool != null) {
setCursor(currentCursorTool.getCursor());
}
}
});
keyHandler = new MapPaneKeyHandler(this);
addKeyListener(keyHandler);
/*
* Note: we listen for both resizing events (with HierarchyBoundsListener)
* and showing events (with HierarchyListener). Although showing
* is often accompanied by resizing this is not reliable in Swing.
*/
addHierarchyListener(new HierarchyListener() {
@Override
public void hierarchyChanged(HierarchyEvent he) {
if ((he.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
if (isShowing()) {
onShownOrResized();
}
}
}
});
addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorResized(HierarchyEvent he) {
if (isShowing()) {
onShownOrResized();
}
}
});
doSetMapContent(content);
doSetRenderingExecutor(executor);
}
/**
* Draws layers into one or more images which will then be displayed
* by the map pane.
*
* @param recreate
*/
protected abstract void drawLayers(boolean recreate);
/**
* Gets the rendering executor, creating a default one if
* necessary.
*
* @return the rendering executor
*/
public RenderingExecutor getRenderingExecutor() {
if (renderingExecutor == null) {
doSetRenderingExecutor( new DefaultRenderingExecutor() );
}
return renderingExecutor;
}
/**
* {@inheritDoc}
*/
@Override
public MapMouseEventDispatcher getMouseEventDispatcher() {
return mouseEventDispatcher;
}
/**
* {@inheritDoc}
*/
@Override
public void setMouseEventDispatcher(MapMouseEventDispatcher dispatcher) {
if (mouseEventDispatcher != null) {
mouseEventDispatcher.removeAllListeners();
}
mouseEventDispatcher = dispatcher;
}
/**
* Sets the rendering executor. If {@code executor} is {@code null},
* the default {@linkplain DefaultRenderingExecutor} will be set on
* the next call to {@linkplain #getRenderingExecutor()}.
*
* @param newExecutor the rendering executor
*/
public void setRenderingExecutor(RenderingExecutor executor) {
doSetRenderingExecutor(executor);
}
private void doSetRenderingExecutor(RenderingExecutor newExecutor) {
if (renderingExecutor != null) {
renderingExecutor.shutdown();
}
renderingExecutor = newExecutor;
}
/**
* Gets the current handler for keyboard actions.
*
* @return current handler (may be {@code null})
*/
public KeyListener getKeyHandler() {
return keyHandler;
}
/**
* Sets a handler for keyboard actions which control the map pane's
* display. The default handler is {@linkplain MapPaneKeyHandler} which
* provides for scrolling and zooming.
*
* @param controller the new handler or {@code null} to disable key handling
*/
public void setKeyHandler(KeyListener controller) {
if (keyHandler != null) {
removeKeyListener(keyHandler);
}
if (controller != null) {
addKeyListener(controller);
}
keyHandler = controller;
}
/**
* Gets the current paint delay interval in milliseconds. The map pane
* uses this delay period to avoid flickering and redundant rendering
* when drag-resizing the pane or panning the map image.
*
* @return delay in milliseconds
*/
public long getPaintDelay() {
paramsLock.readLock().lock();
try {
return paintDelay;
} finally {
paramsLock.readLock().unlock();
}
}
/**
* Sets the current paint delay interval in milliseconds. The map pane
* uses this delay period to avoid flickering and redundant rendering
* when drag-resizing the pane or panning the map image.
*
* @param delay the delay in milliseconds; if {@code <=} 0 the default delay
* period will be set
*/
public void setPaintDelay(int delay) {
paramsLock.writeLock().lock();
try {
if (delay < 0) {
paintDelay = DEFAULT_PAINT_DELAY;
} else {
paintDelay = delay;
}
} finally {
paramsLock.writeLock().unlock();
}
}
/**
* Specify whether the map pane should defer its normal
* repainting behaviour.
* <p>
* Typical use:
* <pre>{@code
* myMapPane.setRepaint(false);
*
* // do various things that would cause time-consuming
* // re-paints normally
*
* myMapPane.setRepaint(true);
* myMapPane.repaint();
* }</pre>
*
* @param repaint if true, paint requests will be handled normally;
* if false, paint requests will be deferred.
*
* @see #isAcceptingRepaints()
*/
@Override
public void setIgnoreRepaint(boolean ignoreRepaint) {
drawingLock.lock();
try {
super.setIgnoreRepaint(ignoreRepaint);
acceptRepaintRequests.set( !ignoreRepaint );
} finally {
drawingLock.unlock();
}
}
/**
* Query whether the map pane is currently accepting or ignoring
* repaint requests from other GUI components and the system.
*
* @return true if the pane is currently accepting repaint requests;
* false if it is ignoring them
*
* @see #setRepaint(boolean)
*/
public boolean isAcceptingRepaints() {
return acceptRepaintRequests.get();
}
protected void onShownOrResized() {
if (resizedFuture != null && !resizedFuture.isDone()) {
resizedFuture.cancel(true);
}
resizedFuture = paneTaskExecutor.schedule(new Runnable() {
@Override
public void run() {
setForNewSize();
// Call repaint here rather than within setForNewSize so that
// drawingLock will be available in paintComponent
repaint();
}
}, paintDelay, TimeUnit.MILLISECONDS);
}
protected void setForNewSize() {
drawingLock.lock();
try {
if (mapContent != null) {
/*
* Compare the new pane screen size to the viewport's screen area
* and skip further action if the two rectangles are equal. This
* check avoid extra rendering requests when redundant resize events
* are received (e.g. on mouse button release after drag resizing).
*/
if (mapContent.getViewport().getScreenArea().equals(getVisibleRect())) {
return;
}
mapContent.getViewport().setScreenArea(getVisibleRect());
if (pendingDisplayArea != null) {
doSetDisplayArea(pendingDisplayArea);
pendingDisplayArea = null;
} else if (mapContent.getViewport().getBounds().isEmpty()) {
setFullExtent();
doSetDisplayArea(fullExtent);
}
publishEvent(new MapPaneEvent(this,
MapPaneEvent.Type.DISPLAY_AREA_CHANGED,
getDisplayArea()));
acceptRepaintRequests.set(true);
drawLayers(true);
}
} finally {
drawingLock.unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public void moveImage(int dx, int dy) {
drawingLock.lock();
try {
if (isShowing() && !getVisibleRect().isEmpty()) {
imageOrigin.translate(dx, dy);
baseImageMoved.set(true);
repaint();
onImageMoved();
}
} finally {
drawingLock.unlock();
}
}
protected void onImageMoved() {
if (imageMovedFuture != null && !imageMovedFuture.isDone()) {
imageMovedFuture.cancel(true);
}
imageMovedFuture = paneTaskExecutor.schedule(new Runnable() {
@Override
public void run() {
afterImageMoved();
clearLabelCache.set(true);
drawLayers(false);
repaint();
}
}, paintDelay, TimeUnit.MILLISECONDS);
}
/**
* Called after the base image has been dragged. Sets the new map area and
* transforms
*/
protected void afterImageMoved() {
paramsLock.writeLock().lock();
try {
int dx = imageOrigin.x;
int dy = imageOrigin.y;
DirectPosition2D newPos = new DirectPosition2D(dx, dy);
mapContent.getViewport().getScreenToWorld().transform(newPos, newPos);
ReferencedEnvelope env = new ReferencedEnvelope(mapContent.getViewport().getBounds());
env.translate(env.getMinimum(0) - newPos.x, env.getMaximum(1) - newPos.y);
doSetDisplayArea(env);
imageOrigin.setLocation(0, 0);
baseImageMoved.set(false);
} finally {
paramsLock.writeLock().unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public MapContent getMapContent() {
paramsLock.readLock().lock();
try {
return mapContent;
} finally {
paramsLock.readLock().unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public void setMapContent(MapContent content) {
paramsLock.writeLock().lock();
try {
doSetMapContent(content);
} finally {
paramsLock.writeLock().unlock();
}
}
private void doSetMapContent(MapContent newMapContent) {
if (mapContent != newMapContent) {
if (mapContent != null) {
mapContent.removeMapLayerListListener(this);
for( Layer layer : mapContent.layers() ){
if( layer instanceof ComponentListener){
removeComponentListener( (ComponentListener) layer );
}
}
}
mapContent = newMapContent;
if (mapContent != null) {
MapViewport viewport = mapContent.getViewport();
viewport.setMatchingAspectRatio(true);
Rectangle rect = getVisibleRect();
if (!rect.isEmpty()) {
viewport.setScreenArea(rect);
}
mapContent.addMapLayerListListener(this);
mapContent.addMapBoundsListener(this);
if (!mapContent.layers().isEmpty()) {
// set all layers as selected by default for the info tool
for (Layer layer : mapContent.layers()) {
layer.setSelected(true);
if (layer instanceof ComponentListener) {
addComponentListener((ComponentListener) layer);
}
}
setFullExtent();
doSetDisplayArea(mapContent.getViewport().getBounds());
}
}
MapPaneEvent event = new MapPaneEvent(
this, MapPaneEvent.Type.NEW_MAPCONTENT, mapContent);
publishEvent(event);
drawLayers(false);
}
}
/**
* {@inheritDoc}
*/
@Override
public ReferencedEnvelope getDisplayArea() {
paramsLock.readLock().lock();
try {
if (mapContent != null) {
return mapContent.getViewport().getBounds();
} else if (pendingDisplayArea != null) {
return new ReferencedEnvelope(pendingDisplayArea);
} else {
return new ReferencedEnvelope();
}
} finally {
paramsLock.readLock().unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public void setDisplayArea(Envelope envelope) {
paramsLock.writeLock().lock();
try {
if (envelope == null) {
throw new IllegalArgumentException("envelope must not be null");
}
doSetDisplayArea(envelope);
if (mapContent != null) {
clearLabelCache.set(true);
drawLayers(false);
}
} finally {
paramsLock.writeLock().unlock();
}
}
/**
* Helper method for {@linkplain #setDisplayArea} which is also called by
* other methods that want to set the display area without provoking
* repainting of the display
*
* @param envelope requested display area
*/
protected void doSetDisplayArea(Envelope envelope) {
if (mapContent != null) {
CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
if (crs == null) {
// assume that it is the current CRS
crs = mapContent.getCoordinateReferenceSystem();
}
ReferencedEnvelope refEnv = new ReferencedEnvelope(
envelope.getMinimum(0), envelope.getMaximum(0),
envelope.getMinimum(1), envelope.getMaximum(1),
crs);
mapContent.getViewport().setBounds(refEnv);
} else {
pendingDisplayArea = new ReferencedEnvelope(envelope);
}
// Publish the resulting display area with the event
publishEvent( new MapPaneEvent(this,
MapPaneEvent.Type.DISPLAY_AREA_CHANGED,
getDisplayArea()) );
}
/**
* {@inheritDoc}
*/
@Override
public void reset() {
paramsLock.writeLock().lock();
try {
if (fullExtent != null) {
setDisplayArea(fullExtent);
}
} finally {
paramsLock.writeLock().unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public AffineTransform getScreenToWorldTransform() {
paramsLock.readLock().lock();
try {
if (mapContent != null) {
return mapContent.getViewport().getScreenToWorld();
} else {
return null;
}
} finally {
paramsLock.readLock().unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public AffineTransform getWorldToScreenTransform() {
paramsLock.readLock().lock();
try {
if (mapContent != null) {
return mapContent.getViewport().getWorldToScreen();
} else {
return null;
}
} finally {
paramsLock.readLock().unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public void addMapPaneListener(MapPaneListener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener must not be null");
}
listeners.add(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void removeMapPaneListener(MapPaneListener listener) {
if (listener != null) {
listeners.remove(listener);
}
}
/**
* {@inheritDoc}
*/
@Override
public void addMouseListener(MapMouseListener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener must not be null");
}
mouseEventDispatcher.addMouseListener(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void removeMouseListener(MapMouseListener listener) {
if (listener != null) {
mouseEventDispatcher.removeMouseListener(listener);
}
}
/**
* {@inheritDoc}
*/
@Override
public CursorTool getCursorTool() {
return currentCursorTool;
}
/**
* {@inheritDoc}
*/
@Override
public void setCursorTool(CursorTool tool) {
paramsLock.writeLock().lock();
try {
if (currentCursorTool != null) {
mouseEventDispatcher.removeMouseListener(currentCursorTool);
}
currentCursorTool = tool;
if (currentCursorTool == null) {
setCursor(Cursor.getDefaultCursor());
dragBox.setEnabled(false);
} else {
setCursor(currentCursorTool.getCursor());
dragBox.setEnabled(currentCursorTool.drawDragBox());
currentCursorTool.setMapPane(this);
mouseEventDispatcher.addMouseListener(currentCursorTool);
}
} finally {
paramsLock.writeLock().unlock();
}
}
/**
* Called when a new map layer has been added. Sets the layer
* as selected (for queries) and, if the layer table is being
* used, adds the new layer to the table.
*/
@Override
public void layerAdded(MapLayerListEvent event) {
paramsLock.writeLock().lock();
try {
Layer layer = event.getElement();
if (layer instanceof ComponentListener) {
addComponentListener((ComponentListener) layer);
}
setFullExtent();
MapViewport viewport = mapContent.getViewport();
if (viewport.getBounds().isEmpty()) {
viewport.setBounds(fullExtent);
}
} finally {
paramsLock.writeLock().unlock();
}
drawLayers(false);
repaint();
}
/**
* Called when a map layer has been removed
*/
@Override
public void layerRemoved(MapLayerListEvent event) {
paramsLock.writeLock().lock();
try {
Layer layer = event.getElement();
if (layer instanceof ComponentListener) {
addComponentListener((ComponentListener) layer);
}
if (mapContent.layers().isEmpty()) {
fullExtent = null;
} else {
setFullExtent();
}
} finally {
paramsLock.writeLock().unlock();
}
drawLayers(false);
repaint();
}
/**
* Called when a map layer has changed, e.g. features added
* to a displayed feature collection
*/
@Override
public void layerChanged(MapLayerListEvent event) {
paramsLock.writeLock().lock();
try {
int reason = event.getMapLayerEvent().getReason();
if (reason == MapLayerEvent.DATA_CHANGED) {
setFullExtent();
}
if (reason != MapLayerEvent.SELECTION_CHANGED) {
clearLabelCache.set(true);
drawLayers(false);
}
} finally {
paramsLock.writeLock().unlock();
}
repaint();
}
/**
* {@inheritDoc}
*/
@Override
public void layerMoved(MapLayerListEvent event) {
drawLayers(false);
repaint();
}
/**
* {@inheritDoc}
*/
@Override
public void layerPreDispose(MapLayerListEvent event) {
getRenderingExecutor().cancelAll();
}
/**
* Called by the map content's viewport when its bounds have changed. Used
* here to watch for a changed CRS, in which case the map is re-displayed
* at full extent.
*/
@Override
public void mapBoundsChanged(MapBoundsEvent event) {
paramsLock.writeLock().lock();
try {
int type = event.getType();
if ((type & MapBoundsEvent.COORDINATE_SYSTEM_MASK) != 0) {
/*
* The coordinate reference system has changed. Set the map
* to display the full extent of layer bounds to avoid the
* effect of a shrinking map
*/
setFullExtent();
reset();
}
} finally {
paramsLock.writeLock().unlock();
}
}
/**
* Publish a MapPaneEvent to registered listeners
*
* @param ev the event to publish
* @see MapPaneListener
*/
protected void publishEvent(MapPaneEvent ev) {
for (MapPaneListener listener : listeners) {
switch (ev.getType()) {
case NEW_MAPCONTENT:
listener.onNewMapContent(ev);
break;
case DISPLAY_AREA_CHANGED:
listener.onDisplayAreaChanged(ev);
break;
case RENDERING_STARTED:
listener.onRenderingStarted(ev);
break;
case RENDERING_STOPPED:
listener.onRenderingStopped(ev);
break;
}
}
}
/**
* Determines the full extent of of
*
* @return {@code true} if full extent was set successfully
*/
protected boolean setFullExtent() {
if (mapContent != null && !mapContent.layers().isEmpty()) {
try {
fullExtent = mapContent.getMaxBounds();
/*
* Guard against degenerate envelopes (e.g. empty
* map layer or single point feature)
*/
if (fullExtent == null ) {
// set arbitrary bounds centred on 0,0
fullExtent = new ReferencedEnvelope(-1, 1, -1, 1, mapContent.getCoordinateReferenceSystem());
} else {
double w = fullExtent.getWidth();
double h = fullExtent.getHeight();
double x = fullExtent.getMinimum(0);
double y = fullExtent.getMinimum(1);
double xmin = x;
double xmax = x + w;
if (w <= 0.0) {
xmin = x - 1.0;
xmax = x + 1.0;
}
double ymin = y;
double ymax = y + h;
if (h <= 0.0) {
ymin = y - 1.0;
ymax = y + 1.0;
}
fullExtent = new ReferencedEnvelope(xmin, xmax, ymin, ymax, mapContent.getCoordinateReferenceSystem());
}
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
} else {
fullExtent = null;
}
return fullExtent != null;
}
/**
* {@inheritDoc}
* Publishes a {@linkplain MapPaneEvent} of type
* {@code MapPaneEvent.Type.RENDERING_STARTED} to listeners.
*/
@Override
public void onRenderingStarted(RenderingExecutorEvent ev) {
publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STARTED));
}
/**
* {@inheritDoc}
* Publishes a {@linkplain MapPaneEvent} of type
* {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners.
*/
@Override
public void onRenderingCompleted(RenderingExecutorEvent event) {
if (clearLabelCache.get()) {
labelCache.clear();
}
clearLabelCache.set(false);
repaint();
publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED));
}
/**
* {@inheritDoc}
* Publishes a {@linkplain MapPaneEvent} of type
* {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners.
*/
@Override
public void onRenderingFailed(RenderingExecutorEvent ev) {
publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED));
}
}