/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2011, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.swing; import java.awt.Color; import java.awt.Cursor; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Rectangle; import java.awt.Transparency; 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.image.BufferedImage; import java.awt.image.RenderedImage; import java.util.HashMap; import java.util.HashSet; import java.util.Map; 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.MapContent; import org.geotools.map.Layer; 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; import org.opengis.referencing.crs.CoordinateReferenceSystem; /** * A map display pane that works with a renderer ({@code StreamingRenderer} by default) * to display map layers contained in a {@code MapContent} instance. 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 MapPane, MapLayerListListener, MapBoundsListener, RenderingExecutorListener { /** * 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 /** * Default background color (white). */ public static final Color DEFAULT_BACKGROUND_COLOR = Color.WHITE; 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; /* * 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 MapContent mapContent; private GTRenderer renderer; private LabelCache labelCache; private RenderingExecutor renderingExecutor; private MapToolManager toolManager; private MapLayerTable layerTable; private Set<MapPaneListener> listeners = new HashSet<MapPaneListener>(); private BufferedImage baseImage; private Graphics2D baseImageGraphics; private Point imageOrigin; private AtomicBoolean baseImageMoved; private AtomicBoolean clearLabelCache; /** * Creates a new map pane. */ public JMapPane() { this(null, null); } /** * Creates a new map pane with the given renderer and map content. * Either or both of {@code renderer} and {@code content} may be * {@code null} when the {@link #setRenderer(GTRenderer)} and * {@link #setMapContent(MapContent)} methods are to be called * subsequently. * * @param renderer the renderer to use for drawing layers * @param content the {@code MapContent} instance containing layers to * display */ public JMapPane(GTRenderer renderer, MapContent content) { this(renderer, content, null); } /** * Creates a new map pane with the given renderer and map content. * Either or both of {@code renderer} and {@code content} may be * {@code null} when the {@link #setRenderer(GTRenderer)} and * {@link #setMapContent(MapContent)} methods are to be called * subsequently. If {@code executor} is {@code null}, a default * rendering executor (an instance of {@linkplain SingleTaskRenderingExecutor}) * will be set. * * @param renderer the renderer to use for drawing layers * @param content the {@code MapContent} instance containing layers to * display * @param executor the rendering executor */ public JMapPane(GTRenderer renderer, MapContent content, RenderingExecutor executor) { imageOrigin = new Point(0, 0); setBackground(DEFAULT_BACKGROUND_COLOR); acceptRepaintRequests = true; baseImageMoved = new AtomicBoolean(); clearLabelCache = new AtomicBoolean(); /* * An interval is set for delayed painting to avoid * 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); doSetMapContent(content); doSetRenderingExecutor(executor); 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()) { onShownOrResized(); } } } }); 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() { if (mapContent != null) { /* * Compare the new pane screen size to the viewport's screen area * and skip further action if the two rectangles are equal. This * check avoid extra rendering requests when redundant resize events * are received (e.g. on mouse button release after drag resizing). */ if (mapContent.getViewport().getScreenArea().equals(getVisibleRect())) { return; } mapContent.getViewport().setScreenArea(getVisibleRect()); if (pendingDisplayArea != null) { doSetDisplayArea(pendingDisplayArea); pendingDisplayArea = null; } else if (mapContent.getViewport().getBounds().isEmpty()) { setFullExtent(); doSetDisplayArea(fullExtent); } publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.DISPLAY_AREA_CHANGED, getDisplayArea())); acceptRepaintRequests = true; drawBaseImage(true); } } /** * {@inheritDoc} */ 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()); } } /** * {@inheritDoc} */ public void addMouseListener(MapMouseListener listener) { if (listener == null) { throw new IllegalArgumentException("listener must not be null"); } toolManager.addMouseListener(listener); } /** * {@inheritDoc} */ public void removeMouseListener(MapMouseListener listener) { if (listener != null) { toolManager.removeMouseListener(listener); } } /** * {@inheritDoc} */ public void addMapPaneListener(MapPaneListener listener) { if (listener == null) { throw new IllegalArgumentException("listener must not be null"); } listeners.add(listener); } /** * {@inheritDoc} */ public void removeMapPaneListener(MapPaneListener listener) { if (listener != null) { listeners.remove(listener); } } /** * Registers 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("layerTable must not be null"); } this.layerTable = layerTable; resetMapLayerTable(); } private void resetMapLayerTable() { if (layerTable != null && mapContent != null) { layerTable.clear(); for (Layer layer : mapContent.layers()) { layerTable.onAddLayer(layer); } } } /** * {@inheritDoc} */ public void setRenderer(GTRenderer renderer) { doSetRenderer(renderer); } /** * {@inheritDoc} */ public GTRenderer getRenderer() { return renderer; } private void doSetRenderer(GTRenderer newRenderer) { if (newRenderer != null) { Map<Object, Object> hints = newRenderer.getRendererHints(); if (hints == null) { hints = new HashMap<Object, Object>(); } if (newRenderer instanceof StreamingRenderer) { 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); } } newRenderer.setRendererHints(hints); if (mapContent != null) { newRenderer.setMapContent(mapContent); } } renderer = newRenderer; MapPaneEvent event = new MapPaneEvent(this, MapPaneEvent.Type.NEW_RENDERER, renderer); publishEvent(event); } /** * Sets the rendering executor. If {@code executor} is {@code null}, * the default {@linkplain SingleTaskRenderingExecutor} will be set. * * @param newExecutor the rendering executor */ public void setRenderingExecutor(RenderingExecutor executor) { doSetRenderingExecutor(executor); } /** * Gets the rendering executor. * * @return the rendering executor */ public RenderingExecutor getRenderingExecutor() { return renderingExecutor; } private void doSetRenderingExecutor(RenderingExecutor newExecutor) { if (newExecutor == null) { newExecutor = new SingleTaskRenderingExecutor(); } if (renderingExecutor != null) { renderingExecutor.shutdown(); } renderingExecutor = newExecutor; } /** * {@inheritDoc} */ public void setMapContent(MapContent content) { doSetMapContent(content); } /** * {@inheritDoc} */ public MapContent getMapContent() { return mapContent; } private void doSetMapContent(MapContent newMapContent) { if (mapContent != newMapContent) { if (mapContent != null) { mapContent.removeMapLayerListListener(this); for( Layer layer : mapContent.layers() ){ if( layer instanceof ComponentListener){ removeComponentListener( (ComponentListener) layer ); } } } mapContent = newMapContent; mapContent.getViewport().setMatchingAspectRatio(true); if (mapContent != null) { mapContent.addMapLayerListListener(this); mapContent.addMapBoundsListener(this); resetMapLayerTable(); if (!mapContent.layers().isEmpty()) { // set all layers as selected by default for the info tool for (Layer layer : mapContent.layers()) { layer.setSelected(true); if (layer instanceof ComponentListener) { addComponentListener((ComponentListener) layer); } } setFullExtent(); doSetDisplayArea(fullExtent); } } if (renderer != null) { renderer.setMapContent(mapContent); } MapPaneEvent event = new MapPaneEvent( this, MapPaneEvent.Type.NEW_MAPCONTENT, mapContent); publishEvent(event); drawBaseImage(false); } } /** * {@inheritDoc} */ public ReferencedEnvelope getDisplayArea() { if (mapContent != null) { return mapContent.getViewport().getBounds(); } else if (pendingDisplayArea != null) { return new ReferencedEnvelope(pendingDisplayArea); } else { return new ReferencedEnvelope(); } } /** * {@inheritDoc} */ public void setDisplayArea(Envelope envelope) { if (envelope == null) { throw new IllegalArgumentException("envelope must not be null"); } doSetDisplayArea(envelope); if (mapContent != null) { clearLabelCache.set(true); drawBaseImage(false); } } /** * 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) { if (mapContent != null) { if (equalsFullExtent(envelope)) { mapContent.getViewport().setBounds(fullExtent); } else { CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem(); if (crs == null) { // assume that it is the current CRS crs = mapContent.getCoordinateReferenceSystem(); } ReferencedEnvelope refEnv = new ReferencedEnvelope( envelope.getMinimum(0), envelope.getMaximum(0), envelope.getMinimum(1), envelope.getMaximum(1), crs); mapContent.getViewport().setBounds(refEnv); } } else { pendingDisplayArea = new ReferencedEnvelope(envelope); } // Publish the resulting display area with the event publishEvent( new MapPaneEvent(this, MapPaneEvent.Type.DISPLAY_AREA_CHANGED, getDisplayArea()) ); } /** * 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 (envelope == null || mapContent == null) { return false; } if (fullExtent == null && !setFullExtent()) { return false; } final double TOL = 1.0e-6d * Math.min(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; } /** * {@inheritDoc} */ 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; } } /** * {@inheritDoc} */ public AffineTransform getScreenToWorldTransform() { if (mapContent != null) { return mapContent.getViewport().getScreenToWorld(); } else { return null; } } /** * {@inheritDoc} */ public AffineTransform getWorldToScreenTransform() { if (mapContent != null) { return mapContent.getViewport().getWorldToScreen(); } 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 (mapContent != null && !mapContent.getViewport().isEmpty() && acceptRepaintRequests) { Rectangle r = getVisibleRect(); if (baseImage == null || createNewImage) { baseImage = GraphicsEnvironment.getLocalGraphicsEnvironment(). getDefaultScreenDevice().getDefaultConfiguration(). createCompatibleImage(r.width, r.height, Transparency.TRANSLUCENT); if (baseImageGraphics != null) { baseImageGraphics.dispose(); } baseImageGraphics = baseImage.createGraphics(); clearLabelCache.set(true); } else { baseImageGraphics.setBackground(getBackground()); baseImageGraphics.clearRect(0, 0, r.width, r.height); } if (renderer != null && mapContent != null && !mapContent.layers().isEmpty()) { renderingExecutor.submit(mapContent, renderer, baseImageGraphics, this); } } } /** * {@inheritDoc} * Publishes a {@linkplain MapPaneEvent} of type * {@code MapPaneEvent.Type.RENDERING_STARTED} to listeners. */ public void onRenderingStarted(RenderingExecutorEvent ev) { publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STARTED)); } /** * {@inheritDoc} * Publishes a {@linkplain MapPaneEvent} of type * {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners. */ public void onRenderingCompleted(RenderingExecutorEvent event) { if (clearLabelCache.get()) { labelCache.clear(); } clearLabelCache.set(false); repaint(); publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED)); } /** * {@inheritDoc} * Publishes a {@linkplain MapPaneEvent} of type * {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners. */ public void onRenderingCancelled(RenderingExecutorEvent event) { publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED)); } /** * {@inheritDoc} * Publishes a {@linkplain MapPaneEvent} of type * {@code MapPaneEvent.Type.RENDERING_STOPPED} to listeners. */ public void onRenderingFailed(RenderingExecutorEvent ev) { publishEvent(new MapPaneEvent(this, MapPaneEvent.Type.RENDERING_STOPPED)); } /** * 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); mapContent.getViewport().getScreenToWorld().transform(newPos, newPos); ReferencedEnvelope env = new ReferencedEnvelope(mapContent.getViewport().getBounds()); env.translate(env.getMinimum(0) - newPos.x, env.getMaximum(1) - newPos.y); doSetDisplayArea(env); imageOrigin.setLocation(0, 0); baseImageMoved.set(false); } /** * 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) { Layer layer = event.getElement(); if (layerTable != null) { layerTable.onAddLayer(layer); } if( layer instanceof ComponentListener ){ addComponentListener( (ComponentListener) layer ); } boolean atFullExtent = equalsFullExtent(getDisplayArea()); setFullExtent(); if (mapContent.layers().size() == 1 || atFullExtent) { reset(); } drawBaseImage(false); repaint(); } /** * Called when a map layer has been removed */ public void layerRemoved(MapLayerListEvent event) { Layer layer = event.getElement(); if (layerTable != null) { layerTable.onRemoveLayer(layer); } if( layer instanceof ComponentListener ){ addComponentListener( (ComponentListener) layer ); } if (mapContent.layers().isEmpty()) { fullExtent = null; } 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.getElement()); } int reason = event.getMapLayerEvent().getReason(); if (reason == MapLayerEvent.DATA_CHANGED) { setFullExtent(); } if (reason != MapLayerEvent.SELECTION_CHANGED) { clearLabelCache.set(true); 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. * * @return {@code true} if full extent was set successfully */ private boolean setFullExtent() { if (mapContent != null && !mapContent.layers().isEmpty()) { try { fullExtent = mapContent.getMaxBounds(); /* * Guard against degenerate envelopes (e.g. empty * map layer or single point feature) */ if (fullExtent == null ) { // set arbitrary bounds centred on 0,0 fullExtent = new ReferencedEnvelope(-1, 1, -1, 1, mapContent.getCoordinateReferenceSystem()); } else { double w = fullExtent.getWidth(); double h = fullExtent.getHeight(); double x = fullExtent.getMinimum(0); double y = fullExtent.getMinimum(1); double xmin = x; double xmax = x + w; if (w <= 0.0) { xmin = x - 1.0; xmax = x + 1.0; } double ymin = y; double ymax = y + h; if (h <= 0.0) { ymin = y - 1.0; ymax = y + 1.0; } fullExtent = new ReferencedEnvelope(xmin, xmax, ymin, ymax, mapContent.getCoordinateReferenceSystem()); } } catch (Exception ex) { throw new IllegalStateException(ex); } } else { fullExtent = null; } return fullExtent != null; } /** * 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_MAPCONTENT: listener.onNewContent(ev); break; case NEW_RENDERER: listener.onNewRenderer(ev); break; case DISPLAY_AREA_CHANGED: listener.onDisplayAreaChanged(ev); break; case RENDERING_STARTED: listener.onRenderingStarted(ev); break; case RENDERING_STOPPED: listener.onRenderingStopped(ev); break; } } } }