/*
* 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.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
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.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.ResourceBundle;
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 javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.MapContext;
import org.geotools.map.MapLayer;
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.GTRenderer;
import org.geotools.renderer.label.LabelCacheImpl;
import org.geotools.renderer.lite.LabelCache;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.swing.event.MapMouseListener;
import org.geotools.swing.event.MapPaneEvent;
import org.geotools.swing.event.MapPaneListener;
import org.geotools.swing.tool.CursorTool;
import org.geotools.swing.tool.MapToolManager;
import org.opengis.geometry.Envelope;
/**
* A map display pane that works with a GTRenderer and
* MapContext to display features. It supports the use of tool classes
* to implement, for example, mouse-controlled zooming and panning.
* <p>
* Rendering is performed on a background thread and is managed by
* the {@linkplain RenderingExecutor} class.
*
* @see JMapFrame
* @see MapPaneListener
* @see CursorTool
*
* @author Michael Bedward
* @author Ian Turton
* @since 2.6
*
* @source $URL$
* @version $Id$
*/
public class JMapPane extends JPanel implements MapLayerListListener, MapBoundsListener {
private static final ResourceBundle stringRes = ResourceBundle.getBundle("org/geotools/swing/Text");
/**
* Default delay (milliseconds) before the map will be redrawn when resizing
* the pane. This is to avoid flickering while drag-resizing.
*/
public static final int DEFAULT_RESIZING_PAINT_DELAY = 500; // delay in milliseconds
private ScheduledExecutorService paneTaskExecutor = Executors.newSingleThreadScheduledExecutor();
private Future<?> resizedFuture;
private Future<?> imageMovedFuture;
private int resizingPaintDelay;
private boolean acceptRepaintRequests;
/*
* 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.
*/
private ReferencedEnvelope pendingDisplayArea;
/* Current display area */
private ReferencedEnvelope currentDisplayArea;
/*
* This field is used to cache the full extent of the combined map
* layers.
*/
private ReferencedEnvelope fullExtent;
/**
* Encapsulates XOR box drawing logic used with mouse dragging
*/
private class DragBox extends MouseInputAdapter {
private Point startPos;
private Rectangle rect;
private boolean dragged;
private boolean enabled;
DragBox() {
rect = new Rectangle();
dragged = false;
enabled = false;
}
void setEnabled(boolean state) {
enabled = state;
}
@Override
public void mousePressed(MouseEvent e) {
startPos = new Point(e.getPoint());
}
@Override
public void mouseDragged(MouseEvent e) {
if (enabled) {
Graphics2D g2D = (Graphics2D) JMapPane.this.getGraphics();
g2D.setColor(Color.WHITE);
g2D.setXORMode(Color.RED);
if (dragged) {
g2D.drawRect(rect.x, rect.y, rect.width, rect.height);
}
rect.setFrameFromDiagonal(startPos, e.getPoint());
g2D.drawRect(rect.x, rect.y, rect.width, rect.height);
dragged = true;
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (dragged) {
Graphics2D g2D = (Graphics2D) JMapPane.this.getGraphics();
g2D.setColor(Color.WHITE);
g2D.setXORMode(Color.RED);
g2D.drawRect(rect.x, rect.y, rect.width, rect.height);
dragged = false;
}
}
}
private DragBox dragBox;
private MapContext context;
private GTRenderer renderer;
private LabelCache labelCache;
private RenderingExecutor renderingExecutor;
private MapToolManager toolManager;
private MapLayerTable layerTable;
private Set<MapPaneListener> listeners = new HashSet<MapPaneListener>();
private AffineTransform worldToScreen;
private AffineTransform screenToWorld;
private Rectangle currentPaintArea;
private BufferedImage baseImage;
private Graphics2D baseImageGraphics;
private Point imageOrigin;
private AtomicBoolean baseImageMoved;
private AtomicBoolean clearLabelCache;
/**
* Constructor - creates an instance of JMapPane with no map
* context or renderer initially
*/
public JMapPane() {
this(null, null);
}
/**
* Constructor - creates an instance of JMapPane with the given
* renderer and map context.
*
* @param renderer a renderer object
* @param context an instance of MapContext
*/
public JMapPane(GTRenderer renderer, MapContext context) {
imageOrigin = new Point(0, 0);
acceptRepaintRequests = true;
baseImageMoved = new AtomicBoolean();
clearLabelCache = new AtomicBoolean();
/*
* We use a Timer object to avoid rendering delays and
* flickering when the user is drag-resizing the parent
* container of this map pane.
*
* Using a ComponentListener doesn't work because, unlike
* a JFrame, the pane receives a stream of events during
* drag-resizing.
*/
resizingPaintDelay = DEFAULT_RESIZING_PAINT_DELAY;
doSetRenderer(renderer);
doSetMapContext(context);
renderingExecutor = new RenderingExecutor(this);
toolManager = new MapToolManager(this);
dragBox = new DragBox();
this.addMouseListener(dragBox);
this.addMouseMotionListener(dragBox);
this.addMouseListener(toolManager);
this.addMouseMotionListener(toolManager);
this.addMouseWheelListener(toolManager);
/*
* 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)
*/
this.addMouseListener(new MouseInputAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
CursorTool tool = toolManager.getCursorTool();
if (tool != null) {
JMapPane.this.setCursor(tool.getCursor());
}
}
});
/*
* 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() {
public void hierarchyChanged(HierarchyEvent he) {
if ((he.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
if (isShowing() && baseImage == null) {
onShownOrResized();
//repaint();
}
}
}
});
addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorResized(HierarchyEvent he) {
if (isShowing()) {
onShownOrResized();
}
}
});
}
private void onShownOrResized() {
if (resizedFuture != null && !resizedFuture.isDone()) {
resizedFuture.cancel(true);
}
resizedFuture = paneTaskExecutor.schedule(new Runnable() {
public void run() {
setForNewSize();
}
}, resizingPaintDelay, TimeUnit.MILLISECONDS);
}
private void setForNewSize() {
currentPaintArea = getVisibleRect();
// allow a single pixel margin at the right and bottom edges
currentPaintArea.width -= 1;
currentPaintArea.height -= 1;
if (context != null && context.getLayerCount() > 0) {
if (fullExtent == null) {
setFullExtent();
}
if (pendingDisplayArea != null) {
doSetDisplayArea(pendingDisplayArea);
pendingDisplayArea = null;
} else {
doSetDisplayArea(fullExtent);
}
acceptRepaintRequests = true;
drawBaseImage(true);
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.PANE_RESIZED);
publishEvent(ev);
}
}
/**
* Set the current cursor tool
*
* @param tool the tool to set; null means no active cursor tool
*/
public void setCursorTool(CursorTool tool) {
if (tool == null) {
toolManager.setNoCursorTool();
this.setCursor(Cursor.getDefaultCursor());
dragBox.setEnabled(false);
} else {
this.setCursor(tool.getCursor());
toolManager.setCursorTool(tool);
dragBox.setEnabled(tool.drawDragBox());
}
}
/**
* Register an object that wishes to receive {@code MapMouseEvent}s
* such as a {@linkplain org.geotools.swing.StatusBar}
*
* @param listener an object that implements {@code MapMouseListener}
* @throws IllegalArgumentException if listener is null
* @see MapMouseListener
*/
public void addMouseListener(MapMouseListener listener) {
if (listener == null) {
throw new IllegalArgumentException(stringRes.getString("arg_null_error"));
}
toolManager.addMouseListener(listener);
}
/**
* Unregister the {@code MapMouseListener} object.
*
* @param listener the listener to remove
* @throws IllegalArgumentException if listener is null
*/
public void removeMouseListener(MapMouseListener listener) {
if (listener == null) {
throw new IllegalArgumentException(stringRes.getString("arg_null_error"));
}
toolManager.removeMouseListener(listener);
}
/**
* Register an object that wishes to receive {@code MapPaneEvent}s
*
* @param listener an object that implements {@code MapPaneListener}
* @see MapPaneListener
*/
public void addMapPaneListener(MapPaneListener listener) {
if (listener == null) {
throw new IllegalArgumentException(stringRes.getString("arg_null_error"));
}
listeners.add(listener);
}
/**
* Register a {@linkplain MapLayerTable} object to be receive
* layer change events from this map pane and to control layer
* ordering, visibility and selection.
*
* @param layerTable an instance of MapLayerTable
*
* @throws IllegalArgumentException if layerTable is null
*/
public void setMapLayerTable(MapLayerTable layerTable) {
if (layerTable == null) {
throw new IllegalArgumentException(stringRes.getString("arg_null_error"));
}
this.layerTable = layerTable;
resetMapLayerTable();
}
private void resetMapLayerTable() {
if (layerTable != null && context != null) {
layerTable.clear();
for (MapLayer layer : context.getLayers()) {
layerTable.onAddLayer(layer);
}
}
}
/**
* Get the renderer being used by this map pane
*
* @return live reference to the renderer being used
*/
public GTRenderer getRenderer() {
return renderer;
}
/**
* Set the renderer for this map pane.
*
* @param renderer the renderer to use
*/
public void setRenderer(GTRenderer renderer) {
doSetRenderer(renderer);
}
private void doSetRenderer(GTRenderer renderer) {
if (renderer != null) {
Map<Object, Object> hints;
if (renderer instanceof StreamingRenderer) {
hints = renderer.getRendererHints();
if (hints == null) {
hints = new HashMap<Object, Object>();
}
if (hints.containsKey(StreamingRenderer.LABEL_CACHE_KEY)) {
labelCache = (LabelCache) hints.get(StreamingRenderer.LABEL_CACHE_KEY);
} else {
labelCache = new LabelCacheImpl();
hints.put(StreamingRenderer.LABEL_CACHE_KEY, labelCache);
}
renderer.setRendererHints(hints);
if (this.context != null) {
renderer.setContext(this.context);
}
}
}
this.renderer = renderer;
}
/**
* Get the map context associated with this map pane
* @return a live reference to the current map context
*/
public MapContext getMapContext() {
return context;
}
/**
* Set the map context for this map pane to display
* @param context the map context
*/
public void setMapContext(MapContext context) {
doSetMapContext(context);
}
private void doSetMapContext(MapContext context) {
if (this.context != context) {
if (this.context != null) {
this.context.removeMapLayerListListener(this);
for( MapLayer layer : this.context.getLayers() ){
if( layer instanceof ComponentListener){
removeComponentListener( (ComponentListener) layer );
}
}
}
this.context = context;
if (context != null) {
this.context.addMapLayerListListener(this);
this.context.addMapBoundsListener(this);
resetMapLayerTable();
if (context.getLayerCount() > 0) {
// set all layers as selected by default for the info tool
for (MapLayer layer : context.getLayers()) {
layer.setSelected(true);
if (layer instanceof ComponentListener) {
addComponentListener((ComponentListener) layer);
}
}
setFullExtent();
doSetDisplayArea(fullExtent);
}
}
if (renderer != null) {
renderer.setContext(this.context);
}
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.NEW_CONTEXT);
publishEvent(ev);
drawBaseImage(false);
}
}
/**
* Return a (copy of) the currently displayed map area.
* <p>
* Note, this will not always be the same as the envelope returned by
* {@code MapContext.getAreaOfInterest()}. For example, when the
* map is displayed at the full extent of all layers
* {@code MapContext.getAreaOfInterest()} will return the union of the
* layer bounds while this method will return an evnelope that can
* included extra space beyond the bounds of the layers.
*
* @return the display area in world coordinates as a new {@code ReferencedEnvelope}
*/
public ReferencedEnvelope getDisplayArea() {
if (currentDisplayArea == null) {
// return empty envelope
return new ReferencedEnvelope();
} else {
// return copy of envelope
return new ReferencedEnvelope(currentDisplayArea);
}
}
private void calculateDisplayArea() {
if (currentPaintArea != null && screenToWorld != null) {
Point2D p0 = new Point2D.Double(currentPaintArea.getMinX(), currentPaintArea.getMinY());
Point2D p1 = new Point2D.Double(currentPaintArea.getMaxX(), currentPaintArea.getMaxY());
screenToWorld.transform(p0, p0);
screenToWorld.transform(p1, p1);
currentDisplayArea = new ReferencedEnvelope(
Math.min(p0.getX(), p1.getX()),
Math.max(p0.getX(), p1.getX()),
Math.min(p0.getY(), p1.getY()),
Math.max(p0.getY(), p1.getY()),
context.getCoordinateReferenceSystem());
}
}
/**
* Sets the area to display. Does nothing if the MapContext has not been set.
* <p>
* The map area that ends up being displayed will often be larger than the requested
* display area. For instance, if the square area is requested, but the map pane's
* screen area is a rectangle with width greater than height, then the displayed area
* will be centred on the requested square but include additional area on each side.
* <p>
* You can pass any GeoAPI Envelope implementation to this method such as
* ReferenedEnvelope or Envelope2D.
* <p>
* Note: This method does <b>not</b> check that the requested area overlaps
* the bounds of the current map layers.
*
* @param envelope the bounds of the map to display
*
* @throws IllegalStateException if a map context is not set
*/
public void setDisplayArea(Envelope envelope) {
if (context != null) {
/*
* If the pane has not been displayed yet or is zero size then
* just record the requested display area and defer setting transforms
* etc.
*/
if (currentPaintArea == null || currentPaintArea.isEmpty()) {
pendingDisplayArea = new ReferencedEnvelope(envelope);
} else {
doSetDisplayArea(envelope);
clearLabelCache.set(true);
drawBaseImage(false);
}
} else {
throw new IllegalStateException("Map context must be set before setting the display area");
}
}
/**
* 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
*/
private void doSetDisplayArea(Envelope envelope) {
assert (context != null && currentPaintArea != null && !currentPaintArea.isEmpty());
if (equalsFullExtent(envelope)) {
setTransforms(fullExtent, currentPaintArea);
} else {
setTransforms(envelope, currentPaintArea);
}
calculateDisplayArea();
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.DISPLAY_AREA_CHANGED);
publishEvent(ev);
}
/**
* Check if the envelope corresponds to full extent. It will probably not
* equal the full extent envelope because of slack space in the display
* area, so we check that at least one pair of opposite edges are
* equal to the full extent envelope, allowing for slack space on the
* other two sides.
* <p>
* Note: this method returns {@code false} if the full extent envelope
* is wholly within the requested envelope (e.g. user has zoomed out
* from full extent), only touches one edge, or touches two adjacent edges.
* In all these cases we assume that the user wants to maintain the slack
* space in the display.
* <p>
* This method is part of the work-around that the map pane needs because
* of the differences in how raster and vector layers are treated by the
* renderer classes.
*
* @param envelope a pending display envelope to compare to the full extent
* envelope
*
* @return true if the envelope is coincident with the full extent evenlope
* on at least two edges; false otherwise
*
* @todo My logic here seems overly complex - I'm sure there must be a simpler
* way for the map pane to handle this.
*/
private boolean equalsFullExtent(final Envelope envelope) {
if (fullExtent == null || envelope == null) {
return false;
}
final double TOL = 1.0e-6d * (fullExtent.getWidth() + fullExtent.getHeight());
boolean touch = false;
if (Math.abs(envelope.getMinimum(0) - fullExtent.getMinimum(0)) < TOL) {
touch = true;
}
if (Math.abs(envelope.getMaximum(0) - fullExtent.getMaximum(0)) < TOL) {
if (touch) {
return true;
}
}
if (Math.abs(envelope.getMinimum(1) - fullExtent.getMinimum(1)) < TOL) {
touch = true;
}
if (Math.abs(envelope.getMaximum(1) - fullExtent.getMaximum(1)) < TOL) {
if (touch) {
return true;
}
}
return false;
}
/**
* Reset the map area to include the full extent of all
* layers and redraw the display
*/
public void reset() {
if (fullExtent != null) {
setDisplayArea(fullExtent);
}
}
/**
* 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()
*/
public void setRepaint(boolean repaint) {
acceptRepaintRequests = repaint;
// we also want to accept / ignore system requests for repainting
setIgnoreRepaint(!repaint);
}
/**
* 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;
}
/**
* Retrieve the map pane's current base image.
* <p>
* The map pane caches the most recent rendering of map layers
* as an image to avoid time-consuming rendering requests whenever
* possible. The base image will be re-drawn whenever there is a
* change to map layer data, style or visibility; and it will be
* replaced by a new image when the pane is resized.
* <p>
* This method returns a <b>live</b> reference to the current
* base image. Use with caution.
*
* @return a live reference to the current base image
*/
public RenderedImage getBaseImage() {
return this.baseImage;
}
/**
* Get the length of the delay period between the pane being
* resized and the next repaint.
* <p>
* The map pane imposes a delay between resize events and repainting
* to avoid flickering of the display during drag-resizing.
*
* @return delay in milliseconds
*/
public int getResizeDelay() {
return resizingPaintDelay;
}
/**
* Set the length of the delay period between the pane being
* resized and the next repaint.
* <p>
* The map pane imposes a delay between resize events and repainting
* to avoid flickering of the display during drag-resizing.
*
* @param delay the delay in milliseconds; if {@code <} 0 the default delay
* period will be set
*/
public void setResizeDelay(int delay) {
if (delay < 0) {
resizingPaintDelay = DEFAULT_RESIZING_PAINT_DELAY;
} else {
resizingPaintDelay = delay;
}
}
/**
* Get a (copy of) the screen to world coordinate transform
* being used by this map pane.
*
* @return a copy of the screen to world coordinate transform
*/
public AffineTransform getScreenToWorldTransform() {
if (screenToWorld != null) {
return new AffineTransform(screenToWorld);
} else {
return null;
}
}
/**
* Get a (copy of) the world to screen coordinate transform
* being used by this map pane. This method can be
* used to determine the current drawing scale...
* <pre>{@code
* double scale = mapPane.getWorldToScreenTransform().getScaleX();
* }</pre>
* @return a copy of the world to screen coordinate transform
*/
public AffineTransform getWorldToScreenTransform() {
if (worldToScreen != null) {
return new AffineTransform(worldToScreen);
} else {
return null;
}
}
/**
* Move the image currently displayed by the map pane from
* its current origin (x,y) to (x+dx, y+dy). This method
* allows dragging the map without the overhead of redrawing
* the features during the drag. For example, it is used by
* {@link org.geotools.swing.tool.PanTool}.
*
* @param dx the x offset in pixels
* @param dy the y offset in pixels.
*/
public void moveImage(int dx, int dy) {
imageOrigin.translate(dx, dy);
baseImageMoved.set(true);
repaint();
onImageMoved();
}
private void onImageMoved() {
if (imageMovedFuture != null && !imageMovedFuture.isDone()) {
imageMovedFuture.cancel(true);
}
imageMovedFuture = paneTaskExecutor.schedule(new Runnable() {
public void run() {
afterImageMoved();
clearLabelCache.set(true);
drawBaseImage(false);
repaint();
}
}, resizingPaintDelay, TimeUnit.MILLISECONDS);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (baseImage != null) {
Graphics2D g2 = (Graphics2D) g;
g2.drawImage(baseImage, imageOrigin.x, imageOrigin.y, null);
return;
}
}
private void drawBaseImage(boolean createNewImage) {
if (currentPaintArea == null) {
return;
}
if (acceptRepaintRequests) {
if (baseImage == null || createNewImage) {
baseImage = new BufferedImage(
currentPaintArea.width + 1, currentPaintArea.height + 1,
BufferedImage.TYPE_INT_ARGB);
if (baseImageGraphics != null) {
baseImageGraphics.dispose();
}
baseImageGraphics = baseImage.createGraphics();
clearLabelCache.set(true);
}
if (renderer != null && context != null && context.getLayerCount() > 0) {
if (renderingExecutor.submit(currentDisplayArea, currentPaintArea, baseImageGraphics)) {
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STARTED);
publishEvent(ev);
} else {
onRenderingRejected();
}
}
}
}
/**
* Called by the {@linkplain JMapPane.RenderingTask} when rendering has been completed
* Publishes a {@linkplain MapPaneEvent} of type
* {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners.
*
* @see MapPaneListener#onRenderingStopped(org.geotools.swing.event.MapPaneEvent)
*/
public void onRenderingCompleted() {
if (clearLabelCache.get()) {
labelCache.clear();
}
clearLabelCache.set(false);
repaint();
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED);
publishEvent(ev);
}
/**
* Called by the {@linkplain JMapPane.RenderingTask} when rendering was cancelled.
* Publishes a {@linkplain MapPaneEvent} of type
* {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners.
*
* @see MapPaneListener#onRenderingStopped(org.geotools.swing.event.MapPaneEvent)
*/
public void onRenderingCancelled() {
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED);
publishEvent(ev);
}
/**
* Called by the {@linkplain JMapPane.RenderingTask} when rendering failed.
* Publishes a {@linkplain MapPaneEvent} of type
* {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners.
*
* @see MapPaneListener#onRenderingStopped(org.geotools.swing.event.MapPaneEvent)
*/
public void onRenderingFailed() {
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED);
publishEvent(ev);
}
/**
* Called when a rendering request has been rejected. This will be common, such as
* when the user pauses during drag-resizing of the map pane. The base implementation
* does nothing. It is provided for sub-classes to override if required.
*/
public void onRenderingRejected() {
// do nothing
}
/**
* Called after the base image has been dragged. Sets the new map area and
* transforms
*/
protected void afterImageMoved() {
int dx = imageOrigin.x;
int dy = imageOrigin.y;
DirectPosition2D newPos = new DirectPosition2D(dx, dy);
screenToWorld.transform(newPos, newPos);
ReferencedEnvelope env = new ReferencedEnvelope(currentDisplayArea);
env.translate(env.getMinimum(0) - newPos.x, env.getMaximum(1) - newPos.y);
doSetDisplayArea(env);
imageOrigin.setLocation(0, 0);
baseImageMoved.set(false);
}
/**
* 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.
*/
public void layerAdded(MapLayerListEvent event) {
if (layerTable != null) {
layerTable.onAddLayer(event.getLayer());
}
MapLayer layer = event.getLayer();
layer.setSelected(true);
if( layer instanceof ComponentListener ){
addComponentListener( (ComponentListener) layer );
}
boolean atFullExtent = equalsFullExtent(getDisplayArea());
setFullExtent();
if (context.getLayerCount() == 1 || atFullExtent) {
reset();
}
drawBaseImage(false);
repaint();
}
/**
* Called when a map layer has been removed
*/
public void layerRemoved(MapLayerListEvent event) {
MapLayer layer = event.getLayer();
if (layerTable != null) {
layerTable.onRemoveLayer(layer);
}
if( layer instanceof ComponentListener ){
addComponentListener( (ComponentListener) layer );
}
if (context.getLayerCount() == 0) {
clearFields();
} else {
setFullExtent();
}
drawBaseImage(false);
repaint();
}
/**
* Called when a map layer has changed, e.g. features added
* to a displayed feature collection
*/
public void layerChanged(MapLayerListEvent event) {
if (layerTable != null) {
layerTable.repaint(event.getLayer());
}
int reason = event.getMapLayerEvent().getReason();
if (reason == MapLayerEvent.DATA_CHANGED) {
setFullExtent();
}
if (reason != MapLayerEvent.SELECTION_CHANGED) {
drawBaseImage(false);
}
repaint();
}
/**
* Called when the bounds of a map layer have changed
*/
public void layerMoved(MapLayerListEvent event) {
drawBaseImage(false);
repaint();
}
/**
* Called by the map context when its bounds have changed. Used
* here to watch for a changed CRS, in which case the map is
* redisplayed at (new) full extent.
*/
public void mapBoundsChanged(MapBoundsEvent event) {
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();
}
}
/**
* Gets the full extent of map context's layers. The only reason
* this method is defined is to avoid having try-catch blocks all
* through other methods.
*/
private void setFullExtent() {
if (context != null && context.getLayerCount() > 0) {
try {
fullExtent = context.getLayerBounds();
/*
* Guard agains 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, context.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, context.getCoordinateReferenceSystem());
}
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
} else {
fullExtent = null;
}
}
/**
* Calculate the affine transforms used to convert between
* world and pixel coordinates. The calculations here are very
* basic and assume a cartesian reference system.
* <p>
* Tne transform is calculated such that {@code envelope} will
* be centred in the display
*
* @param envelope the current map extent (world coordinates)
* @param paintArea the current map pane extent (screen units)
*/
private void setTransforms(final Envelope envelope, final Rectangle paintArea) {
ReferencedEnvelope refEnv = new ReferencedEnvelope(envelope);
double xscale = paintArea.getWidth() / refEnv.getWidth();
double yscale = paintArea.getHeight() / refEnv.getHeight();
double scale = Math.min(xscale, yscale);
double xoff = refEnv.getMedian(0) * scale - paintArea.getCenterX();
double yoff = refEnv.getMedian(1) * scale + paintArea.getCenterY();
worldToScreen = new AffineTransform(scale, 0, 0, -scale, -xoff, yoff);
try {
screenToWorld = worldToScreen.createInverse();
} catch (NoninvertibleTransformException ex) {
throw new RuntimeException("Unable to create coordinate transforms.", ex);
}
}
/**
* Publish a MapPaneEvent to registered listeners
*
* @param ev the event to publish
* @see MapPaneListener
*/
private void publishEvent(MapPaneEvent ev) {
for (MapPaneListener listener : listeners) {
switch (ev.getType()) {
case NEW_CONTEXT:
listener.onNewContext(ev);
break;
case NEW_RENDERER:
listener.onNewRenderer(ev);
break;
case PANE_RESIZED:
listener.onResized(ev);
break;
case DISPLAY_AREA_CHANGED:
listener.onDisplayAreaChanged(ev);
break;
case RENDERING_STARTED:
listener.onRenderingStarted(ev);
break;
case RENDERING_STOPPED:
listener.onRenderingStopped(ev);
break;
case RENDERING_PROGRESS:
listener.onRenderingProgress(ev);
break;
}
}
}
/**
* This method is called if all layers are removed from the context.
*/
private void clearFields() {
fullExtent = null;
worldToScreen = null;
screenToWorld = null;
}
}