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

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

fix #4766 (exception), don't display virtual nodes for disabled objects, i18n update

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