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

Last change on this file since 2719 was 2719, checked in by stoecker, 14 years ago

fixed last checkin

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