source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java@ 2310

Last change on this file since 2310 was 2310, checked in by Gubaer, 15 years ago

fixed #3755: ctrl+drop node merging doesn't use destination node coords

  • Property svn:eol-style set to native
File size: 21.9 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Cursor;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.event.ActionEvent;
11import java.awt.event.InputEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.MouseEvent;
14import java.util.ArrayList;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.HashSet;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Set;
21import java.util.TreeSet;
22import java.util.logging.Logger;
23
24import javax.swing.JOptionPane;
25
26import org.openstreetmap.josm.Main;
27import org.openstreetmap.josm.actions.MergeNodesAction;
28import org.openstreetmap.josm.command.AddCommand;
29import org.openstreetmap.josm.command.ChangeCommand;
30import org.openstreetmap.josm.command.Command;
31import org.openstreetmap.josm.command.MoveCommand;
32import org.openstreetmap.josm.command.RotateCommand;
33import org.openstreetmap.josm.command.SequenceCommand;
34import org.openstreetmap.josm.data.coor.EastNorth;
35import org.openstreetmap.josm.data.osm.DataSet;
36import org.openstreetmap.josm.data.osm.Node;
37import org.openstreetmap.josm.data.osm.OsmPrimitive;
38import org.openstreetmap.josm.data.osm.Way;
39import org.openstreetmap.josm.data.osm.WaySegment;
40import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
41import org.openstreetmap.josm.data.osm.visitor.SimplePaintVisitor;
42import org.openstreetmap.josm.gui.ExtendedDialog;
43import org.openstreetmap.josm.gui.MapFrame;
44import org.openstreetmap.josm.gui.MapView;
45import org.openstreetmap.josm.gui.SelectionManager;
46import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
47import org.openstreetmap.josm.gui.layer.Layer;
48import org.openstreetmap.josm.gui.layer.OsmDataLayer;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.PlatformHookOsx;
51import org.openstreetmap.josm.tools.Shortcut;
52
53/**
54 * Move is an action that can move all kind of OsmPrimitives (except keys for now).
55 *
56 * If an selected object is under the mouse when dragging, move all selected objects.
57 * If an unselected object is under the mouse when dragging, it becomes selected
58 * and will be moved.
59 * If no object is under the mouse, move all selected objects (if any)
60 *
61 * @author imi
62 */
63public class SelectAction extends MapMode implements SelectionEnded {
64 static private final Logger logger = Logger.getLogger(SelectAction.class.getName());
65
66 /**
67 * Replies true if we are currently running on OSX
68 *
69 * @return true if we are currently running on OSX
70 */
71 public static boolean isPlatformOsx() {
72 return Main.platform != null
73 && Main.platform instanceof PlatformHookOsx;
74 }
75
76 enum Mode { move, rotate, select }
77 private Mode mode = null;
78 private long mouseDownTime = 0;
79 private boolean didMove = false;
80 private boolean cancelDrawMode = false;
81 Node virtualNode = null;
82 Collection<WaySegment> virtualWays = new ArrayList<WaySegment>();
83 SequenceCommand virtualCmds = null;
84
85 /**
86 * The old cursor before the user pressed the mouse button.
87 */
88 private Cursor oldCursor;
89 /**
90 * The position of the mouse before the user moves a node.
91 */
92 private Point mousePos;
93 private SelectionManager selectionManager;
94
95 /**
96 * The time which needs to pass between click and release before something
97 * counts as a move, in milliseconds
98 */
99 private int initialMoveDelay;
100
101 /**
102 * The screen distance which needs to be travelled before something
103 * counts as a move, in pixels
104 */
105 private int initialMoveThreshold;
106 private boolean initialMoveThresholdExceeded = false;
107 /**
108 * Create a new SelectAction
109 * @param mapFrame The MapFrame this action belongs to.
110 */
111 public SelectAction(MapFrame mapFrame) {
112 super(tr("Select"), "move/move", tr("Select, move and rotate objects"),
113 Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.GROUP_EDIT),
114 mapFrame,
115 getCursor("normal", "selection", Cursor.DEFAULT_CURSOR));
116 putValue("help", "Action/Move/Move");
117 selectionManager = new SelectionManager(this, false, mapFrame.mapView);
118 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
119 initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold",5);
120 }
121
122 private static Cursor getCursor(String name, String mod, int def) {
123 try {
124 return ImageProvider.getCursor(name, mod);
125 } catch (Exception e) {
126 }
127 return Cursor.getPredefinedCursor(def);
128 }
129
130 private void setCursor(Cursor c) {
131 if (oldCursor == null) {
132 oldCursor = Main.map.mapView.getCursor();
133 Main.map.mapView.setCursor(c);
134 }
135 }
136
137 private void restoreCursor() {
138 if (oldCursor != null) {
139 Main.map.mapView.setCursor(oldCursor);
140 oldCursor = null;
141 }
142 }
143
144 @Override public void enterMode() {
145 super.enterMode();
146 Main.map.mapView.addMouseListener(this);
147 Main.map.mapView.addMouseMotionListener(this);
148 Main.map.mapView.setVirtualNodesEnabled(
149 Main.pref.getInteger("mappaint.node.virtual-size", 8) != 0);
150 }
151
152 @Override public void exitMode() {
153 super.exitMode();
154 selectionManager.unregister(Main.map.mapView);
155 Main.map.mapView.removeMouseListener(this);
156 Main.map.mapView.removeMouseMotionListener(this);
157 Main.map.mapView.setVirtualNodesEnabled(false);
158 }
159
160 /**
161 * If the left mouse button is pressed, move all currently selected
162 * objects (if one of them is under the mouse) or the current one under the
163 * mouse (which will become selected).
164 */
165 @Override public void mouseDragged(MouseEvent e) {
166 if(!Main.map.mapView.isActiveLayerVisible())
167 return;
168
169 cancelDrawMode = true;
170 if (mode == Mode.select) return;
171
172 // do not count anything as a move if it lasts less than 100 milliseconds.
173 if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)) return;
174
175 if(mode != Mode.rotate) // button is pressed in rotate mode
176 if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
177 return;
178
179 if (mode == Mode.move) {
180 setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
181 }
182
183 if (mousePos == null) {
184 mousePos = e.getPoint();
185 return;
186 }
187
188 if (!initialMoveThresholdExceeded) {
189 int dxp = mousePos.x - e.getX();
190 int dyp = mousePos.y - e.getY();
191 int dp = (int) Math.sqrt(dxp*dxp+dyp*dyp);
192 if (dp < initialMoveThreshold) return;
193 initialMoveThresholdExceeded = true;
194 }
195
196 EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
197 EastNorth mouseStartEN = Main.map.mapView.getEastNorth(mousePos.x, mousePos.y);
198 double dx = mouseEN.east() - mouseStartEN.east();
199 double dy = mouseEN.north() - mouseStartEN.north();
200 if (dx == 0 && dy == 0)
201 return;
202
203 if (virtualWays.size() > 0) {
204 Collection<Command> virtualCmds = new LinkedList<Command>();
205 virtualCmds.add(new AddCommand(virtualNode));
206 for(WaySegment virtualWay : virtualWays) {
207 Way w = virtualWay.way;
208 Way wnew = new Way(w);
209 wnew.addNode(virtualWay.lowerIndex+1, virtualNode);
210 virtualCmds.add(new ChangeCommand(w, wnew));
211 }
212 virtualCmds.add(new MoveCommand(virtualNode, dx, dy));
213 String text = trn("Add and move a virtual new node to way",
214 "Add and move a virtual new node to {0} ways", virtualWays.size(),
215 virtualWays.size());
216
217 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
218 selectPrims(Collections.singleton((OsmPrimitive)virtualNode), false, false, false, false);
219 virtualWays.clear();
220 virtualNode = null;
221 } else {
222 // Currently we support moving and rotating, which do not affect relations.
223 // So don't add them in the first place to make handling easier
224 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelectedNodesAndWays();
225 Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
226
227 // when rotating, having only one node makes no sense - quit silently
228 if (mode == Mode.rotate && affectedNodes.size() < 2)
229 return;
230
231 Command c = !Main.main.undoRedo.commands.isEmpty()
232 ? Main.main.undoRedo.commands.getLast() : null;
233 if (c instanceof SequenceCommand) {
234 c = ((SequenceCommand)c).getLastCommand();
235 }
236
237 if (mode == Mode.move) {
238 if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getMovedNodes())) {
239 ((MoveCommand)c).moveAgain(dx,dy);
240 } else {
241 Main.main.undoRedo.add(
242 c = new MoveCommand(selection, dx, dy));
243 }
244
245 for (Node n : affectedNodes) {
246 if (n.getCoor().isOutSideWorld()) {
247 // Revert move
248 ((MoveCommand) c).moveAgain(-dx, -dy);
249
250 JOptionPane.showMessageDialog(
251 Main.parent,
252 tr("Cannot move objects outside of the world."),
253 tr("Warning"),
254 JOptionPane.WARNING_MESSAGE
255
256 );
257 restoreCursor();
258 return;
259 }
260 }
261 } else if (mode == Mode.rotate) {
262 if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand)c).getRotatedNodes())) {
263 ((RotateCommand)c).rotateAgain(mouseStartEN, mouseEN);
264 } else {
265 Main.main.undoRedo.add(new RotateCommand(selection, mouseStartEN, mouseEN));
266 }
267 }
268 }
269
270 Main.map.mapView.repaint();
271 mousePos = e.getPoint();
272
273 didMove = true;
274 }
275
276
277 @Override public void mouseMoved(MouseEvent e) {
278 // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired.
279 //
280 if (isPlatformOsx() && mode == Mode.rotate) {
281 mouseDragged(e);
282 }
283 }
284
285 private Collection<OsmPrimitive> getNearestCollectionVirtual(Point p, boolean allSegements) {
286 MapView c = Main.map.mapView;
287 int snapDistance = Main.pref.getInteger("mappaint.node.virtual-snap-distance", 8);
288 snapDistance *= snapDistance;
289 OsmPrimitive osm = c.getNearestNode(p);
290 virtualWays.clear();
291 virtualNode = null;
292 Node virtualWayNode = null;
293
294 if (osm == null)
295 {
296 Collection<WaySegment> nearestWaySegs = allSegements
297 ? c.getNearestWaySegments(p)
298 : Collections.singleton(c.getNearestWaySegment(p));
299
300 for(WaySegment nearestWS : nearestWaySegs) {
301 if (nearestWS == null) {
302 continue;
303 }
304
305 osm = nearestWS.way;
306 if(Main.pref.getInteger("mappaint.node.virtual-size", 8) > 0)
307 {
308 Way w = (Way)osm;
309 Point p1 = c.getPoint(w.getNode(nearestWS.lowerIndex));
310 Point p2 = c.getPoint(w.getNode(nearestWS.lowerIndex+1));
311 if(SimplePaintVisitor.isLargeSegment(p1, p2, Main.pref.getInteger("mappaint.node.virtual-space", 70)))
312 {
313 Point pc = new Point((p1.x+p2.x)/2, (p1.y+p2.y)/2);
314 if (p.distanceSq(pc) < snapDistance)
315 {
316 // Check that only segments on top of each other get added to the
317 // virtual ways list. Otherwise ways that coincidentally have their
318 // virtual node at the same spot will be joined which is likely unwanted
319 if(virtualWayNode != null) {
320 if( !w.getNode(nearestWS.lowerIndex+1).equals(virtualWayNode)
321 && !w.getNode(nearestWS.lowerIndex).equals(virtualWayNode)) {
322 continue;
323 }
324 } else {
325 virtualWayNode = w.getNode(nearestWS.lowerIndex+1);
326 }
327
328 virtualWays.add(nearestWS);
329 if(virtualNode == null) {
330 virtualNode = new Node(Main.map.mapView.getLatLon(pc.x, pc.y));
331 }
332 }
333 }
334 }
335 }
336 }
337 if (osm == null)
338 return Collections.emptySet();
339 return Collections.singleton(osm);
340 }
341
342 /**
343 * Look, whether any object is selected. If not, select the nearest node.
344 * If there are no nodes in the dataset, do nothing.
345 *
346 * If the user did not press the left mouse button, do nothing.
347 *
348 * Also remember the starting position of the movement and change the mouse
349 * cursor to movement.
350 */
351 @Override public void mousePressed(MouseEvent e) {
352 if(!Main.map.mapView.isActiveLayerVisible())
353 return;
354 // request focus in order to enable the expected keyboard shortcuts
355 //
356 Main.map.mapView.requestFocus();
357
358 cancelDrawMode = false;
359 if (! (Boolean)this.getValue("active")) return;
360 if (e.getButton() != MouseEvent.BUTTON1)
361 return;
362 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
363 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
364 boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
365
366 // We don't want to change to draw tool if the user tries to (de)select
367 // stuff but accidentally clicks in an empty area when selection is empty
368 if(shift || ctrl) {
369 cancelDrawMode = true;
370 }
371
372 mouseDownTime = System.currentTimeMillis();
373 didMove = false;
374 initialMoveThresholdExceeded = false;
375
376 Collection<OsmPrimitive> osmColl = getNearestCollectionVirtual(e.getPoint(), alt);
377
378 if (ctrl && shift) {
379 if (getCurrentDataSet().getSelected().isEmpty()) {
380 selectPrims(osmColl, true, false, false, false);
381 }
382 mode = Mode.rotate;
383 setCursor(ImageProvider.getCursor("rotate", null));
384 } else if (!osmColl.isEmpty()) {
385 // Don't replace the selection now if the user clicked on a
386 // selected object (this would break moving of selected groups).
387 // We'll do that later in mouseReleased if the user didn't try to
388 // move.
389 selectPrims(osmColl,
390 shift || getCurrentDataSet().getSelected().containsAll(osmColl),
391 ctrl, false, false);
392 mode = Mode.move;
393 } else {
394 mode = Mode.select;
395 oldCursor = Main.map.mapView.getCursor();
396 selectionManager.register(Main.map.mapView);
397 selectionManager.mousePressed(e);
398 }
399 if(mode != Mode.move || shift || ctrl)
400 {
401 virtualNode = null;
402 virtualWays.clear();
403 }
404
405 updateStatusLine();
406 // Mode.select redraws when selectPrims is called
407 // Mode.move redraws when mouseDragged is called
408 // Mode.rotate redraws here
409 if(mode == Mode.rotate) {
410 Main.map.mapView.repaint();
411 }
412
413 mousePos = e.getPoint();
414 }
415
416 /**
417 * Restore the old mouse cursor.
418 */
419 @Override public void mouseReleased(MouseEvent e) {
420 if(!Main.map.mapView.isActiveLayerVisible())
421 return;
422
423 if (mode == Mode.select) {
424 selectionManager.unregister(Main.map.mapView);
425
426 // Select Draw Tool if no selection has been made
427 if(getCurrentDataSet().getSelected().size() == 0 && !cancelDrawMode) {
428 Main.map.selectDrawTool(true);
429 return;
430 }
431 }
432 restoreCursor();
433
434 if (mode == Mode.move) {
435 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
436 boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
437 if (!didMove) {
438 selectPrims(
439 Main.map.mapView.getNearestCollection(e.getPoint()),
440 shift, ctrl, true, false);
441
442 // If the user double-clicked a node, change to draw mode
443 List<OsmPrimitive> sel = new ArrayList<OsmPrimitive>(getCurrentDataSet().getSelected());
444 if(e.getClickCount() >=2 && sel.size() == 1 && sel.get(0) instanceof Node) {
445 // We need to do it like this as otherwise drawAction will see a double
446 // click and switch back to SelectMode
447 Main.worker.execute(new Runnable(){
448 public void run() {
449 Main.map.selectDrawTool(true);
450 }
451 });
452 return;
453 }
454 } else {
455 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
456 Collection<OsmPrimitive> s = new TreeSet<OsmPrimitive>();
457 int max = Main.pref.getInteger("warn.move.maxelements", 20);
458 for (OsmPrimitive osm : selection)
459 {
460 if(osm instanceof Node) {
461 s.add(osm);
462 } else if(osm instanceof Way)
463 {
464 s.add(osm);
465 s.addAll(((Way)osm).getNodes());
466 }
467 if(s.size() > max)
468 {
469 ExtendedDialog ed = new ExtendedDialog(
470 Main.parent,
471 tr("Move elements"),
472 new String[] {tr("Move them"), tr("Undo move")});
473 ed.setButtonIcons(new String[] {"reorder.png", "cancel.png"});
474 ed.setContent(tr("You moved more than {0} elements. "
475 + "Moving a large number of elements is often an error.\n"
476 + "Really move them?", max));
477 ed.toggleEnable("movedManyElements");
478 ed.setToggleCheckboxText(tr("Always move and don't show dialog again"));
479 ed.showDialog();
480
481 if(ed.getValue() != 1 && ed.getValue() != ExtendedDialog.DialogNotShown)
482 {
483 Main.main.undoRedo.undo();
484 }
485 break;
486 }
487 }
488 if (ctrl) {
489 Collection<Node> affectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
490 Collection<Node> nn = Main.map.mapView.getNearestNodes(e.getPoint(), affectedNodes);
491 if (nn != null) {
492 Node targetNode = nn.iterator().next();
493 Set<Node> nodesToMerge = new HashSet<Node>(affectedNodes);
494 nodesToMerge.add(targetNode);
495 if (!nodesToMerge.isEmpty()) {
496 Command cmd = MergeNodesAction.mergeNodes(Main.main.getEditLayer(),nodesToMerge, targetNode);
497 Main.main.undoRedo.add(cmd);
498 }
499 }
500 }
501 DataSet.fireSelectionChanged(selection);
502 }
503 }
504
505 // I don't see why we need this.
506 //updateStatusLine();
507 mode = null;
508 updateStatusLine();
509 }
510
511 public void selectionEnded(Rectangle r, boolean alt, boolean shift, boolean ctrl) {
512 selectPrims(selectionManager.getObjectsInRectangle(r, alt), shift, ctrl, true, true);
513 }
514
515 public void selectPrims(Collection<OsmPrimitive> selectionList, boolean shift,
516 boolean ctrl, boolean released, boolean area) {
517 if ((shift && ctrl) || (ctrl && !released))
518 return; // not allowed together
519
520 Collection<OsmPrimitive> curSel;
521 if (!ctrl && !shift) {
522 curSel = new LinkedList<OsmPrimitive>(); // new selection will replace the old.
523 } else {
524 curSel = getCurrentDataSet().getSelected();
525 }
526
527 for (OsmPrimitive osm : selectionList)
528 {
529 if (ctrl)
530 {
531 if(curSel.contains(osm)) {
532 curSel.remove(osm);
533 } else if(!area) {
534 curSel.add(osm);
535 }
536 } else {
537 curSel.add(osm);
538 }
539 }
540 getCurrentDataSet().setSelected(curSel);
541 Main.map.mapView.repaint();
542 }
543
544 @Override public String getModeHelpText() {
545 if (mode == Mode.select)
546 return tr("Release the mouse button to select the objects in the rectangle.");
547 else if (mode == Mode.move)
548 return tr("Release the mouse button to stop moving. Ctrl to merge with nearest node.");
549 else if (mode == Mode.rotate)
550 return tr("Release the mouse button to stop rotating.");
551 else
552 return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; or change selection");
553 }
554
555 @Override public boolean layerIsSupported(Layer l) {
556 return l instanceof OsmDataLayer;
557 }
558}
Note: See TracBrowser for help on using the repository browser.