// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui;
import java.awt.Component;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.LinkedList;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.SelectByInternalPointAction;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
/**
* Manages the selection of a rectangle. Listening to left and right mouse button
* presses and to mouse motions and draw the rectangle accordingly.
*
* Left mouse button selects a rectangle from the press until release. Pressing
* right mouse button while left is still pressed enable the rectangle to move
* around. Releasing the left button fires an action event to the listener given
* at constructor, except if the right is still pressed, which just remove the
* selection rectangle and does nothing.
*
* The point where the left mouse button was pressed and the current mouse
* position are two opposite corners of the selection rectangle.
*
* It is possible to specify an aspect ratio (width per height) which the
* selection rectangle always must have. In this case, the selection rectangle
* will be the largest window with this aspect ratio, where the position the left
* mouse button was pressed and the corner of the current mouse position are at
* opposite sites (the mouse position corner is the corner nearest to the mouse
* cursor).
*
* When the left mouse button was released, an ActionEvent is send to the
* ActionListener given at constructor. The source of this event is this manager.
*
* @author imi
*/
public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener {
/**
* This is the interface that an user of SelectionManager has to implement
* to get informed when a selection closes.
* @author imi
*/
public interface SelectionEnded {
/**
* Called, when the left mouse button was released.
* @param r The rectangle that is currently the selection.
* @param e The mouse event.
* @see InputEvent#getModifiersEx()
*/
public void selectionEnded(Rectangle r, MouseEvent e);
/**
* Called to register the selection manager for "active" property.
* @param listener The listener to register
*/
public void addPropertyChangeListener(PropertyChangeListener listener);
/**
* Called to remove the selection manager from the listener list
* for "active" property.
* @param listener The listener to register
*/
public void removePropertyChangeListener(PropertyChangeListener listener);
}
/**
* The listener that receives the events after left mouse button is released.
*/
private final SelectionEnded selectionEndedListener;
/**
* Position of the map when the mouse button was pressed.
* If this is not <code>null</code>, a rectangle is drawn on screen.
*/
private Point mousePosStart;
/**
* Position of the map when the selection rectangle was last drawn.
*/
private Point mousePos;
/**
* The Component, the selection rectangle is drawn onto.
*/
private final NavigatableComponent nc;
/**
* Whether the selection rectangle must obtain the aspect ratio of the
* drawComponent.
*/
private boolean aspectRatio;
private boolean lassoMode;
private Polygon lasso = new Polygon();
/**
* Create a new SelectionManager.
*
* @param selectionEndedListener The action listener that receives the event when
* the left button is released.
* @param aspectRatio If true, the selection window must obtain the aspect
* ratio of the drawComponent.
* @param navComp The component, the rectangle is drawn onto.
*/
public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
this.selectionEndedListener = selectionEndedListener;
this.aspectRatio = aspectRatio;
this.nc = navComp;
}
/**
* Register itself at the given event source.
* @param eventSource The emitter of the mouse events.
* @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
*/
public void register(NavigatableComponent eventSource, boolean lassoMode) {
this.lassoMode = lassoMode;
eventSource.addMouseListener(this);
eventSource.addMouseMotionListener(this);
selectionEndedListener.addPropertyChangeListener(this);
eventSource.addPropertyChangeListener("scale", new PropertyChangeListener(){
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (mousePosStart != null) {
paintRect();
mousePos = mousePosStart = null;
}
}
});
}
/**
* Unregister itself from the given event source. If a selection rectangle is
* shown, hide it first.
*
* @param eventSource The emitter of the mouse events.
*/
public void unregister(Component eventSource) {
eventSource.removeMouseListener(this);
eventSource.removeMouseMotionListener(this);
selectionEndedListener.removePropertyChangeListener(this);
}
/**
* If the correct button, from the "drawing rectangle" mode
*/
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1) {
SelectByInternalPointAction.performSelection(Main.map.mapView.getEastNorth(e.getX(), e.getY()),
(e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) > 0,
(e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0);
} else if (e.getButton() == MouseEvent.BUTTON1) {
mousePosStart = mousePos = e.getPoint();
lasso.reset();
lasso.addPoint(mousePosStart.x, mousePosStart.y);
}
}
/**
* If the correct button is hold, draw the rectangle.
*/
@Override
public void mouseDragged(MouseEvent e) {
int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK);
if (buttonPressed != 0) {
if (mousePosStart == null) {
mousePosStart = mousePos = e.getPoint();
}
if (!lassoMode) {
paintRect();
}
}
if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) {
mousePos = e.getPoint();
if (lassoMode) {
paintLasso();
} else {
paintRect();
}
} else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) {
mousePosStart.x += e.getX()-mousePos.x;
mousePosStart.y += e.getY()-mousePos.y;
mousePos = e.getPoint();
paintRect();
}
}
/**
* Check the state of the keys and buttons and set the selection accordingly.
*/
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1)
return;
if (mousePos == null || mousePosStart == null)
return; // injected release from outside
// disable the selection rect
Rectangle r;
if (!lassoMode) {
nc.requestClearRect();
r = getSelectionRectangle();
lasso = rectToPolygon(r);
} else {
nc.requestClearPoly();
lasso.addPoint(mousePos.x, mousePos.y);
r = lasso.getBounds();
}
mousePosStart = null;
mousePos = null;
if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) {
selectionEndedListener.selectionEnded(r, e);
}
}
/**
* Draws a selection rectangle on screen.
*/
private void paintRect() {
if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
return;
nc.requestPaintRect(getSelectionRectangle());
}
private void paintLasso() {
if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) {
return;
}
lasso.addPoint(mousePos.x, mousePos.y);
nc.requestPaintPoly(lasso);
}
/**
* Calculate and return the current selection rectangle
* @return A rectangle that spans from mousePos to mouseStartPos
*/
private Rectangle getSelectionRectangle() {
int x = mousePosStart.x;
int y = mousePosStart.y;
int w = mousePos.x - mousePosStart.x;
int h = mousePos.y - mousePosStart.y;
if (w < 0) {
x += w;
w = -w;
}
if (h < 0) {
y += h;
h = -h;
}
if (aspectRatio) {
/* Keep the aspect ratio by growing the rectangle; the
* rectangle is always under the cursor. */
double aspectRatio = (double)nc.getWidth()/nc.getHeight();
if ((double)w/h < aspectRatio) {
int neww = (int)(h*aspectRatio);
if (mousePos.x < mousePosStart.x) {
x += w - neww;
}
w = neww;
} else {
int newh = (int)(w/aspectRatio);
if (mousePos.y < mousePosStart.y) {
y += h - newh;
}
h = newh;
}
}
return new Rectangle(x,y,w,h);
}
/**
* If the action goes inactive, remove the selection rectangle from screen
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("active".equals(evt.getPropertyName()) && !(Boolean)evt.getNewValue() && mousePosStart != null) {
paintRect();
mousePosStart = null;
mousePos = null;
}
}
/**
* Return a list of all objects in the selection, respecting the different
* modifier.
*
* @param alt Whether the alt key was pressed, which means select all
* objects that are touched, instead those which are completely covered.
* @return The collection of selected objects.
*/
public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
Collection<OsmPrimitive> selection = new LinkedList<>();
// whether user only clicked, not dragged.
boolean clicked = false;
Rectangle bounding = lasso.getBounds();
if (bounding.height <= 2 && bounding.width <= 2) {
clicked = true;
}
if (clicked) {
Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]);
OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false);
if (osm != null) {
selection.add(osm);
}
} else {
// nodes
for (Node n : nc.getCurrentDataSet().getNodes()) {
if (n.isSelectable() && lasso.contains(nc.getPoint2D(n))) {
selection.add(n);
}
}
// ways
for (Way w : nc.getCurrentDataSet().getWays()) {
if (!w.isSelectable() || w.getNodesCount() == 0) {
continue;
}
if (alt) {
for (Node n : w.getNodes()) {
if (!n.isIncomplete() && lasso.contains(nc.getPoint2D(n))) {
selection.add(w);
break;
}
}
} else {
boolean allIn = true;
for (Node n : w.getNodes()) {
if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) {
allIn = false;
break;
}
}
if (allIn) {
selection.add(w);
}
}
}
}
return selection;
}
private Polygon rectToPolygon(Rectangle r) {
Polygon poly = new Polygon();
poly.addPoint(r.x, r.y);
poly.addPoint(r.x, r.y + r.height);
poly.addPoint(r.x + r.width, r.y + r.height);
poly.addPoint(r.x + r.width, r.y);
return poly;
}
/**
* Enables or disables the lasso mode.
* @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
*/
public void setLassoMode(boolean lassoMode) {
this.lassoMode = lassoMode;
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
@Override
public void mouseMoved(MouseEvent e) {}
}