// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.actions.mapmode;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.MergeNodesAction;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.MoveCommand;
import org.openstreetmap.josm.command.RotateCommand;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WaySegment;
import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
import org.openstreetmap.josm.data.osm.visitor.paint.SimplePaintVisitor;
import org.openstreetmap.josm.gui.ExtendedDialog;
import org.openstreetmap.josm.gui.MapFrame;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.SelectionManager;
import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.PlatformHookOsx;
import org.openstreetmap.josm.tools.Shortcut;
/**
* Move is an action that can move all kind of OsmPrimitives (except keys for now).
*
* If an selected object is under the mouse when dragging, move all selected objects.
* If an unselected object is under the mouse when dragging, it becomes selected
* and will be moved.
* If no object is under the mouse, move all selected objects (if any)
*
* @author imi
*/
public class SelectAction extends MapMode implements SelectionEnded {
//static private final Logger logger = Logger.getLogger(SelectAction.class.getName());
/**
* Replies true if we are currently running on OSX
*
* @return true if we are currently running on OSX
*/
public static boolean isPlatformOsx() {
return Main.platform != null
&& Main.platform instanceof PlatformHookOsx;
}
enum Mode { move, rotate, select }
private Mode mode = null;
private long mouseDownTime = 0;
private boolean didMove = false;
private boolean cancelDrawMode = false;
private Node virtualNode = null;
private Collection<WaySegment> virtualWays = new ArrayList<WaySegment>();
/**
* The old cursor before the user pressed the mouse button.
*/
private Cursor oldCursor;
/**
* The position of the mouse before the user moves a node.
*/
private Point mousePos;
private SelectionManager selectionManager;
/**
* The time which needs to pass between click and release before something
* counts as a move, in milliseconds
*/
private int initialMoveDelay;
/**
* The screen distance which needs to be travelled before something
* counts as a move, in pixels
*/
private int initialMoveThreshold;
private boolean initialMoveThresholdExceeded = false;
/**
* Create a new SelectAction
* @param mapFrame The MapFrame this action belongs to.
*/
public SelectAction(MapFrame mapFrame) {
super(tr("Select"), "move/move", tr("Select, move and rotate objects"),
Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.GROUP_EDIT),
mapFrame,
getCursor("normal", "selection", Cursor.DEFAULT_CURSOR));
putValue("help", "Action/Move/Move");
selectionManager = new SelectionManager(this, false, mapFrame.mapView);
initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold",5);
}
private static Cursor getCursor(String name, String mod, int def) {
try {
return ImageProvider.getCursor(name, mod);
} catch (Exception e) {
}
return Cursor.getPredefinedCursor(def);
}
private void setCursor(Cursor c) {
if (oldCursor == null) {
oldCursor = Main.map.mapView.getCursor();
Main.map.mapView.setCursor(c);
}
}
private void restoreCursor() {
if (oldCursor != null) {
Main.map.mapView.setCursor(oldCursor);
oldCursor = null;
}
}
@Override public void enterMode() {
super.enterMode();
Main.map.mapView.addMouseListener(this);
Main.map.mapView.addMouseMotionListener(this);
Main.map.mapView.setVirtualNodesEnabled(
Main.pref.getInteger("mappaint.node.virtual-size", 8) != 0);
}
@Override public void exitMode() {
super.exitMode();
selectionManager.unregister(Main.map.mapView);
Main.map.mapView.removeMouseListener(this);
Main.map.mapView.removeMouseMotionListener(this);
Main.map.mapView.setVirtualNodesEnabled(false);
}
/**
* If the left mouse button is pressed, move all currently selected
* objects (if one of them is under the mouse) or the current one under the
* mouse (which will become selected).
*/
@Override public void mouseDragged(MouseEvent e) {
if(!Main.map.mapView.isActiveLayerVisible())
return;
cancelDrawMode = true;
if (mode == Mode.select) return;
// do not count anything as a move if it lasts less than 100 milliseconds.
if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)) return;
if(mode != Mode.rotate) // button is pressed in rotate mode
if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
return;
if (mode == Mode.move) {
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
if (mousePos == null) {
mousePos = e.getPoint();
return;
}
if (!initialMoveThresholdExceeded) {
int dxp = mousePos.x - e.getX();
int dyp = mousePos.y - e.getY();
int dp = (int) Math.sqrt(dxp*dxp+dyp*dyp);
if (dp < initialMoveThreshold) return;
initialMoveThresholdExceeded = true;
}
EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
EastNorth mouseStartEN = Main.map.mapView.getEastNorth(mousePos.x, mousePos.y);
double dx = mouseEN.east() - mouseStartEN.east();
double dy = mouseEN.north() - mouseStartEN.north();
if (dx == 0 && dy == 0)
return;
if (virtualWays.size() > 0) {
Collection<Command> virtualCmds = new LinkedList<Command>();
virtualCmds.add(new AddCommand(virtualNode));
for (WaySegment virtualWay : virtualWays) {
Way w = virtualWay.way;
Way wnew = new Way(w);
wnew.addNode(virtualWay.lowerIndex+1, virtualNode);
virtualCmds.add(new ChangeCommand(w, wnew));
}
virtualCmds.add(new MoveCommand(virtualNode, dx, dy));
String text = trn("Add and move a virtual new node to way",
"Add and move a virtual new node to {0} ways", virtualWays.size(),
virtualWays.size());
Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
selectPrims(Collections.singleton((OsmPrimitive)virtualNode), false, false, false, false);
virtualWays.clear();
virtualNode = null;
} else {
// Currently we support moving and rotating, which do not affect relations.
// So don't add them in the first place to make handling easier
Collection<OsmPrimitive> selection = getCurrentDataSet().getSelectedNodesAndWays();
Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
// when rotating, having only one node makes no sense - quit silently
if (mode == Mode.rotate && affectedNodes.size() < 2)
return;
Command c = !Main.main.undoRedo.commands.isEmpty()
? Main.main.undoRedo.commands.getLast() : null;
if (c instanceof SequenceCommand) {
c = ((SequenceCommand)c).getLastCommand();
}
if (mode == Mode.move) {
if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getMovedNodes())) {
((MoveCommand)c).moveAgain(dx,dy);
} else {
Main.main.undoRedo.add(
c = new MoveCommand(selection, dx, dy));
}
for (Node n : affectedNodes) {
if (n.getCoor().isOutSideWorld()) {
// Revert move
((MoveCommand) c).moveAgain(-dx, -dy);
JOptionPane.showMessageDialog(
Main.parent,
tr("Cannot move objects outside of the world."),
tr("Warning"),
JOptionPane.WARNING_MESSAGE
);
restoreCursor();
return;
}
}
} else if (mode == Mode.rotate) {
if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand)c).getRotatedNodes())) {
((RotateCommand)c).rotateAgain(mouseStartEN, mouseEN);
} else {
Main.main.undoRedo.add(new RotateCommand(selection, mouseStartEN, mouseEN));
}
}
}
Main.map.mapView.repaint();
mousePos = e.getPoint();
didMove = true;
}
@Override public void mouseMoved(MouseEvent e) {
// Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired.
//
if (isPlatformOsx() && mode == Mode.rotate) {
mouseDragged(e);
}
}
private Collection<OsmPrimitive> getNearestCollectionVirtual(Point p, boolean allSegements) {
MapView c = Main.map.mapView;
int snapDistance = Main.pref.getInteger("mappaint.node.virtual-snap-distance", 8);
snapDistance *= snapDistance;
OsmPrimitive osm = c.getNearestNode(p, OsmPrimitive.isSelectablePredicate);
virtualWays.clear();
virtualNode = null;
Node virtualWayNode = null;
if (osm == null)
{
Collection<WaySegment> nearestWaySegs = allSegements
? c.getNearestWaySegments(p, OsmPrimitive.isSelectablePredicate)
: Collections.singleton(c.getNearestWaySegment(p, OsmPrimitive.isSelectablePredicate));
for(WaySegment nearestWS : nearestWaySegs) {
if (nearestWS == null) {
continue;
}
osm = nearestWS.way;
if(Main.pref.getInteger("mappaint.node.virtual-size", 8) > 0)
{
Way w = (Way)osm;
Point p1 = c.getPoint(w.getNode(nearestWS.lowerIndex));
Point p2 = c.getPoint(w.getNode(nearestWS.lowerIndex+1));
if(SimplePaintVisitor.isLargeSegment(p1, p2, Main.pref.getInteger("mappaint.node.virtual-space", 70)))
{
Point pc = new Point((p1.x+p2.x)/2, (p1.y+p2.y)/2);
if (p.distanceSq(pc) < snapDistance)
{
// Check that only segments on top of each other get added to the
// virtual ways list. Otherwise ways that coincidentally have their
// virtual node at the same spot will be joined which is likely unwanted
if(virtualWayNode != null) {
if( !w.getNode(nearestWS.lowerIndex+1).equals(virtualWayNode)
&& !w.getNode(nearestWS.lowerIndex).equals(virtualWayNode)) {
continue;
}
} else {
virtualWayNode = w.getNode(nearestWS.lowerIndex+1);
}
virtualWays.add(nearestWS);
if(virtualNode == null) {
virtualNode = new Node(Main.map.mapView.getLatLon(pc.x, pc.y));
}
}
}
}
}
}
if (osm == null)
return Collections.emptySet();
return Collections.singleton(osm);
}
/**
* Look, whether any object is selected. If not, select the nearest node.
* If there are no nodes in the dataset, do nothing.
*
* If the user did not press the left mouse button, do nothing.
*
* Also remember the starting position of the movement and change the mouse
* cursor to movement.
*/
@Override public void mousePressed(MouseEvent e) {
if(!Main.map.mapView.isActiveLayerVisible())
return;
// request focus in order to enable the expected keyboard shortcuts
//
Main.map.mapView.requestFocus();
cancelDrawMode = false;
if (! (Boolean)this.getValue("active")) return;
if (e.getButton() != MouseEvent.BUTTON1)
return;
boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
// We don't want to change to draw tool if the user tries to (de)select
// stuff but accidentally clicks in an empty area when selection is empty
if(shift || ctrl) {
cancelDrawMode = true;
}
mouseDownTime = System.currentTimeMillis();
didMove = false;
initialMoveThresholdExceeded = false;
Collection<OsmPrimitive> osmColl = getNearestCollectionVirtual(e.getPoint(), alt);
if (ctrl && shift) {
if (getCurrentDataSet().getSelected().isEmpty()) {
selectPrims(osmColl, true, false, false, false);
}
mode = Mode.rotate;
setCursor(ImageProvider.getCursor("rotate", null));
} else if (!osmColl.isEmpty()) {
// Don't replace the selection now if the user clicked on a
// selected object (this would break moving of selected groups).
// We'll do that later in mouseReleased if the user didn't try to
// move.
selectPrims(osmColl,
shift || getCurrentDataSet().getSelected().containsAll(osmColl),
ctrl, false, false);
mode = Mode.move;
} else {
mode = Mode.select;
oldCursor = Main.map.mapView.getCursor();
selectionManager.register(Main.map.mapView);
selectionManager.mousePressed(e);
}
if(mode != Mode.move || shift || ctrl)
{
virtualNode = null;
virtualWays.clear();
}
updateStatusLine();
// Mode.select redraws when selectPrims is called
// Mode.move redraws when mouseDragged is called
// Mode.rotate redraws here
if(mode == Mode.rotate) {
Main.map.mapView.repaint();
}
mousePos = e.getPoint();
}
/**
* Restore the old mouse cursor.
*/
@Override public void mouseReleased(MouseEvent e) {
if(!Main.map.mapView.isActiveLayerVisible())
return;
if (mode == Mode.select) {
selectionManager.unregister(Main.map.mapView);
// Select Draw Tool if no selection has been made
if(getCurrentDataSet().getSelected().size() == 0 && !cancelDrawMode) {
Main.map.selectDrawTool(true);
return;
}
}
restoreCursor();
if (mode == Mode.move) {
boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
if (!didMove) {
selectPrims(
Main.map.mapView.getNearestCollection(e.getPoint(), OsmPrimitive.isSelectablePredicate),
shift, ctrl, true, false);
// If the user double-clicked a node, change to draw mode
List<OsmPrimitive> sel = new ArrayList<OsmPrimitive>(getCurrentDataSet().getSelected());
if(e.getClickCount() >=2 && sel.size() == 1 && sel.get(0) instanceof Node) {
// We need to do it like this as otherwise drawAction will see a double
// click and switch back to SelectMode
Main.worker.execute(new Runnable(){
public void run() {
Main.map.selectDrawTool(true);
}
});
return;
}
} else {
Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
Collection<OsmPrimitive> s = new TreeSet<OsmPrimitive>();
int max = Main.pref.getInteger("warn.move.maxelements", 20);
for (OsmPrimitive osm : selection)
{
if(osm instanceof Node) {
s.add(osm);
} else if(osm instanceof Way)
{
s.add(osm);
s.addAll(((Way)osm).getNodes());
}
if(s.size() > max)
{
ExtendedDialog ed = new ExtendedDialog(
Main.parent,
tr("Move elements"),
new String[] {tr("Move them"), tr("Undo move")});
ed.setButtonIcons(new String[] {"reorder.png", "cancel.png"});
ed.setContent(tr("You moved more than {0} elements. "
+ "Moving a large number of elements is often an error.\n"
+ "Really move them?", max));
ed.toggleEnable("movedManyElements");
ed.showDialog();
if(ed.getValue() != 1)
{
Main.main.undoRedo.undo();
}
break;
}
}
if (ctrl) {
Collection<Node> affectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
Collection<Node> nn = Main.map.mapView.getNearestNodes(e.getPoint(), affectedNodes, OsmPrimitive.isSelectablePredicate);
if (nn != null) {
Node targetNode = nn.iterator().next();
Set<Node> nodesToMerge = new HashSet<Node>(affectedNodes);
nodesToMerge.add(targetNode);
if (!nodesToMerge.isEmpty()) {
Command cmd = MergeNodesAction.mergeNodes(Main.main.getEditLayer(),nodesToMerge, targetNode);
if(cmd != null)
Main.main.undoRedo.add(cmd);
}
}
}
getCurrentDataSet().fireSelectionChanged();
}
}
// I don't see why we need this.
//updateStatusLine();
mode = null;
updateStatusLine();
}
public void selectionEnded(Rectangle r, boolean alt, boolean shift, boolean ctrl) {
selectPrims(selectionManager.getObjectsInRectangle(r, alt), shift, ctrl, true, true);
}
public void selectPrims(Collection<OsmPrimitive> selectionList, boolean shift,
boolean ctrl, boolean released, boolean area) {
DataSet ds = getCurrentDataSet();
if ((shift && ctrl) || (ctrl && !released))
return; // not allowed together
// plain clicks with no modifiers clear the selection
if (!ctrl && !shift) {
ds.clearSelection();
}
if (ctrl) {
// Ctrl on an item toggles its selection status,
// but Ctrl on an *area* just clears those items
// out of the selection.
if (area) {
ds.clearSelection(selectionList);
} else {
ds.toggleSelected(selectionList);
}
} else {
// This is either a plain click (which means we
// previously cleared the selection), or a
// shift-click where we are adding things to an
// existing selection.
ds.addSelected(selectionList);
}
Main.map.mapView.repaint();
}
@Override public String getModeHelpText() {
if (mode == Mode.select)
return tr("Release the mouse button to select the objects in the rectangle.");
else if (mode == Mode.move)
return tr("Release the mouse button to stop moving. Ctrl to merge with nearest node.");
else if (mode == Mode.rotate)
return tr("Release the mouse button to stop rotating.");
else
return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; or change selection");
}
@Override public boolean layerIsSupported(Layer l) {
return l instanceof OsmDataLayer;
}
}