/* * 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.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Cursor; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.Map; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.RenderedImage; import java.util.HashSet; import java.util.ResourceBundle; import java.util.Set; import javax.swing.JPanel; import javax.swing.Timer; import javax.swing.event.MouseInputAdapter; import org.geotools.geometry.DirectPosition2D; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.event.MapBoundsEvent; 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.geotools.map.MapContext; import org.geotools.map.MapLayer; 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.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. * <p> * Adapted from original code by Ian Turton. * * @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 Timer resizeTimer; 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; /** * 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 curPaintArea; private BufferedImage baseImage; private Graphics2D baseImageGraphics; private Point imageOrigin; private boolean redrawBaseImage; private boolean needNewBaseImage; private boolean baseImageMoved; private boolean 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; needNewBaseImage = true; redrawBaseImage = true; baseImageMoved = false; clearLabelCache = false; /* * 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; resizeTimer = new Timer(resizingPaintDelay, new ActionListener() { public void actionPerformed(ActionEvent e) { onResizingCompleted(); } }); resizeTimer.setRepeats(false); setRenderer(renderer); setMapContext(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()); } } }); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent ev) { acceptRepaintRequests = false; renderingExecutor.cancelTask(); resizeTimer.restart(); } }); } /** * Repaint the map when resizing has finished. This method will * also be called if the user pauses drag-resizing for a period * longer than resizingPaintDelay milliseconds, and when the * map pane is first displayed. */ private void onResizingCompleted() { acceptRepaintRequests = true; needNewBaseImage = true; curPaintArea = getVisibleRect(); // allow a single pixel margin at the right and bottom edges curPaintArea.width -= 1; curPaintArea.height -= 1; if (context != null && context.getLayerCount() > 0) { if (fullExtent == null) { setFullExtent(); } if (pendingDisplayArea != null) { doSetDisplayArea(pendingDisplayArea); pendingDisplayArea = null; } else { doSetDisplayArea(fullExtent); } repaint(); 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; } /** * 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) { Point2D p0 = new Point2D.Double(curPaintArea.getMinX(), curPaintArea.getMinY()); Point2D p1 = new Point2D.Double(curPaintArea.getMaxX(), curPaintArea.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; } /** * 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 the pane has not been displayed yet or is zero size then * just record the requested display area and defer setting transforms * etc. */ if (curPaintArea == null || curPaintArea.isEmpty()) { pendingDisplayArea = new ReferencedEnvelope(envelope); } else { doSetDisplayArea(envelope); clearLabelCache = true; repaint(); } } 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) { 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); redrawBaseImage = false; baseImageMoved = true; repaint(); } /** * Called by the system to draw the layers currently visible layers. * Client code should not use this method directly; instead call * repaint(). */ @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (acceptRepaintRequests) { if (curPaintArea == null || context == null || context.getLayerCount() == 0 || renderer == null) { return; } if (needNewBaseImage) { baseImage = new BufferedImage( curPaintArea.width + 1, curPaintArea.height + 1, BufferedImage.TYPE_INT_ARGB); if (baseImageGraphics != null) { baseImageGraphics.dispose(); } baseImageGraphics = baseImage.createGraphics(); needNewBaseImage = false; redrawBaseImage = true; clearLabelCache = true; } final ReferencedEnvelope mapAOI = context.getAreaOfInterest(); if (mapAOI == null) { return; } if (redrawBaseImage) { if (baseImageMoved) { afterImageMove(mapAOI, curPaintArea); baseImageMoved = false; clearLabelCache = true; } if (renderingExecutor.submit(mapAOI, curPaintArea, baseImageGraphics)) { MapPaneEvent ev = new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STARTED); publishEvent(ev); } else { onRenderingRejected(); } } else { Graphics2D g2 = (Graphics2D) g; g2.drawImage(baseImage, imageOrigin.x, imageOrigin.y, null); } redrawBaseImage = true; } } /** * 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) { labelCache.clear(); } clearLabelCache = false; Graphics2D paneGr = (Graphics2D) this.getGraphics(); paneGr.drawImage(baseImage, imageOrigin.x, imageOrigin.y, null); 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 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) */ protected void afterImageMove(ReferencedEnvelope env, Rectangle paintArea) { 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); } /** * 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(); } 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(); } 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) { repaint(); } } /** * Called when the bounds of a map layer have changed */ public void layerMoved(MapLayerListEvent event) { 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) { ex.printStackTrace(); } } /** * Erase the base image. This is much faster than recreating a new BufferedImage * object each time we need to redraw the image */ private void clearBaseImage() { assert(baseImage != null && baseImageGraphics != null); Composite composite = baseImageGraphics.getComposite(); baseImageGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f)); Rectangle2D.Double rect = new Rectangle2D.Double( 0, 0, baseImage.getWidth(), baseImage.getHeight()); baseImageGraphics.fill(rect); baseImageGraphics.setComposite(composite); } /** * 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; } }