/* * 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.Component; import java.awt.Cursor; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import gov.nasa.worldwind.Movable; import gov.nasa.worldwind.View; import gov.nasa.worldwind.WWObjectImpl; import gov.nasa.worldwind.WorldWindow; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.event.DragSelectEvent; import gov.nasa.worldwind.event.RenderingEvent; import gov.nasa.worldwind.event.RenderingListener; import gov.nasa.worldwind.event.SelectEvent; import gov.nasa.worldwind.event.SelectListener; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Intersection; import gov.nasa.worldwind.geom.Line; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.EllipsoidalGlobe; import gov.nasa.worldwind.layers.Layer; import gov.nasa.worldwind.layers.LayerList; import gov.nasa.worldwind.layers.RenderableLayer; import gov.nasa.worldwind.pick.PickedObject; import gov.nasa.worldwind.pick.PickedObjectList; import gov.nasa.worldwind.render.BasicShapeAttributes; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.render.Material; import gov.nasa.worldwind.render.ShapeAttributes; import gov.nasa.worldwind.render.SurfaceSector; import gov.nasa.worldwind.util.Logging; /** * Provides an interactive region selector. To use, construct and call enable/disable. Register a property listener to * receive changes to the sector as they occur, or just wait until the user is done and then query the result via {@link * #getSector()}. * * @author tag * @version $Id: SectorSelector.java 1171 2013-02-11 21:45:02Z dcollins $ */ public class SectorSelector extends WWObjectImpl implements SelectListener, MouseListener, MouseMotionListener, RenderingListener { public final static String SECTOR_PROPERTY = "gov.nasa.worldwind.SectorSelector"; protected static final int NONE = 0; protected static final int MOVING = 1; protected static final int SIZING = 2; protected static final int NORTH = 1; protected static final int SOUTH = 2; protected static final int EAST = 4; protected static final int WEST = 8; protected static final int NORTHWEST = NORTH + WEST; protected static final int NORTHEAST = NORTH + EAST; protected static final int SOUTHWEST = SOUTH + WEST; protected static final int SOUTHEAST = SOUTH + EAST; private final WorldWindow wwd; private final Layer layer; private final RegionShape shape; private double edgeFactor = 0.10; // state tracking fields private boolean armed = false; private int operation = NONE; private int side = NONE; private Position previousPosition = null; private Sector previousSector = null; public SectorSelector(WorldWindow worldWindow) { if (worldWindow == null) { String msg = Logging.getMessage("nullValue.WorldWindow"); Logging.logger().log(java.util.logging.Level.SEVERE, msg); throw new IllegalArgumentException(msg); } this.wwd = worldWindow; this.layer = new RenderableLayer(); this.shape = new RegionShape(Sector.EMPTY_SECTOR); ((RenderableLayer) this.layer).addRenderable(this.shape); } protected SectorSelector(WorldWindow worldWindow, RegionShape shape, RenderableLayer rLayer) { if (worldWindow == null) { String msg = Logging.getMessage("nullValue.WorldWindow"); Logging.logger().log(java.util.logging.Level.SEVERE, msg); throw new IllegalArgumentException(msg); } if (shape == null) { String msg = Logging.getMessage("nullValue.Shape"); Logging.logger().log(java.util.logging.Level.SEVERE, msg); throw new IllegalArgumentException(msg); } if (rLayer == null) { String msg = Logging.getMessage("nullValue.Layer"); Logging.logger().log(java.util.logging.Level.SEVERE, msg); throw new IllegalArgumentException(msg); } this.wwd = worldWindow; this.shape = shape; this.layer = rLayer; rLayer.addRenderable(this.shape); } public WorldWindow getWwd() { return wwd; } public Layer getLayer() { return layer; } public void enable() { this.getShape().setStartPosition(null); LayerList layers = this.getWwd().getModel().getLayers(); if (!layers.contains(this.getLayer())) layers.add(this.getLayer()); if (!this.getLayer().isEnabled()) this.getLayer().setEnabled(true); this.setArmed(true); this.getWwd().addRenderingListener(this); this.getWwd().addSelectListener(this); this.getWwd().getInputHandler().addMouseListener(this); this.getWwd().getInputHandler().addMouseMotionListener(this); this.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } public void disable() { this.getWwd().removeRenderingListener(this); this.getWwd().removeSelectListener(this); this.getWwd().getInputHandler().removeMouseListener(this); this.getWwd().getInputHandler().removeMouseMotionListener(this); this.getWwd().getModel().getLayers().remove(this.getLayer()); this.getShape().clear(); } public Sector getSector() { return this.getShape().hasSelection() ? this.getShape().getSector() : null; // TODO: Determine how to handle date-line spanning sectors. } public Color getInteriorColor() { return this.getShape().getInteriorColor(); } public void setInteriorColor(Color color) { this.getShape().setInteriorColor(color); } public Color getBorderColor() { return this.getShape().getBorderColor(); } public void setBorderColor(Color color) { this.getShape().setBorderColor(color); } public double getInteriorOpacity() { return this.getShape().getInteriorOpacity(); } public void setInteriorOpacity(double opacity) { this.getShape().setInteriorOpacity(opacity); } public double getBorderOpacity() { return this.getShape().getBorderOpacity(); } public void setBorderOpacity(double opacity) { this.getShape().setBorderOpacity(opacity); } public double getBorderWidth() { return this.getShape().getBorderWidth(); } public void setBorderWidth(double width) { this.getShape().setBorderWidth(width); } protected RegionShape getShape() { return shape; } protected boolean isArmed() { return armed; } protected void setArmed(boolean armed) { this.armed = armed; } protected int getOperation() { return operation; } protected void setOperation(int operation) { this.operation = operation; } protected int getSide() { return side; } protected void setSide(int side) { this.side = side; } protected Position getPreviousPosition() { return previousPosition; } protected void setPreviousPosition(Position previousPosition) { this.previousPosition = previousPosition; } protected double getEdgeFactor() { return edgeFactor; } protected void setEdgeFactor(double edgeFactor) { this.edgeFactor = edgeFactor; } public void stageChanged(RenderingEvent event) { if (!event.getStage().equals(RenderingEvent.AFTER_BUFFER_SWAP)) return; // We notify of changes during this rendering stage because the sector is updated within the region shape's // render method. this.notifySectorChanged(); } protected void notifySectorChanged() { if (this.getShape().hasSelection() && this.getSector() != null && !this.getSector().equals(this.previousSector)) { this.firePropertyChange(SECTOR_PROPERTY, this.previousSector, this.getShape().getSector()); this.previousSector = this.getSector(); } } // // Mouse events are used to initiate and track initial drawing of the region. When the selector is enabled it is // "armed", meaning that the next mouse press on the globe will initiate the region selection and display. The // selector is then disarmed so that subsequent mouse presses either size or move the region when they occur on // the region, or move the globe if they occur outside the region. // public void mousePressed(MouseEvent mouseEvent) { if (MouseEvent.BUTTON1_DOWN_MASK != mouseEvent.getModifiersEx()) return; if (!this.isArmed()) return; this.getShape().setResizeable(true); this.getShape().setStartPosition(null); this.setArmed(false); mouseEvent.consume(); } public void mouseReleased(MouseEvent mouseEvent) { if (MouseEvent.BUTTON1 != mouseEvent.getButton()) return; if (this.getShape().isResizeable()) this.setCursor(null); this.getShape().setResizeable(false); mouseEvent.consume(); // prevent view operations this.firePropertyChange(SECTOR_PROPERTY, this.previousSector, null); } public void mouseDragged(MouseEvent mouseEvent) { if (MouseEvent.BUTTON1_DOWN_MASK != mouseEvent.getModifiersEx()) return; if (this.getShape().isResizeable()) mouseEvent.consume(); // prevent view operations } public void mouseClicked(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mouseMoved(MouseEvent e) { } // // Selection events are used to resize and move the region // public void selected(SelectEvent event) { if (event == null) { String msg = Logging.getMessage("nullValue.EventIsNull"); Logging.logger().log(java.util.logging.Level.FINE, msg); throw new IllegalArgumentException(msg); } if (this.getOperation() == NONE && event.getTopObject() != null && !(event.getTopPickedObject().getParentLayer() == this.layer)) { // this.setCursor(null); return; } if (event.getEventAction().equals(SelectEvent.LEFT_PRESS)) { this.setPreviousPosition(this.getWwd().getCurrentPosition()); } else if (event.getEventAction().equals(SelectEvent.DRAG)) { DragSelectEvent dragEvent = (DragSelectEvent) event; Object topObject = dragEvent.getTopObject(); if (topObject == null) return; RegionShape dragObject = this.getShape(); if (this.getOperation() == SIZING) { Sector newSector = this.resizeShape(dragObject, this.getSide()); if (newSector != null) dragObject.setSector(newSector); event.consume(); } else { this.setSide(this.determineAdjustmentSide(dragObject, this.getEdgeFactor())); if (this.getSide() == NONE || this.getOperation() == MOVING) { this.setOperation(MOVING); this.dragWholeShape(dragEvent, dragObject); } else { Sector newSector = this.resizeShape(dragObject, this.getSide()); if (newSector != null) dragObject.setSector(newSector); this.setOperation(SIZING); } event.consume(); } this.setPreviousPosition(this.getWwd().getCurrentPosition()); this.notifySectorChanged(); } else if (event.getEventAction().equals(SelectEvent.DRAG_END)) { this.setOperation(NONE); this.setPreviousPosition(null); } else if (event.getEventAction().equals(SelectEvent.ROLLOVER) && this.getOperation() == NONE) { if (!(this.getWwd() instanceof Component)) return; if (event.getTopObject() == null || event.getTopPickedObject().isTerrain()) { this.setCursor(null); return; } if (!(event.getTopObject() instanceof Movable)) return; this.setCursor(this.determineAdjustmentSide((Movable) event.getTopObject(), this.getEdgeFactor())); } } protected int determineAdjustmentSide(Movable dragObject, double factor) { if (dragObject instanceof SurfaceSector) { SurfaceSector quad = (SurfaceSector) dragObject; Sector s = quad.getSector(); // TODO: go over all sectors Position p = this.getWwd().getCurrentPosition(); if (p == null) { return NONE; } double dN = abs(s.getMaxLatitude().subtract(p.getLatitude()).degrees); double dS = abs(s.getMinLatitude().subtract(p.getLatitude()).degrees); double dW = abs(s.getMinLongitude().subtract(p.getLongitude()).degrees); double dE = abs(s.getMaxLongitude().subtract(p.getLongitude()).degrees); double sLat = factor * s.getDeltaLatDegrees(); double sLon = factor * s.getDeltaLonDegrees(); if (dN < sLat && dW < sLon) return NORTHWEST; if (dN < sLat && dE < sLon) return NORTHEAST; if (dS < sLat && dW < sLon) return SOUTHWEST; if (dS < sLat && dE < sLon) return SOUTHEAST; if (dN < sLat) return NORTH; if (dS < sLat) return SOUTH; if (dW < sLon) return WEST; if (dE < sLon) return EAST; } return NONE; } protected Sector resizeShape(Movable dragObject, int side) { if (dragObject instanceof SurfaceSector) { SurfaceSector quad = (SurfaceSector) dragObject; Sector s = quad.getSector(); // TODO: go over all sectors Position p = this.getWwd().getCurrentPosition(); if (p == null || this.getPreviousPosition() == null) { return null; } Angle dLat = p.getLatitude().subtract(this.getPreviousPosition().getLatitude()); Angle dLon = p.getLongitude().subtract(this.getPreviousPosition().getLongitude()); Angle newMinLat = s.getMinLatitude(); Angle newMinLon = s.getMinLongitude(); Angle newMaxLat = s.getMaxLatitude(); Angle newMaxLon = s.getMaxLongitude(); if (side == NORTH) { newMaxLat = s.getMaxLatitude().add(dLat); } else if (side == SOUTH) { newMinLat = s.getMinLatitude().add(dLat); } else if (side == EAST) { newMaxLon = s.getMaxLongitude().add(dLon); } else if (side == WEST) { newMinLon = s.getMinLongitude().add(dLon); } else if (side == NORTHWEST) { newMaxLat = s.getMaxLatitude().add(dLat); newMinLon = s.getMinLongitude().add(dLon); } else if (side == NORTHEAST) { newMaxLat = s.getMaxLatitude().add(dLat); newMaxLon = s.getMaxLongitude().add(dLon); } else if (side == SOUTHWEST) { newMinLat = s.getMinLatitude().add(dLat); newMinLon = s.getMinLongitude().add(dLon); } else if (side == SOUTHEAST) { newMinLat = s.getMinLatitude().add(dLat); newMaxLon = s.getMaxLongitude().add(dLon); } return new Sector(newMinLat, newMaxLat, newMinLon, newMaxLon); } return null; } private static double abs(double a) { return a >= 0 ? a : -a; } protected void dragWholeShape(DragSelectEvent dragEvent, Movable dragObject) { View view = getWwd().getView(); EllipsoidalGlobe globe = (EllipsoidalGlobe) getWwd().getModel().getGlobe(); // Compute ref-point position in screen coordinates. Position refPos = dragObject.getReferencePosition(); if (refPos == null) return; Vec4 refPoint = globe.computePointFromPosition(refPos); Vec4 screenRefPoint = view.project(refPoint); // Compute screen-coord delta since last event. int dx = dragEvent.getPickPoint().x - dragEvent.getPreviousPickPoint().x; int dy = dragEvent.getPickPoint().y - dragEvent.getPreviousPickPoint().y; // Find intersection of screen coord ref-point with globe. double x = screenRefPoint.x + dx; double y = dragEvent.getMouseEvent().getComponent().getSize().height - screenRefPoint.y + dy - 1; Line ray = view.computeRayFromScreenPoint(x, y); Intersection inters[] = globe.intersect(ray, refPos.getElevation()); if (inters != null) { // Intersection with globe. Move reference point to the intersection point. Position p = globe.computePositionFromPoint(inters[0].getIntersectionPoint()); dragObject.moveTo(p); } } protected void setCursor(int sideName) { Cursor cursor = null; switch (sideName) { case NONE: cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); break; case NORTH: cursor = Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR); break; case SOUTH: cursor = Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR); break; case EAST: cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR); break; case WEST: cursor = Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR); break; case NORTHWEST: cursor = Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR); break; case NORTHEAST: cursor = Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR); break; case SOUTHWEST: cursor = Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR); break; case SOUTHEAST: cursor = Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR); break; } this.setCursor(cursor); } protected void setCursor(Cursor cursor) { ((Component) this.getWwd()).setCursor(cursor != null ? cursor : Cursor.getDefaultCursor()); } protected static class RegionShape extends SurfaceSector { private boolean resizeable = false; private Position startPosition; private Position endPosition; private SurfaceSector borderShape; protected RegionShape(Sector sector) { super(sector); // Create the default border shape. this.setBorder(new SurfaceSector(sector)); // The edges of the region shape should be constant lines of latitude and longitude. this.setPathType(AVKey.LINEAR); this.getBorder().setPathType(AVKey.LINEAR); // Setup default interior rendering attributes. Note that the interior rendering attributes are // configured so only the SurfaceSector's interior is rendered. ShapeAttributes interiorAttrs = new BasicShapeAttributes(); interiorAttrs.setDrawOutline(false); interiorAttrs.setInteriorMaterial(new Material(Color.WHITE)); interiorAttrs.setInteriorOpacity(0.1); this.setAttributes(interiorAttrs); this.setHighlightAttributes(interiorAttrs); // Setup default border rendering attributes. Note that the border rendering attributes are configured // so that only the SurfaceSector's outline is rendered. ShapeAttributes borderAttrs = new BasicShapeAttributes(); borderAttrs.setDrawInterior(false); borderAttrs.setOutlineMaterial(new Material(Color.RED)); borderAttrs.setOutlineOpacity(0.7); borderAttrs.setOutlineWidth(3); this.getBorder().setAttributes(borderAttrs); this.getBorder().setHighlightAttributes(borderAttrs); } public Color getInteriorColor() { return this.getAttributes().getInteriorMaterial().getDiffuse(); } public void setInteriorColor(Color color) { ShapeAttributes attr = this.getAttributes(); attr.setInteriorMaterial(new Material(color)); this.setAttributes(attr); } public Color getBorderColor() { return this.getBorder().getAttributes().getOutlineMaterial().getDiffuse(); } public void setBorderColor(Color color) { ShapeAttributes attr = this.getBorder().getAttributes(); attr.setOutlineMaterial(new Material(color)); this.getBorder().setAttributes(attr); } public double getInteriorOpacity() { return this.getAttributes().getInteriorOpacity(); } public void setInteriorOpacity(double opacity) { ShapeAttributes attr = this.getAttributes(); attr.setInteriorOpacity(opacity); this.setAttributes(attr); } public double getBorderOpacity() { return this.getBorder().getAttributes().getOutlineOpacity(); } public void setBorderOpacity(double opacity) { ShapeAttributes attr = this.getBorder().getAttributes(); attr.setOutlineOpacity(opacity); this.getBorder().setAttributes(attr); } public double getBorderWidth() { return this.getBorder().getAttributes().getOutlineWidth(); } public void setBorderWidth(double width) { ShapeAttributes attr = this.getBorder().getAttributes(); attr.setOutlineWidth(width); this.getBorder().setAttributes(attr); } public void setSector(Sector sector) { super.setSector(sector); this.getBorder().setSector(sector); } protected boolean isResizeable() { return resizeable; } protected void setResizeable(boolean resizeable) { this.resizeable = resizeable; } protected Position getStartPosition() { return startPosition; } protected void setStartPosition(Position startPosition) { this.startPosition = startPosition; } protected Position getEndPosition() { return endPosition; } protected void setEndPosition(Position endPosition) { this.endPosition = endPosition; } protected SurfaceSector getBorder() { return borderShape; } protected void setBorder(SurfaceSector shape) { if (shape == null) { String message = Logging.getMessage("nullValue.Shape"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.borderShape = shape; } protected boolean hasSelection() { return getStartPosition() != null && getEndPosition() != null; } protected void clear() { this.setStartPosition(null); this.setEndPosition(null); this.setSector(Sector.EMPTY_SECTOR); } public void preRender(DrawContext dc) { // This is called twice: once during normal rendering, then again during ordered surface rendering. During // normal renering we pre-render both the interior and border shapes. During ordered surface rendering, both // shapes are already added to the DrawContext and both will be individually processed. Therefore we just // call our superclass behavior if (dc.isOrderedRenderingMode()) { super.preRender(dc); return; } this.doPreRender(dc); } @Override public void render(DrawContext dc) { if (dc.isPickingMode() && this.isResizeable()) return; // This is called twice: once during normal rendering, then again during ordered surface rendering. During // normal renering we render both the interior and border shapes. During ordered surface rendering, both // shapes are already added to the DrawContext and both will be individually processed. Therefore we just // call our superclass behavior if (dc.isOrderedRenderingMode()) { super.render(dc); return; } if (!this.isResizeable()) { if (this.hasSelection()) { this.doRender(dc); } return; } PickedObjectList pos = dc.getPickedObjects(); PickedObject terrainObject = pos != null ? pos.getTerrainObject() : null; if (terrainObject == null) return; if (this.getStartPosition() != null) { Position end = terrainObject.getPosition(); if (!this.getStartPosition().equals(end)) { this.setEndPosition(end); this.setSector(Sector.boundingSector(this.getStartPosition(), this.getEndPosition())); this.doRender(dc); } } else { this.setStartPosition(pos.getTerrainObject().getPosition()); } } protected void doPreRender(DrawContext dc) { this.doPreRenderInterior(dc); this.doPreRenderBorder(dc); } protected void doPreRenderInterior(DrawContext dc) { super.preRender(dc); } protected void doPreRenderBorder(DrawContext dc) { this.getBorder().preRender(dc); } protected void doRender(DrawContext dc) { this.doRenderInterior(dc); this.doRenderBorder(dc); } protected void doRenderInterior(DrawContext dc) { super.render(dc); } protected void doRenderBorder(DrawContext dc) { this.getBorder().render(dc); } } }