/* * Copyright (C) 2012 United States Government as represented by the Administrator of the * National Aeronautics and Space Administration. * All Rights Reserved. */ package org.jgrasstools.nww.utils.selection; import java.awt.Color; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.List; import javax.media.opengl.GL; import javax.media.opengl.GL2; import org.jgrasstools.gears.libs.logging.JGTLogger; import gov.nasa.worldwind.WWObjectImpl; import gov.nasa.worldwind.WorldWindow; import gov.nasa.worldwind.event.Message; import gov.nasa.worldwind.event.MessageListener; import gov.nasa.worldwind.event.SelectEvent; import gov.nasa.worldwind.event.SelectListener; import gov.nasa.worldwind.layers.Layer; import gov.nasa.worldwind.layers.LayerList; import gov.nasa.worldwind.layers.RenderableLayer; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.render.OrderedRenderable; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.OGLStackHandler; import gov.nasa.worldwind.util.OGLUtil; /** * ScreenSelector is an application utility that provides interactive screen * rectangle selection with visual feedback, and tracks the list of objects * intersecting the screen rectangle. The screen rectangle is displayed on a * layer created by ScreenSelector, and is used as the WorldWindow's pick * rectangle to perform object selection. Objects intersecting the screen * rectangle can be accessed by calling {@link #getSelectedObjects()}. * <p/> * <h3>Using ScreenSelector</h3> * <p/> * To use ScreenSelector in an application, create a new instance of * ScreenSelector and specify the application's WorldWindow as the sole * parameter. When the user wants to define a screen selection, call * {@link #enable} and the ScreenSelector then translates mouse events to * changes in the selection rectangle. The selection rectangle is displayed as a * filled rectangle with a 1-pixel wide border drawn in the interiorColor and * borderColor. The ScreenSelector consumes mouse events it responds in order to * suppress navigation events while the user is performing a selection. When the * user selection is complete, call {@link #disable} and the ScreenSelector * stops responding to mouse events. * <p/> * While the ScreenSelector is enabled it keeps track of the objects * intersecting the selection rectangle, which can be accessed by calling * getSelectedObjects. When the list of selected objects changes, SceneSelector * sends SELECTION_STARTED, SELECTION_CHANGED, and SELECTION_ENDED messages to * its message listeners. These three messages correspond to the user starting a * selection, changing what's in the selection, and completing the selection. To * receive a notification when the list of selected objects changes, register a * MessageListener with the SceneSelector by calling * {@link #addMessageListener(gov.nasa.worldwind.event.MessageListener)}. * <p/> * Note that enabling or disabling the ScreenSelector does not change its list * of selected objects. The list of selected objects only changes in response to * user input when the ScreenSelector is enabled. * <p/> * <h3>User Input</h3> * <p/> * When ScreenSelector is enabled, pressing the first mouse button causes * ScreenSelector to set its selection to a rectangle at the cursor with zero * width and height, then clear the list of selected objects. Subsequently * dragging the mouse causes ScreenSelector to update its selection rectangle to * include the starting point and the current cursor point, then update the list * of objects intersecting that rectangle. Finally, releasing the first mouse * button causes ScreenSelector to stop displaying the selection rectangle, but * does not change the list of selected objects. Keeping the list of selected * object available after the selection is complete enables applications to * access the user's final selection by calling getSelectedObjects. * <p/> * To customize ScreenSelector's response to mouse events, create a subclass of * ScreenSelector and override the methods mousePressed, mouseReleased, and * mouseDragged. To customize ScreenSelector's response to screen rectangle * select events, override the method selected. * <p/> * ScreenSelector translates its raw mouse events to the methods * selectionStarted, selectionEnded, and selectionChanged. To customize how * ScreenSelector responds to these semantic events without changing the user * input model, create a subclass of ScreenSelector and override any of these * methods. * <p/> * <h3>Screen Rectangle Appearance</h3> * <p/> * To customize the appearance of the rectangle displayed by ScreenRectangle, * call {@link #setInteriorColor(java.awt.Color)} and * {@link #setBorderColor(java.awt.Color)} to specify the rectangle's interior * and border colors, respectively. Setting either value to <code>null</code> * causes ScreenRectangle to use the default values: 25% opaque white interior, * 100% opaque white border. * <p/> * To further customize the displayed rectangle, create a subclass of * ScreenSelector, override the method createSelectionRectangle, and return a * subclass of the internal class ScreenSelector.SelectionRectangle. * * @author dcollins * @version $Id: ScreenSelector.java 1171 2013-02-11 21:45:02Z dcollins $ */ public class ScreenSelector extends WWObjectImpl implements MouseListener, MouseMotionListener, SelectListener { protected static class SelectionRectangle implements OrderedRenderable { protected static final Color DEFAULT_BORDER_COLOR = Color.BLUE; protected static final Color DEFAULT_INTERIOR_COLOR = new Color(DEFAULT_BORDER_COLOR.getRed(), DEFAULT_BORDER_COLOR.getGreen(), DEFAULT_BORDER_COLOR.getBlue(), 64); protected Rectangle rect; protected Point startPoint; protected Point endPoint; protected Color interiorColor; protected Color borderColor; protected OGLStackHandler BEogsh = new OGLStackHandler(); public SelectionRectangle() { this.rect = new Rectangle(); this.startPoint = new Point(); this.endPoint = new Point(); } public boolean hasSelection() { return !this.rect.isEmpty(); } public Rectangle getSelection() { return this.rect; } public void startSelection( Point point ) { if (point == null) { String msg = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.startPoint.setLocation(point); this.endPoint.setLocation(point); this.rect.setRect(point.x, point.y, 0, 0); } public void endSelection( Point point ) { if (point == null) { String msg = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.endPoint.setLocation(point); // Compute the selection's extremes along the x axis. double minX, maxX; if (this.startPoint.x < this.endPoint.x) { minX = this.startPoint.x; maxX = this.endPoint.x; } else { minX = this.endPoint.x; maxX = this.startPoint.x; } // Compute the selection's extremes along the y axis. The selection // is defined in AWT screen coordinates, so // the origin is in the upper left corner and the y axis points // down. double minY, maxY; if (this.startPoint.y < this.endPoint.y) { minY = this.startPoint.y; maxY = this.endPoint.y; } else { minY = this.endPoint.y; maxY = this.startPoint.y; } // If only one of the selection rectangle's dimensions is zero, then // the selection is either a horizontal or // vertical line. In this case, we set the zero dimension to 1 // because both dimensions must be nonzero to // perform a selection. if (minX == maxX && minY < maxY) maxX = minX + 1; if (minY == maxY && minX < maxX) minY = maxY - 1; this.rect.setRect(minX, maxY, maxX - minX, maxY - minY); } public void clearSelection() { this.startPoint.setLocation(0, 0); this.endPoint.setLocation(0, 0); this.rect.setRect(0, 0, 0, 0); } public Color getInteriorColor() { return this.interiorColor; } public void setInteriorColor( Color color ) { this.interiorColor = color; } public Color getBorderColor() { return this.borderColor; } public void setBorderColor( Color color ) { this.borderColor = color; } public double getDistanceFromEye() { return 0; // Screen rectangle is drawn on top of other ordered // renderables, except other screen objects. } public void pick( DrawContext dc, Point pickPoint ) { // Intentionally left blank. SelectionRectangle is not pickable. } public void render( DrawContext dc ) { if (dc == null) { String msg = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (dc.isOrderedRenderingMode()) this.drawOrderedRenderable(dc); else this.makeOrderedRenderable(dc); } protected void makeOrderedRenderable( DrawContext dc ) { if (this.hasSelection()) dc.addOrderedRenderable(this); } protected void drawOrderedRenderable( DrawContext dc ) { int attrs = GL2.GL_COLOR_BUFFER_BIT // For blend enable, alpha // enable, blend func, alpha // func. | GL2.GL_CURRENT_BIT // For current color. | GL2.GL_DEPTH_BUFFER_BIT; // For depth test disable. Rectangle viewport = dc.getView().getViewport(); Rectangle selection = this.getSelection(); GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 // compatibility. this.BEogsh.pushAttrib(gl, attrs); this.BEogsh.pushClientAttrib(gl, GL2.GL_VERTEX_ARRAY); try { // Configure the modelview-projection matrix to transform vertex // points from screen rectangle // coordinates to clip coordinates without any perspective // transformation. We offset the rectangle by // 0.5 pixels to ensure that the line loop draws a line without // a 1-pixel gap between the line's // beginning and its end. We scale by (width - 1, height - 1) to // ensure that only the actual selected // area is filled. If we scaled by (width, height), GL line // rasterization would fill one pixel beyond // the actual selected area. this.BEogsh.pushProjectionIdentity(gl); gl.glOrtho(0, viewport.getWidth(), 0, viewport.getHeight(), -1, 1); // l, // r, // b, // t, // n, // f this.BEogsh.pushModelviewIdentity(gl); gl.glTranslated(0.5, 0.5, 0.0); gl.glTranslated(selection.getX(), viewport.getHeight() - selection.getY(), 0); gl.glScaled(selection.getWidth() - 1, selection.getHeight() - 1, 1); // Disable the depth test and enable blending so this screen // rectangle appears on top of the existing // framebuffer contents. gl.glDisable(GL.GL_DEPTH_TEST); gl.glEnable(GL.GL_BLEND); OGLUtil.applyBlending(gl, false); // SelectionRectangle does not // use pre-multiplied colors. // Draw this screen rectangle's interior as a filled // quadrilateral. Color c = this.getInteriorColor() != null ? this.getInteriorColor() : DEFAULT_INTERIOR_COLOR; gl.glColor4ub((byte) c.getRed(), (byte) c.getGreen(), (byte) c.getBlue(), (byte) c.getAlpha()); dc.drawUnitQuad(); // Draw this screen rectangle's border as a line loop. This // assumes the default line width of 1.0. c = this.getBorderColor() != null ? this.getBorderColor() : DEFAULT_BORDER_COLOR; gl.glColor4ub((byte) c.getRed(), (byte) c.getGreen(), (byte) c.getBlue(), (byte) c.getAlpha()); dc.drawUnitQuadOutline(); } finally { this.BEogsh.pop(gl); } } } /** * Message type indicating that the user has started their selection. The * ScreenSelector's list of selected objects is empty, and does not change * until a subsequent SELECTION_CHANGED event, if any. If this is followed * by a SELECTION_ENDED event without any SELECTION_CHANGED event in * between, then the user has selected nothing. */ public static final String SELECTION_STARTED = "ScreenSelector.SelectionStarted"; /** * Message type indicating that the list of selected objects has changed. * This may be followed by one or more SELECTION_CHANGED events before the * final SELECTION_ENDED event. */ public static final String SELECTION_CHANGED = "ScreenSelector.SelectionChanged"; /** * Message type indicating that the user has completed their selection. The * ScreenSelector's list of selected objects does not changes until a * subsequent SELECTION_STARTED event, if any. */ public static final String SELECTION_ENDED = "ScreenSelector.SelectionEnded"; protected WorldWindow wwd; protected Layer layer; protected SelectionRectangle selectionRect; protected List<Object> selectedObjects = new ArrayList<Object>(); protected List<MessageListener> messageListeners = new ArrayList<MessageListener>(); protected boolean armed; public ScreenSelector( WorldWindow worldWindow ) { if (worldWindow == null) { String msg = Logging.getMessage("nullValue.WorldWindow"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.wwd = worldWindow; this.layer = this.createLayer(); this.layer.setPickEnabled(false); // The screen selector is not // pickable. this.selectionRect = this.createSelectionRectangle(); ((RenderableLayer) this.layer).addRenderable(this.selectionRect); } protected Layer createLayer() { return new RenderableLayer(); } protected SelectionRectangle createSelectionRectangle() { return new SelectionRectangle(); } public WorldWindow getWwd() { return this.wwd; } public Layer getLayer() { return this.layer; } public Color getInteriorColor() { return this.selectionRect.getInteriorColor(); } public void setInteriorColor( Color color ) { this.selectionRect.setInteriorColor(color); } public Color getBorderColor() { return this.selectionRect.getBorderColor(); } public void setBorderColor( Color color ) { this.selectionRect.setBorderColor(color); } public void enable() { // Clear any existing selection and clear set the SceneController's pick // rectangle. This ensures that this // ScreenSelector starts with the correct state when enabled. this.selectionRect.clearSelection(); this.getWwd().getSceneController().setPickRectangle(null); // Add and enable the layer that displays this ScreenSelector's // selection rectangle. LayerList layers = this.getWwd().getModel().getLayers(); if (!layers.contains(this.getLayer())) layers.add(this.getLayer()); if (!this.getLayer().isEnabled()) this.getLayer().setEnabled(true); // Listen for mouse input on the World Window. this.getWwd().getInputHandler().addMouseListener(this); this.getWwd().getInputHandler().addMouseMotionListener(this); } public void disable() { // Clear the selection, clear the SceneController's pick rectangle, and // stop listening for changes in the pick // rectangle selection. These steps should have been done when the // selection ends, but do them here in case the // caller disables this ScreenSelector before the selection ends. this.selectionRect.clearSelection(); this.getWwd().getSceneController().setPickRectangle(null); this.getWwd().removeSelectListener(this); // Remove the layer that displays this ScreenSelector's selection // rectangle. this.getWwd().getModel().getLayers().remove(this.getLayer()); // Stop listening for mouse input on the world window. this.getWwd().getInputHandler().removeMouseListener(this); this.getWwd().getInputHandler().removeMouseMotionListener(this); } public List< ? > getSelectedObjects() { return this.selectedObjects; } public void addMessageListener( MessageListener listener ) { if (listener == null) { String msg = Logging.getMessage("nullValue.ListenerIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.messageListeners.add(listener); } public void removeMessageListener( MessageListener listener ) { if (listener == null) { String msg = Logging.getMessage("nullValue.ListenerIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.messageListeners.remove(listener); } protected void sendMessage( Message message ) { for( MessageListener listener : this.messageListeners ) { try { listener.onMessage(message); } catch (Exception e) { String msg = Logging.getMessage("generic.ExceptionInvokingMessageListener"); Logging.logger().severe(msg); // Don't throw an exception, just log a severe message and // continue to the next listener. } } } public void mouseClicked( MouseEvent mouseEvent ) { // Intentionally left blank. ScreenSelector does not respond to mouse // clicked events. } public void mousePressed( MouseEvent mouseEvent ) { if (mouseEvent == null) // Ignore null events. return; if (MouseEvent.BUTTON1_DOWN_MASK != mouseEvent.getModifiersEx()) // Respond // to // button // 1 // down // w/o // modifiers. return; this.armed = true; this.selectionStarted(mouseEvent); mouseEvent.consume(); // Consume the mouse event to prevent the view // from responding to it. } public void mouseReleased( MouseEvent mouseEvent ) { if (mouseEvent == null) // Ignore null events. return; if (!this.armed) // Respond to mouse released events when armed. return; this.armed = false; this.selectionEnded(mouseEvent); mouseEvent.consume(); // Consume the mouse event to prevent the view // from responding to it. } public void mouseEntered( MouseEvent mouseEvent ) { // Intentionally left blank. ScreenSelector does not respond to mouse // entered events. } public void mouseExited( MouseEvent mouseEvent ) { // Intentionally left blank. ScreenSelector does not respond to mouse // exited events. } public void mouseDragged( MouseEvent mouseEvent ) { if (mouseEvent == null) // Ignore null events. return; if (!this.armed) // Respond to mouse dragged events when armed. return; this.selectionChanged(mouseEvent); mouseEvent.consume(); // Consume the mouse event to prevent the view // from responding to it. } public void mouseMoved( MouseEvent mouseEvent ) { // Intentionally left blank. ScreenSelector does not respond to mouse // moved events. } protected void selectionStarted( MouseEvent mouseEvent ) { this.selectionRect.startSelection(mouseEvent.getPoint()); this.getWwd().getSceneController().setPickRectangle(null); this.getWwd().addSelectListener(this); // Listen for changes in the pick // rectangle selection. this.getWwd().redraw(); // Clear the list of selected objects and send a message indicating that // the user has started a selection. this.selectedObjects.clear(); this.sendMessage(new Message(SELECTION_STARTED, this)); } @SuppressWarnings({"UnusedParameters"}) protected void selectionEnded( MouseEvent mouseEvent ) { this.selectionRect.clearSelection(); this.getWwd().getSceneController().setPickRectangle(null); this.getWwd().removeSelectListener(this); // Stop listening for changes // the pick rectangle // selection. this.getWwd().redraw(); // Send a message indicating that the user has completed their // selection. We don't clear the list of selected // objects in order to preserve the list of selected objects for the // caller. this.sendMessage(new Message(SELECTION_ENDED, this)); } protected void selectionChanged( MouseEvent mouseEvent ) { // Limit the end point to the World Window's viewport rectangle. This // ensures that a user drag event to define // the selection does not exceed the viewport and the viewing frustum. // This is only necessary during mouse drag // events because those events are reported when the cursor is outside // the World Window's viewport. Point p = this.limitPointToWorldWindow(mouseEvent.getPoint()); // Specify the selection's end point and set the scene controller's pick // rectangle to the selected rectangle. // We create a copy of the selected rectangle to insulate the scene // controller from changes to rectangle // returned by ScreenRectangle.getSelection. this.selectionRect.endSelection(p); this.getWwd().getSceneController() .setPickRectangle(this.selectionRect.hasSelection() ? new Rectangle(this.selectionRect.getSelection()) : null); this.getWwd().redraw(); } /** * Limits the specified point's x and y coordinates to the World Window's * viewport, and returns a new point with the limited coordinates. For * example, if the World Window's viewport rectangle is x=0, y=0, width=100, * height=100 and the point's coordinates are x=50, y=200 this returns a new * point with coordinates x=50, y=100. If the specified point is already * inside the World Window's viewport, this returns a new point with the * same x and y coordinates as the specified point. * * @param point * the point to limit. * * @return a new Point representing the specified point limited to the World * Window's viewport rectangle. */ protected Point limitPointToWorldWindow( Point point ) { Rectangle viewport = this.getWwd().getView().getViewport(); int x = point.x; if (x < viewport.x) x = viewport.x; if (x > viewport.x + viewport.width) x = viewport.x + viewport.width; int y = point.y; if (y < viewport.y) y = viewport.y; if (y > viewport.y + viewport.height) y = viewport.y + viewport.height; return new Point(x, y); } public void selected( SelectEvent event ) { try { // Respond to box rollover select events when armed. if (event.getEventAction().equals(SelectEvent.BOX_ROLLOVER) && this.armed) this.selectObjects(event.getAllTopObjects()); } catch (Exception e) { // Wrap the handler in a try/catch to keep exceptions from bubbling // up JGTLogger.logError(this, e); } } protected void selectObjects( List< ? > list ) { if (this.selectedObjects.equals(list)) return; // Same thing selected. this.selectedObjects.clear(); // If the selection is empty, then we've cleared the list of selected // objects and there's nothing left to do. // Otherwise, we add the selected objects to our list. if (list != null) this.selectedObjects.addAll(list); // Send a message indicating that the user has ended selection. We don't // clear the list of selected objects // in order to preserve the list of selected objects for the caller. this.sendMessage(new Message(SELECTION_CHANGED, this)); } }