/*
* 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.swt;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
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.Set;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.jts.JTS;
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.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
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.swt.event.MapMouseListener;
import org.geotools.swt.event.MapPaneEvent;
import org.geotools.swt.event.MapPaneListener;
import org.geotools.swt.tool.CursorTool;
import org.geotools.swt.tool.MapToolManager;
import org.geotools.swt.utils.CursorManager;
import org.geotools.swt.utils.Messages;
import org.geotools.swt.utils.Utils;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
/**
* 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.
* <p>
* Adapted from original code by Ian Turton.
*
* @author Andrea Antonello (www.hydrologis.com)
* @author Michael Bedward
* @author Ian Turton
*
*
* @source $URL: http://svn.osgeo.org/geotools/trunk/modules/unsupported/swt/src/main/java/org/geotools/swt/SwtMapPane.java $
*/
public class SwtMapPane extends Canvas implements Listener, MapLayerListListener, MapBoundsListener {
private static final PaletteData PALETTE_DATA = new PaletteData(0xFF0000, 0xFF00, 0xFF);
/** RGB value to use as transparent color */
private static final int TRANSPARENT_COLOR = 0x123456;
/**
* 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 int resizingPaintDelay;
private boolean acceptRepaintRequests;
/**
* This field is used to cache the full extent of the combined map
* layers.
*/
private ReferencedEnvelope fullExtent;
private MapContext context;
private GTRenderer renderer;
private LabelCache labelCache;
private MapToolManager toolManager;
private MapLayerComposite layerTable;
private Set<MapPaneListener> listeners = new HashSet<MapPaneListener>();
private AffineTransform worldToScreen;
private AffineTransform screenToWorld;
private Rectangle curPaintArea;
private BufferedImage baseImage;
private Point imageOrigin;
private boolean redrawBaseImage;
private boolean baseImageMoved;
private boolean clearLabelCache;
private boolean dragEnabled;
/**
* swt image used to draw
*/
private Image swtImage;
private GC gc;
private boolean mouseDown = false;
private int startX;
private int startY;
private int endX;
private int endY;
private boolean isDragging = false;
private Image overlayImage;
private ReferencedEnvelope overlayEnvelope;
private boolean overlayDoXor;
/**
* Constructor - creates an instance of JMapPane with no map
* context or renderer initially
*/
public SwtMapPane( Composite parent, int style ) {
this(parent, style, 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 SwtMapPane( Composite parent, int style, GTRenderer renderer, MapContext context ) {
super(parent, style);
addListener(SWT.Paint, this);
addListener(SWT.MouseDown, this);
addListener(SWT.MouseUp, this);
imageOrigin = new Point(0, 0);
acceptRepaintRequests = true;
redrawBaseImage = true;
baseImageMoved = false;
clearLabelCache = false;
setRenderer(renderer);
setMapContext(context);
toolManager = new MapToolManager(this);
this.addMouseListener(toolManager);
this.addMouseMoveListener(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.addMouseMoveListener(new MouseMoveListener(){
public void mouseMove( MouseEvent event ) {
if (mouseDown) {
endX = event.x;
endY = event.y;
isDragging = true;
redraw();
}
}
});
addControlListener(new ControlAdapter(){
public void controlResized( ControlEvent e ) {
curPaintArea = getVisibleRect();
doSetDisplayArea(SwtMapPane.this.context.getAreaOfInterest());
}
});
}
/**
* 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(CursorManager.getInstance().getArrowCursor());
dragEnabled = false;
} else {
this.setCursor(tool.getCursor());
toolManager.setCursorTool(tool);
dragEnabled = 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(Messages.getString("arg_null_error")); //$NON-NLS-1$
}
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(Messages.getString("arg_null_error")); //$NON-NLS-1$
}
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(Messages.getString("arg_null_error")); //$NON-NLS-1$
}
listeners.add(listener);
}
/**
* Register a {@linkplain MapLayerComposite} 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( MapLayerComposite layerTable ) {
if (layerTable == null) {
throw new IllegalArgumentException(Messages.getString("arg_null_error")); //$NON-NLS-1$
}
this.layerTable = layerTable;
}
/**
* 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 ) {
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 ) {
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);
// 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();
}
if (renderer != null) {
renderer.setContext(this.context);
}
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.NEW_CONTEXT);
publishEvent(ev);
}
}
/**
* 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() {
ReferencedEnvelope aoi = null;
if (curPaintArea != null && screenToWorld != null) {
Rectangle2D awtRectangle = Utils.toAwtRectangle(curPaintArea);
Point2D p0 = new Point2D.Double(awtRectangle.getMinX(), awtRectangle.getMinY());
Point2D p1 = new Point2D.Double(awtRectangle.getMaxX(), awtRectangle.getMaxY());
screenToWorld.transform(p0, p0);
screenToWorld.transform(p1, p1);
aoi = 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());
}
return aoi;
}
public void setCrs( CoordinateReferenceSystem crs ) {
try {
System.out.println(context.getLayerCount());
ReferencedEnvelope rEnv = getDisplayArea();
System.out.println(rEnv);
CoordinateReferenceSystem sourceCRS = rEnv.getCoordinateReferenceSystem();
CoordinateReferenceSystem targetCRS = crs;
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS);
com.vividsolutions.jts.geom.Envelope newJtsEnv = JTS.transform(rEnv, transform);
ReferencedEnvelope newEnvelope = new ReferencedEnvelope(newJtsEnv, targetCRS);
context.setAreaOfInterest(newEnvelope);
fullExtent = null;
doSetDisplayArea(newEnvelope);
ReferencedEnvelope displayArea = getDisplayArea();
System.out.println(displayArea);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Sets the area to display by calling the {@linkplain MapContext#setAreaOfInterest}
* method of this pane's map context. Does nothing if the MapContext has not been set.
* If neither the context or the envelope have coordinate reference systems defined
* this method does nothing.
* <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 (curPaintArea == null || curPaintArea.isEmpty()) {
return;
} else {
doSetDisplayArea(envelope);
clearLabelCache = true;
redraw();
}
} 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 && curPaintArea != null && !curPaintArea.isEmpty());
if (equalsFullExtent(envelope)) {
setTransforms(fullExtent, curPaintArea);
} else {
setTransforms(envelope, curPaintArea);
}
ReferencedEnvelope adjustedEnvelope = getDisplayArea();
context.setAreaOfInterest(adjustedEnvelope);
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) {
setFullExtent();
}
try {
fullExtent = new ReferencedEnvelope(CRS.transform(fullExtent, context.getCoordinateReferenceSystem()));
} catch (Exception e) {
e.printStackTrace();
}
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;
}
/**
* 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);
redrawBaseImage = false;
baseImageMoved = true;
redraw();
}
/**
* Called by the {@linkplain SwtMapPane.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) {
labelCache.clear();
}
clearLabelCache = false;
// Graphics2D paneGr = (Graphics2D) this.getGraphics();
// paneGr.drawImage(baseImage, imageOrigin.x, imageOrigin.y, null);
// swtImage = new Image(this.getDisplay(), awtToSwt(baseImage, curPaintArea.width,
// curPaintArea.height));
// if (gc != null && !gc.isDisposed() && swtImage != null)
// gc.drawImage(swtImage, imageOrigin.x, imageOrigin.y);
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED);
publishEvent(ev);
}
/**
* Called by the {@linkplain SwtMapPane.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 SwtMapPane.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 fo 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
* @param env the display area (world coordinates) prior to the image being moved
* @param paintArea the current drawing area (screen units)
*/
private void afterImageMove() {
final ReferencedEnvelope env = context.getAreaOfInterest();
if (env == null)
return;
int dx = imageOrigin.x;
int dy = imageOrigin.y;
DirectPosition2D newPos = new DirectPosition2D(dx, dy);
screenToWorld.transform(newPos, newPos);
env.translate(env.getMinimum(0) - newPos.x, env.getMaximum(1) - newPos.y);
doSetDisplayArea(env);
imageOrigin.setLocation(0, 0);
redrawBaseImage = true;
}
/**
* 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);
boolean atFullExtent = equalsFullExtent(getDisplayArea());
boolean firstLayer = context.getLayerCount() == 1;
if (firstLayer || atFullExtent) {
reset();
if (firstLayer) {
setCrs(layer.getBounds().getCoordinateReferenceSystem());
return;
}
}
redraw();
}
/**
* Called when a map layer has been removed
*/
public void layerRemoved( MapLayerListEvent event ) {
MapLayer layer = event.getLayer();
if (layerTable != null) {
layerTable.onRemoveLayer(layer);
}
if (context.getLayerCount() == 0) {
clearFields();
} else {
setFullExtent();
}
redraw();
}
/**
* 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) {
redraw();
}
}
/**
* Called when the bounds of a map layer have changed
*/
public void layerMoved( MapLayerListEvent event ) {
redraw();
}
/**
* 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 ) {
redrawBaseImage = true;
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 = worldEnvelope();// 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 = null;
if (envelope != null) {
refEnv = new ReferencedEnvelope(envelope);
} else {
refEnv = worldEnvelope();
context.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84);
}
java.awt.Rectangle awtPaintArea = Utils.toAwtRectangle(paintArea);
double xscale = awtPaintArea.getWidth() / refEnv.getWidth();
double yscale = awtPaintArea.getHeight() / refEnv.getHeight();
double scale = Math.min(xscale, yscale);
double xoff = refEnv.getMedian(0) * scale - awtPaintArea.getCenterX();
double yoff = refEnv.getMedian(1) * scale + awtPaintArea.getCenterY();
worldToScreen = new AffineTransform(scale, 0, 0, -scale, -xoff, yoff);
try {
screenToWorld = worldToScreen.createInverse();
} catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
private ReferencedEnvelope worldEnvelope() {
return new ReferencedEnvelope(-180, 180, -90, 90, DefaultGeographicCRS.WGS84);
}
/**
* 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;
}
public Rectangle getVisibleRect() {
return getClientArea();
}
/**
* Define an image that has to be set as overlay.
*
* <p>The image will be scaled to fit into the supplied envelope.
*
* @param overlayImage the image to overlay.
* @param overlayEnvelope the envelope it has to cover.
* @param overlayDoXor flag for Xor mode.
*/
public void setOverlay( Image overlayImage, ReferencedEnvelope overlayEnvelope, boolean overlayDoXor, boolean boundsChanged ) {
if (this.overlayImage != null)
this.overlayImage.dispose();
this.overlayImage = overlayImage;
this.overlayEnvelope = overlayEnvelope;
this.overlayDoXor = overlayDoXor;
if (boundsChanged || swtImage == null) {
redrawBaseImage = true;
} else {
redrawBaseImage = false;
}
redraw();
}
@SuppressWarnings("deprecation")
public void handleEvent( Event event ) {
curPaintArea = getVisibleRect();
// System.out.println("event: " + event.type);
if (event.type == SWT.MouseDown) {
startX = event.x;
startY = event.y;
mouseDown = true;
} else if (event.type == SWT.MouseUp) {
endX = event.x;
endY = event.y;
if (baseImageMoved) {
afterImageMove();
baseImageMoved = false;
}
mouseDown = false;
isDragging = false;
} else if (event.type == SWT.Paint) {
if (acceptRepaintRequests) {
// System.out.println("paint");
gc = event.gc;
if (baseImageMoved) {
if (gc != null && !gc.isDisposed() && swtImage != null) {
/*
* double buffer necessary, since the SWT.NO_BACKGROUND
* needed by the canvas to properly draw background, doesn't
* clean the parts outside the bounds of the moving panned image,
* giving a spilling image effect.
*/
Image tmpImage = new Image(getDisplay(), curPaintArea.width, curPaintArea.height);
GC tmpGc = new GC(tmpImage);
tmpGc.drawImage(swtImage, imageOrigin.x, imageOrigin.y);
gc.drawImage(tmpImage, 0, 0);
tmpImage.dispose();
}
return;
}
if (isDragging) {
if (dragEnabled) {
// System.out.println("draw box: " + startX + "/" + startY + "/" + endX +
// "/" + endY);
if (swtImage != null) {
drawFinalImage(swtImage);
}
gc.setXORMode(true);
org.eclipse.swt.graphics.Color fC = gc.getForeground();
gc.setLineWidth(2);
gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_YELLOW));
gc.drawRectangle(startX, startY, endX - startX, endY - startY);
gc.setForeground(fC);
gc.setXORMode(false);
return;
}
}
System.out.println("PAINT");
if (curPaintArea == null || context == null || renderer == null) {
return;
}
if (context.getLayerCount() == 0) {
gc.setForeground(getDisplay().getSystemColor(SWT.COLOR_YELLOW));
gc.fillRectangle(0, 0, curPaintArea.width + 1, curPaintArea.height + 1);
if (overlayImage == null)
return;
}
final ReferencedEnvelope mapAOI = context.getAreaOfInterest();
if (mapAOI == null) {
return;
}
if (redrawBaseImage) {
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STARTED);
publishEvent(ev);
baseImage = new BufferedImage(curPaintArea.width + 1, curPaintArea.height + 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = baseImage.createGraphics();
g2d.fillRect(0, 0, curPaintArea.width + 1, curPaintArea.height + 1);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// renderer.setContext(context);
java.awt.Rectangle awtRectangle = Utils.toAwtRectangle(curPaintArea);
renderer.paint(g2d, awtRectangle, mapAOI, getWorldToScreenTransform());
// swtImage.dispose();
if (swtImage != null && !swtImage.isDisposed()) {
swtImage.dispose();
swtImage = null;
}
System.out.println("create img");
swtImage = new Image(getDisplay(), awtToSwt(baseImage, curPaintArea.width + 1, curPaintArea.height + 1));
}
if (swtImage != null) {
drawFinalImage(swtImage);
}
MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED);
publishEvent(ev);
clearLabelCache = true;
onRenderingCompleted();
redrawBaseImage = true;
}
}
}
private void drawFinalImage( Image swtImage ) {
if (overlayImage != null) {
Image tmpImage = new Image(getDisplay(), curPaintArea.width, curPaintArea.height);
GC tmpGc = new GC(tmpImage);
tmpGc.drawImage(swtImage, imageOrigin.x, imageOrigin.y);
doOverlayImage(tmpGc);
if (gc != null && !gc.isDisposed() && swtImage != null)
gc.drawImage(tmpImage, imageOrigin.x, imageOrigin.y);
} else {
if (gc != null && !gc.isDisposed() && swtImage != null)
gc.drawImage(swtImage, imageOrigin.x, imageOrigin.y);
}
}
private void doOverlayImage( GC gc ) {
Point2D lowerLeft = new Point2D.Double(overlayEnvelope.getMinX(), overlayEnvelope.getMinY());
worldToScreen.transform(lowerLeft, lowerLeft);
Point2D upperRight = new Point2D.Double(overlayEnvelope.getMaxX(), overlayEnvelope.getMaxY());
worldToScreen.transform(upperRight, upperRight);
Rectangle bounds = overlayImage.getBounds();
if (overlayDoXor) {
gc.setXORMode(true);
}
gc.drawImage(overlayImage,//
0,//
0,//
bounds.width, //
bounds.height,//
(int) lowerLeft.getX(),//
(int) upperRight.getY(),//
(int) (upperRight.getX() - lowerLeft.getX()),//
(int) Math.abs(upperRight.getY() - lowerLeft.getY())//
);
if (overlayDoXor) {
gc.setXORMode(false);
}
}
/**
* Transform a java2d bufferedimage to a swt image.
*
* @param bufferedImage the image to trasform.
* @param width the image width.
* @param height the image height.
* @return swt image.
*/
private ImageData awtToSwt( BufferedImage bufferedImage, int width, int height ) {
final int[] awtPixels = new int[width * height];
ImageData swtImageData = new ImageData(width, height, 24, PALETTE_DATA);
swtImageData.transparentPixel = TRANSPARENT_COLOR;
int step = swtImageData.depth / 8;
final byte[] data = swtImageData.data;
bufferedImage.getRGB(0, 0, width, height, awtPixels, 0, width);
for( int i = 0; i < height; i++ ) {
int idx = (0 + i) * swtImageData.bytesPerLine + 0 * step;
for( int j = 0; j < width; j++ ) {
int rgb = awtPixels[j + i * width];
for( int k = swtImageData.depth - 8; k >= 0; k -= 8 ) {
data[idx++] = (byte) ((rgb >> k) & 0xFF);
}
}
}
return swtImageData;
}
}