001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions.mapmode;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    import static org.openstreetmap.josm.tools.I18n.trn;
007    
008    import java.awt.AWTEvent;
009    import java.awt.Cursor;
010    import java.awt.Point;
011    import java.awt.Rectangle;
012    import java.awt.Toolkit;
013    import java.awt.event.AWTEventListener;
014    import java.awt.event.InputEvent;
015    import java.awt.event.KeyEvent;
016    import java.awt.event.MouseEvent;
017    import java.awt.geom.Point2D;
018    import java.util.Collection;
019    import java.util.Collections;
020    import java.util.HashSet;
021    import java.util.Iterator;
022    import java.util.LinkedList;
023    import java.util.Set;
024    
025    import javax.swing.JOptionPane;
026    
027    import org.openstreetmap.josm.Main;
028    import org.openstreetmap.josm.actions.MergeNodesAction;
029    import org.openstreetmap.josm.command.AddCommand;
030    import org.openstreetmap.josm.command.ChangeCommand;
031    import org.openstreetmap.josm.command.Command;
032    import org.openstreetmap.josm.command.MoveCommand;
033    import org.openstreetmap.josm.command.RotateCommand;
034    import org.openstreetmap.josm.command.ScaleCommand;
035    import org.openstreetmap.josm.command.SequenceCommand;
036    import org.openstreetmap.josm.data.coor.EastNorth;
037    import org.openstreetmap.josm.data.osm.DataSet;
038    import org.openstreetmap.josm.data.osm.Node;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.data.osm.Way;
041    import org.openstreetmap.josm.data.osm.WaySegment;
042    import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
043    import org.openstreetmap.josm.data.osm.visitor.paint.WireframeMapRenderer;
044    import org.openstreetmap.josm.gui.ExtendedDialog;
045    import org.openstreetmap.josm.gui.MapFrame;
046    import org.openstreetmap.josm.gui.MapView;
047    import org.openstreetmap.josm.gui.SelectionManager;
048    import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
049    import org.openstreetmap.josm.gui.layer.Layer;
050    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
051    import org.openstreetmap.josm.tools.ImageProvider;
052    import org.openstreetmap.josm.tools.Pair;
053    import org.openstreetmap.josm.tools.PlatformHookOsx;
054    import org.openstreetmap.josm.tools.Shortcut;
055    
056    /**
057     * Move is an action that can move all kind of OsmPrimitives (except keys for now).
058     *
059     * If an selected object is under the mouse when dragging, move all selected objects.
060     * If an unselected object is under the mouse when dragging, it becomes selected
061     * and will be moved.
062     * If no object is under the mouse, move all selected objects (if any)
063     *
064     * @author imi
065     */
066    public class SelectAction extends MapMode implements AWTEventListener, SelectionEnded {
067        // "select" means the selection rectangle and "move" means either dragging
068        // or select if no mouse movement occurs (i.e. just clicking)
069        enum Mode { move, rotate, scale, select }
070    
071        // contains all possible cases the cursor can be in the SelectAction
072        static private enum SelectActionCursor {
073            rect("normal", "selection"),
074            rect_add("normal", "select_add"),
075            rect_rm("normal", "select_remove"),
076            way("normal", "select_way"),
077            way_add("normal", "select_way_add"),
078            way_rm("normal", "select_way_remove"),
079            node("normal", "select_node"),
080            node_add("normal", "select_node_add"),
081            node_rm("normal", "select_node_remove"),
082            virtual_node("normal", "addnode"),
083            scale("scale", null),
084            rotate("rotate", null),
085            merge("crosshair", null),
086            lasso("normal", "rope"),
087            merge_to_node("crosshair", "joinnode"),
088            move(Cursor.MOVE_CURSOR);
089    
090            private final Cursor c;
091            private SelectActionCursor(String main, String sub) {
092                c = ImageProvider.getCursor(main, sub);
093            }
094            private SelectActionCursor(int systemCursor) {
095                c = Cursor.getPredefinedCursor(systemCursor);
096            }
097            public Cursor cursor() {
098                return c;
099            }
100        }
101    
102        private boolean lassoMode = false;
103    
104        // Cache previous mouse event (needed when only the modifier keys are
105        // pressed but the mouse isn't moved)
106        private MouseEvent oldEvent = null;
107    
108        private Mode mode = null;
109        private SelectionManager selectionManager;
110        private boolean cancelDrawMode = false;
111        private boolean drawTargetHighlight;
112        private boolean didMouseDrag = false;
113        /**
114         * The component this SelectAction is associated with.
115         */
116        private final MapView mv;
117        /**
118         * The old cursor before the user pressed the mouse button.
119         */
120        private Point startingDraggingPos;
121        /**
122         * point where user pressed the mouse to start movement
123         */
124        EastNorth startEN;
125        /**
126         * The last known position of the mouse.
127         */
128        private Point lastMousePos;
129        /**
130         * The time of the user mouse down event.
131         */
132        private long mouseDownTime = 0;
133        /**
134         * The pressed button of the user mouse down event.
135         */
136        private int mouseDownButton = 0;
137        /**
138         * The time of the user mouse down event.
139         */
140        private long mouseReleaseTime = 0;
141        /**
142         * The time which needs to pass between click and release before something
143         * counts as a move, in milliseconds
144         */
145        private int initialMoveDelay;
146        /**
147         * The screen distance which needs to be travelled before something
148         * counts as a move, in pixels
149         */
150        private int initialMoveThreshold;
151        private boolean initialMoveThresholdExceeded = false;
152    
153        /**
154         * elements that have been highlighted in the previous iteration. Used
155         * to remove the highlight from them again as otherwise the whole data
156         * set would have to be checked.
157         */
158        private Set<OsmPrimitive> oldHighlights = new HashSet<OsmPrimitive>();
159    
160        /**
161         * Create a new SelectAction
162         * @param mapFrame The MapFrame this action belongs to.
163         */
164        public SelectAction(MapFrame mapFrame) {
165            super(tr("Select"), "move/move", tr("Select, move, scale and rotate objects"),
166                    Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.DIRECT),
167                    mapFrame,
168                    ImageProvider.getCursor("normal", "selection"));
169            mv = mapFrame.mapView;
170            putValue("help", ht("/Action/Select"));
171            selectionManager = new SelectionManager(this, false, mv);
172            initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", 200);
173            initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5);
174        }
175    
176        @Override
177        public void enterMode() {
178            super.enterMode();
179            mv.addMouseListener(this);
180            mv.addMouseMotionListener(this);
181            mv.setVirtualNodesEnabled(Main.pref.getInteger("mappaint.node.virtual-size", 8) != 0);
182            drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true);
183            cycleManager.init();
184            virtualManager.init();
185            // This is required to update the cursors when ctrl/shift/alt is pressed
186            try {
187                Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
188            } catch (SecurityException ex) {}
189        }
190    
191        @Override
192        public void exitMode() {
193            super.exitMode();
194            selectionManager.unregister(mv);
195            mv.removeMouseListener(this);
196            mv.removeMouseMotionListener(this);
197            mv.setVirtualNodesEnabled(false);
198            try {
199                Toolkit.getDefaultToolkit().removeAWTEventListener(this);
200            } catch (SecurityException ex) {}
201            removeHighlighting();
202        }
203    
204        int previousModifiers;
205        
206         /**
207         * This is called whenever the keyboard modifier status changes
208         */
209        public void eventDispatched(AWTEvent e) {
210            if(oldEvent == null)
211                return;
212            // We don't have a mouse event, so we pass the old mouse event but the
213            // new modifiers.
214            int modif = ((InputEvent) e).getModifiers();
215            if (previousModifiers == modif)
216                return;
217            previousModifiers = modif;
218            if(giveUserFeedback(oldEvent, ((InputEvent) e).getModifiers())) {
219                mv.repaint();
220            }
221        }
222    
223        /**
224         * handles adding highlights and updating the cursor for the given mouse event.
225         * Please note that the highlighting for merging while moving is handled via mouseDragged.
226         * @param MouseEvent which should be used as base for the feedback
227         * @return true if repaint is required
228         */
229        private boolean giveUserFeedback(MouseEvent e) {
230            return giveUserFeedback(e, e.getModifiers());
231        }
232    
233        /**
234         * handles adding highlights and updating the cursor for the given mouse event.
235         * Please note that the highlighting for merging while moving is handled via mouseDragged.
236         * @param MouseEvent which should be used as base for the feedback
237         * @param define custom keyboard modifiers if the ones from MouseEvent are outdated or similar
238         * @return true if repaint is required
239         */
240        private boolean giveUserFeedback(MouseEvent e, int modifiers) {
241            Collection<OsmPrimitive> c = MapView.asColl(
242                    mv.getNearestNodeOrWay(e.getPoint(), OsmPrimitive.isSelectablePredicate, true));
243    
244            updateKeyModifiers(modifiers);
245            determineMapMode(!c.isEmpty());
246    
247            HashSet<OsmPrimitive> newHighlights = new HashSet<OsmPrimitive>();
248    
249            virtualManager.clear();
250            if(mode == Mode.move) {
251                if (!dragInProgress() && virtualManager.activateVirtualNodeNearPoint(e.getPoint())) {
252                    DataSet ds = getCurrentDataSet();
253                    if (ds != null && drawTargetHighlight) {
254                        ds.setHighlightedVirtualNodes(virtualManager.virtualWays);
255                    }
256                    mv.setNewCursor(SelectActionCursor.virtual_node.cursor(), this);
257                    // don't highlight anything else if a virtual node will be
258                    return repaintIfRequired(newHighlights);
259                }
260            }
261    
262            mv.setNewCursor(getCursor(c), this);
263    
264            // return early if there can't be any highlights
265            if(!drawTargetHighlight || mode != Mode.move || c.isEmpty())
266                return repaintIfRequired(newHighlights);
267    
268            // CTRL toggles selection, but if while dragging CTRL means merge
269            final boolean isToggleMode = ctrl && !dragInProgress();
270            for(OsmPrimitive x : c) {
271                // only highlight primitives that will change the selection
272                // when clicked. I.e. don't highlight selected elements unless
273                // we are in toggle mode.
274                if(isToggleMode || !x.isSelected()) {
275                    newHighlights.add(x);
276                }
277            }
278            return repaintIfRequired(newHighlights);
279        }
280        
281        /**
282         * works out which cursor should be displayed for most of SelectAction's
283         * features. The only exception is the "move" cursor when actually dragging
284         * primitives.
285         * @param nearbyStuff  primitives near the cursor
286         * @return the cursor that should be displayed
287         */
288        private Cursor getCursor(Collection<OsmPrimitive> nearbyStuff) {
289            String c = "rect";
290            switch(mode) {
291            case move:
292                if(virtualManager.hasVirtualNode()) {
293                    c = "virtual_node";
294                    break;
295                }
296                final Iterator<OsmPrimitive> it = nearbyStuff.iterator();
297                final OsmPrimitive osm = it.hasNext() ? it.next() : null;
298    
299                if(dragInProgress()) {
300                    // only consider merge if ctrl is pressed and there are nodes in
301                    // the selection that could be merged
302                    if(!ctrl || getCurrentDataSet().getSelectedNodes().isEmpty()) {
303                        c = "move";
304                        break;
305                    }
306                    // only show merge to node cursor if nearby node and that node is currently
307                    // not being dragged
308                    final boolean hasTarget = osm != null && osm instanceof Node && !osm.isSelected();
309                    c = hasTarget ? "merge_to_node" : "merge";
310                    break;
311                }
312    
313                c = (osm instanceof Node) ? "node" : c;
314                c = (osm instanceof Way) ? "way" : c;
315                if(shift) {
316                    c += "_add";
317                } else if(ctrl) {
318                    c += osm == null || osm.isSelected() ? "_rm" : "_add";
319                }
320                break;
321            case rotate:
322                c = "rotate";
323                break;
324            case scale:
325                c = "scale";
326                break;
327            case select:
328                if (lassoMode) {
329                    c = "lasso";
330                } else {
331                    c = "rect" + (shift ? "_add" : (ctrl ? "_rm" : ""));
332                }
333                break;
334            }
335            return SelectActionCursor.valueOf(c).cursor();
336        }
337    
338        /**
339         * Removes all existing highlights.
340         * @return true if a repaint is required
341         */
342        private boolean removeHighlighting() {
343            boolean needsRepaint = false;
344            DataSet ds = getCurrentDataSet();
345            if(ds != null && !ds.getHighlightedVirtualNodes().isEmpty()) {
346                needsRepaint = true;
347                ds.clearHighlightedVirtualNodes();
348            }
349            if(oldHighlights.isEmpty())
350                return needsRepaint;
351    
352            for(OsmPrimitive prim : oldHighlights) {
353                prim.setHighlighted(false);
354            }
355            oldHighlights = new HashSet<OsmPrimitive>();
356            return true;
357        }
358    
359        private boolean repaintIfRequired(HashSet<OsmPrimitive> newHighlights) {
360            if(!drawTargetHighlight)
361                return false;
362    
363            boolean needsRepaint = false;
364            for(OsmPrimitive x : newHighlights) {
365                if(oldHighlights.contains(x)) {
366                    continue;
367                }
368                needsRepaint = true;
369                x.setHighlighted(true);
370            }
371            oldHighlights.removeAll(newHighlights);
372            for(OsmPrimitive x : oldHighlights) {
373                x.setHighlighted(false);
374                needsRepaint = true;
375            }
376            oldHighlights = newHighlights;
377            return needsRepaint;
378        }
379        
380         /**
381         * Look, whether any object is selected. If not, select the nearest node.
382         * If there are no nodes in the dataset, do nothing.
383         *
384         * If the user did not press the left mouse button, do nothing.
385         *
386         * Also remember the starting position of the movement and change the mouse
387         * cursor to movement.
388         */
389        @Override
390        public void mousePressed(MouseEvent e) {
391            mouseDownButton = e.getButton();
392            // return early
393            if (!mv.isActiveLayerVisible() || !(Boolean) this.getValue("active") || mouseDownButton != MouseEvent.BUTTON1)
394                return;
395            
396            // left-button mouse click only is processed here
397            
398            // request focus in order to enable the expected keyboard shortcuts
399            mv.requestFocus();
400    
401            // update which modifiers are pressed (shift, alt, ctrl)
402            updateKeyModifiers(e);
403    
404            // We don't want to change to draw tool if the user tries to (de)select
405            // stuff but accidentally clicks in an empty area when selection is empty
406            cancelDrawMode = (shift || ctrl);
407            didMouseDrag = false;
408            initialMoveThresholdExceeded = false;
409            mouseDownTime = System.currentTimeMillis();
410            lastMousePos = e.getPoint();
411            startEN = mv.getEastNorth(lastMousePos.x,lastMousePos.y);
412    
413            // primitives under cursor are stored in c collection
414            
415            OsmPrimitive nearestPrimitive = mv.getNearestNodeOrWay(e.getPoint(), OsmPrimitive.isSelectablePredicate, true);
416    
417            determineMapMode(nearestPrimitive!=null);
418            
419            switch(mode) {
420            case rotate:
421            case scale:
422                //  if nothing was selected, select primitive under cursor for scaling or rotating
423                if (getCurrentDataSet().getSelected().isEmpty()) {
424                    getCurrentDataSet().setSelected(MapView.asColl(nearestPrimitive));
425                }
426    
427                // Mode.select redraws when selectPrims is called
428                // Mode.move   redraws when mouseDragged is called
429                // Mode.rotate redraws here
430                // Mode.scale redraws here
431                break;
432            case move:
433                // also include case when some primitive is under cursor and no shift+ctrl / alt+ctrl is pressed
434                // so this is not movement, but selection on primitive under cursor
435                if (!cancelDrawMode && nearestPrimitive instanceof Way) {
436                    virtualManager.activateVirtualNodeNearPoint(e.getPoint());
437                }
438                OsmPrimitive toSelect = cycleManager.cycleSetup(nearestPrimitive, e.getPoint());
439                selectPrims(mv.asColl(toSelect), false, false);
440                useLastMoveCommandIfPossible();
441                break;
442            case select:
443            default:
444                // start working with rectangle or lasso
445                selectionManager.register(mv, lassoMode);
446                selectionManager.mousePressed(e);
447                break;
448            }
449            if (giveUserFeedback(e)) {
450                mv.repaint();
451            }
452            updateStatusLine();
453        }
454        
455        @Override
456        public void mouseMoved(MouseEvent e) {
457            // Mac OSX simulates with  ctrl + mouse 1  the second mouse button hence no dragging events get fired.
458            if ((Main.platform instanceof PlatformHookOsx) && (mode == Mode.rotate || mode == Mode.scale)) {
459                mouseDragged(e);
460                return;
461            }
462            oldEvent = e;
463            if(giveUserFeedback(e)) {
464                mv.repaint();
465            }
466        }
467        
468        /**
469         * If the left mouse button is pressed, move all currently selected
470         * objects (if one of them is under the mouse) or the current one under the
471         * mouse (which will become selected).
472         */
473        @Override
474        public void mouseDragged(MouseEvent e) {
475            if (!mv.isActiveLayerVisible())
476                return;
477            
478            // Swing sends random mouseDragged events when closing dialogs by double-clicking their top-left icon on Windows
479            // Ignore such false events to prevent issues like #7078
480            if (mouseDownButton == MouseEvent.BUTTON1 && mouseReleaseTime > mouseDownTime)
481                return;
482            
483            cancelDrawMode = true;
484            if (mode == Mode.select)
485                return;
486    
487            // do not count anything as a move if it lasts less than 100 milliseconds.
488            if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay))
489                return;
490    
491            if (mode != Mode.rotate && mode != Mode.scale) // button is pressed in rotate mode
492            {
493                if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
494                    return;
495            }
496    
497            if (mode == Mode.move) {
498                // If ctrl is pressed we are in merge mode. Look for a nearby node,
499                // highlight it and adjust the cursor accordingly.
500                final boolean canMerge = ctrl && !getCurrentDataSet().getSelectedNodes().isEmpty();
501                final OsmPrimitive p = canMerge ? (OsmPrimitive)findNodeToMergeTo(e.getPoint()) : null;
502                boolean needsRepaint = removeHighlighting();
503                if(p != null) {
504                    p.setHighlighted(true);
505                    oldHighlights.add(p);
506                    needsRepaint = true;
507                }
508                mv.setNewCursor(getCursor(MapView.asColl(p)), this);
509                // also update the stored mouse event, so we can display the correct cursor
510                // when dragging a node onto another one and then press CTRL to merge
511                oldEvent = e;
512                if(needsRepaint) {
513                    mv.repaint();
514                }
515            }
516    
517            if (startingDraggingPos == null) {
518                startingDraggingPos = new Point(e.getX(), e.getY());
519            }
520    
521            if( lastMousePos == null ) {
522                lastMousePos = e.getPoint();
523                return;
524            }
525    
526            if (!initialMoveThresholdExceeded) {
527                int dp = (int) lastMousePos.distance(e.getX(), e.getY());
528                if (dp < initialMoveThreshold)
529                    return; // ignore small drags
530                initialMoveThresholdExceeded = true; //no more ingnoring uintil nex mouse press
531            }
532            if (e.getPoint().equals(lastMousePos))
533                return;
534            
535            EastNorth currentEN = mv.getEastNorth(e.getX(), e.getY());
536    
537            if (virtualManager.hasVirtualWaysToBeConstructed()) {
538                virtualManager.createMiddleNodeFromVirtual(currentEN);
539            } else {
540                if (!updateCommandWhileDragging(currentEN)) return;
541            }
542    
543            mv.repaint();
544            if (mode != Mode.scale) {
545                lastMousePos = e.getPoint();
546            }
547    
548            didMouseDrag = true;
549        }
550    
551        
552    
553        @Override
554        public void mouseExited(MouseEvent e) {
555            if(removeHighlighting()) {
556                mv.repaint();
557            }
558        }
559    
560        
561        @Override
562        public void mouseReleased(MouseEvent e) {
563            if (!mv.isActiveLayerVisible())
564                return;
565    
566            startingDraggingPos = null;
567            mouseReleaseTime = System.currentTimeMillis();
568    
569            if (mode == Mode.select) {
570                selectionManager.unregister(mv);
571    
572                // Select Draw Tool if no selection has been made
573                if (getCurrentDataSet().getSelected().isEmpty() && !cancelDrawMode) {
574                    Main.map.selectDrawTool(true);
575                    return;
576                }
577            }
578    
579            if (mode == Mode.move && e.getButton() == MouseEvent.BUTTON1) {
580                if (!didMouseDrag) {
581                    // only built in move mode
582                    virtualManager.clear();
583                    // do nothing if the click was to short too be recognized as a drag,
584                    // but the release position is farther than 10px away from the press position
585                    if (lastMousePos == null || lastMousePos.distanceSq(e.getPoint()) < 100) {
586                        updateKeyModifiers(e);
587                        selectPrims(cycleManager.cyclePrims(), true, false);
588    
589                        // If the user double-clicked a node, change to draw mode
590                        Collection<OsmPrimitive> c = getCurrentDataSet().getSelected();
591                        if (e.getClickCount() >= 2 && c.size() == 1 && c.iterator().next() instanceof Node) {
592                            // We need to do it like this as otherwise drawAction will see a double
593                            // click and switch back to SelectMode
594                            Main.worker.execute(new Runnable() {
595                                public void run() {
596                                    Main.map.selectDrawTool(true);
597                                }
598                            });
599                            return;
600                        }
601                    }
602                } else {
603                    confirmOrUndoMovement(e);
604                }
605            }
606    
607            mode = null;
608    
609            // simply remove any highlights if the middle click popup is active because
610            // the highlights don't depend on the cursor position there. If something was
611            // selected beforehand this would put us into move mode as well, which breaks
612            // the cycling through primitives on top of each other (see #6739).
613            if(e.getButton() == MouseEvent.BUTTON2) {
614                removeHighlighting();
615            } else {
616                giveUserFeedback(e);
617            }
618            updateStatusLine();
619        }
620    
621        @Override
622        public void selectionEnded(Rectangle r, MouseEvent e) {
623            updateKeyModifiers(e);
624            selectPrims(selectionManager.getSelectedObjects(alt), true, true);
625        }
626    
627        /**
628         * sets the mapmode according to key modifiers and if there are any
629         * selectables nearby. Everything has to be pre-determined for this
630         * function; its main purpose is to centralize what the modifiers do.
631         * @param hasSelectionNearby
632         */
633        private void determineMapMode(boolean hasSelectionNearby) {
634            if (shift && ctrl) {
635                mode = Mode.rotate;
636            } else if (alt && ctrl) {
637                mode = Mode.scale;
638            } else if (hasSelectionNearby || dragInProgress()) {
639                mode = Mode.move;
640            } else {
641                mode = Mode.select;
642            }
643        }
644        
645        /** returns true whenever elements have been grabbed and moved (i.e. the initial
646         * thresholds have been exceeded) and is still in progress (i.e. mouse button
647         * still pressed)
648         */
649        final private boolean dragInProgress() {
650            return didMouseDrag && startingDraggingPos != null;
651        }
652    
653        
654        /**
655         * Create or update data modification command while dragging mouse - implementation of 
656         * continuous moving, scaling and rotation
657         * @param currentEN - mouse position
658         * @return status of action (<code>true</code> when action was performed)
659         */
660        private boolean updateCommandWhileDragging(EastNorth currentEN) {
661            // Currently we support only transformations which do not affect relations.
662            // So don't add them in the first place to make handling easier
663            Collection<OsmPrimitive> selection = getCurrentDataSet().getSelectedNodesAndWays();
664            if (selection.isEmpty()) { // if nothing was selected to drag, just select nearest node/way to the cursor
665                OsmPrimitive nearestPrimitive = mv.getNearestNodeOrWay(mv.getPoint(startEN), OsmPrimitive.isSelectablePredicate, true);
666                getCurrentDataSet().setSelected(nearestPrimitive);
667            }
668            
669            Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
670            // for these transformations, having only one node makes no sense - quit silently
671            if (affectedNodes.size() < 2 && (mode == Mode.rotate || mode == Mode.scale)) {
672                return false;
673            }
674            Command c = getLastCommand();
675            if (mode == Mode.move) {
676                getCurrentDataSet().beginUpdate();
677                if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand) c).getParticipatingPrimitives())) {
678                    ((MoveCommand) c).saveCheckpoint();
679                    ((MoveCommand) c).applyVectorTo(currentEN);
680                } else {
681                    Main.main.undoRedo.add(
682                            c = new MoveCommand(selection, startEN, currentEN));
683                }
684                for (Node n : affectedNodes) {
685                    if (n.getCoor().isOutSideWorld()) {
686                        // Revert move
687                        ((MoveCommand) c).resetToCheckpoint();
688                        JOptionPane.showMessageDialog(
689                                Main.parent,
690                                tr("Cannot move objects outside of the world."),
691                                tr("Warning"),
692                                JOptionPane.WARNING_MESSAGE);
693                        mv.setNewCursor(cursor, this);
694                        return false;
695                    }
696                }
697                getCurrentDataSet().endUpdate();
698                return true;
699            } 
700    
701            startEN = currentEN; // drag can continue after scaling/rotation
702    
703            if (mode == Mode.rotate) {
704                getCurrentDataSet().beginUpdate();
705                if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand) c).getTransformedNodes())) {
706                    ((RotateCommand) c).handleEvent(currentEN);
707                } else {
708                    Main.main.undoRedo.add(new RotateCommand(selection, currentEN));
709                }
710                getCurrentDataSet().endUpdate();
711            } else if (mode == Mode.scale) {
712                getCurrentDataSet().beginUpdate();
713                if (c instanceof ScaleCommand && affectedNodes.equals(((ScaleCommand) c).getTransformedNodes())) {
714                    ((ScaleCommand) c).handleEvent(currentEN);
715                } else {
716                    Main.main.undoRedo.add(new ScaleCommand(selection, currentEN));
717                }
718                getCurrentDataSet().endUpdate();
719            }
720            return true;
721        }
722        
723        /**
724         * Adapt last move command (if it is suitable) to work with next drag, started at point startEN
725         */
726        private void useLastMoveCommandIfPossible() {
727            Command c = getLastCommand();
728            Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(getCurrentDataSet().getSelected());
729            if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand) c).getParticipatingPrimitives())) {
730                // old command was created with different base point of movement, we need to recalculate it
731                ((MoveCommand) c).changeStartPoint(startEN);
732            }
733        }
734           
735        /**
736         * Obtain command in undoRedo stack to "continue" when dragging
737         */
738        private Command getLastCommand() {
739            Command c = !Main.main.undoRedo.commands.isEmpty()
740                    ? Main.main.undoRedo.commands.getLast() : null;
741            if (c instanceof SequenceCommand) {
742                c = ((SequenceCommand) c).getLastCommand();
743            }
744            return c;
745        }
746        
747        /**
748         * Present warning in case of large and possibly unwanted movements and undo
749         * unwanted movements.
750         *
751         * @param e the mouse event causing the action (mouse released)
752         */
753        private void confirmOrUndoMovement(MouseEvent e) {
754            int max = Main.pref.getInteger("warn.move.maxelements", 20), limit = max;
755            for (OsmPrimitive osm : getCurrentDataSet().getSelected()) {
756                if (osm instanceof Way) {
757                    limit -= ((Way) osm).getNodes().size();
758                }
759                if ((limit -= 1) < 0) {
760                    break;
761                }
762            }
763            if (limit < 0) {
764                ExtendedDialog ed = new ExtendedDialog(
765                        Main.parent,
766                        tr("Move elements"),
767                        new String[]{tr("Move them"), tr("Undo move")});
768                ed.setButtonIcons(new String[]{"reorder.png", "cancel.png"});
769                ed.setContent(tr("You moved more than {0} elements. " + "Moving a large number of elements is often an error.\n" + "Really move them?", max));
770                ed.setCancelButton(2);
771                ed.toggleEnable("movedManyElements");
772                ed.showDialog();
773    
774                if (ed.getValue() != 1) {
775                    Main.main.undoRedo.undo();
776                }
777            } else {
778                // if small number of elements were moved,
779                updateKeyModifiers(e);
780                if (ctrl) mergePrims(e.getPoint());
781            }
782            getCurrentDataSet().fireSelectionChanged();
783        }
784        
785        /**
786         * Merges the selected nodes to the one closest to the given mouse position iff the control
787         * key is pressed. If there is no such node, no action will be done and no error will be
788         * reported. If there is, it will execute the merge and add it to the undo buffer.
789         */
790        final private void mergePrims(Point p) {
791            Collection<Node> selNodes = getCurrentDataSet().getSelectedNodes();
792            if (selNodes.isEmpty())
793                return;
794    
795            Node target = findNodeToMergeTo(p);
796            if (target == null)
797                return;
798    
799            Collection<Node> nodesToMerge = new LinkedList<Node>(selNodes);
800            nodesToMerge.add(target);
801            MergeNodesAction.doMergeNodes(Main.main.getEditLayer(), nodesToMerge, target);
802        }
803    
804        /**
805         * Tries to find a node to merge to when in move-merge mode for the current mouse
806         * position. Either returns the node or null, if no suitable one is nearby.
807         */
808        final private Node findNodeToMergeTo(Point p) {
809            Collection<Node> target = mv.getNearestNodes(p,
810                    getCurrentDataSet().getSelectedNodes(),
811                    OsmPrimitive.isSelectablePredicate);
812            return target.isEmpty() ? null : target.iterator().next();
813        }
814    
815        private void selectPrims(Collection<OsmPrimitive> prims, boolean released, boolean area) {
816            DataSet ds = getCurrentDataSet();
817    
818            // not allowed together: do not change dataset selection, return early
819            // Virtual Ways: if non-empty the cursor is above a virtual node. So don't highlight
820            // anything if about to drag the virtual node (i.e. !released) but continue if the
821            // cursor is only released above a virtual node by accident (i.e. released). See #7018
822            if ((shift && ctrl) || (ctrl && !released) || (virtualManager.hasVirtualWaysToBeConstructed() && !released))
823                return;
824    
825            if (!released) {
826                // Don't replace the selection if the user clicked on a
827                // selected object (it breaks moving of selected groups).
828                // Do it later, on mouse release.
829                shift |= getCurrentDataSet().getSelected().containsAll(prims);
830            }
831    
832            if (ctrl) {
833                // Ctrl on an item toggles its selection status,
834                // but Ctrl on an *area* just clears those items
835                // out of the selection.
836                if (area) {
837                    ds.clearSelection(prims);
838                } else {
839                    ds.toggleSelected(prims);
840                }
841            } else if (shift) {
842                // add prims to an existing selection
843                ds.addSelected(prims);
844            } else {
845                // clear selection, then select the prims clicked
846                ds.setSelected(prims);
847            }
848        }
849    
850        @Override
851        public String getModeHelpText() {
852            if (mode == Mode.select)
853                return tr("Release the mouse button to select the objects in the rectangle.");
854            else if (mode == Mode.move) {
855                final boolean canMerge = getCurrentDataSet()!=null && !getCurrentDataSet().getSelectedNodes().isEmpty();
856                final String mergeHelp = canMerge ? (" " + tr("Ctrl to merge with nearest node.")) : "";
857                return tr("Release the mouse button to stop moving.") + mergeHelp;
858            } else if (mode == Mode.rotate)
859                return tr("Release the mouse button to stop rotating.");
860            else if (mode == Mode.scale)
861                return tr("Release the mouse button to stop scaling.");
862            else
863                return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; Alt-Ctrl to scale selected; or change selection");
864        }
865    
866        @Override
867        public boolean layerIsSupported(Layer l) {
868            return l instanceof OsmDataLayer;
869        }
870    
871        public void setLassoMode(boolean lassoMode) {
872            this.selectionManager.setLassoMode(lassoMode);
873            this.lassoMode = lassoMode;
874        }
875        
876        CycleManager cycleManager = new CycleManager();
877        VirtualManager virtualManager = new VirtualManager();
878        
879        private class CycleManager {
880    
881            private Collection<OsmPrimitive> cycleList = Collections.emptyList();
882            private boolean cyclePrims = false;
883            private OsmPrimitive cycleStart = null;
884            private boolean waitForMouseUpParameter;
885            private boolean multipleMatchesParameter;
886            /**
887             * read preferences
888             */
889            private void init() {
890                waitForMouseUpParameter = Main.pref.getBoolean("mappaint.select.waits-for-mouse-up", false);
891                multipleMatchesParameter = Main.pref.getBoolean("selectaction.cycles.multiple.matches", false);
892            }
893            
894            /**
895             * Determine prmitive to be selected and build cycleList
896             * @param nearest primitive found by simple method
897             * @param p point where user clicked
898             * @return OsmPrimitive to be selected
899             */
900            private OsmPrimitive cycleSetup(OsmPrimitive nearest, Point p) {
901                OsmPrimitive osm = null;
902    
903                if (nearest != null) {
904                    osm = nearest;
905    
906                    // Point p = e.getPoint();
907    //              updateKeyModifiers(e); // cycleSetup called only after updateModifiers !
908                    
909                    if (!(alt || multipleMatchesParameter)) {
910                        // no real cycling, just one element in cycle list                    
911                        cycleList = MapView.asColl(osm);
912    
913                        if (waitForMouseUpParameter) {
914                            // prefer a selected nearest node or way, if possible
915                            osm = mv.getNearestNodeOrWay(p, OsmPrimitive.isSelectablePredicate, true);
916                        }
917                    } else {
918                        // Alt + left mouse button pressed: we need to build cycle list
919                        cycleList = mv.getAllNearest(p, OsmPrimitive.isSelectablePredicate);
920    
921                        if (cycleList.size() > 1) {
922                            cyclePrims = false;
923    
924                            // find first already selected element in cycle list
925                            OsmPrimitive old = osm;
926                            for (OsmPrimitive o : cycleList) {
927                                if (o.isSelected()) {
928                                    cyclePrims = true;
929                                    osm = o;
930                                    break;
931                                }
932                            }
933    
934                            // special case:  for cycle groups of 2, we can toggle to the
935                            // true nearest primitive on mousePressed right away
936                            if (cycleList.size() == 2 && !waitForMouseUpParameter) {
937                                if (!(osm.equals(old) || osm.isNew() || ctrl)) {
938                                    cyclePrims = false;
939                                    osm = old;
940                                } // else defer toggling to mouseRelease time in those cases:
941                                /*
942                                 * osm == old -- the true nearest node is the
943                                 * selected one osm is a new node -- do not break
944                                 * unglue ways in ALT mode ctrl is pressed -- ctrl
945                                 * generally works on mouseReleased
946                                 */
947                            }
948                        }
949                    }
950                }
951                return osm;
952            }
953    
954            /**
955             * Modifies current selection state and returns the next element in a
956             * selection cycle given by
957             * <code>cycleList</code> field
958             * @return the next element of cycle list
959             */
960            private Collection<OsmPrimitive> cyclePrims() {
961                OsmPrimitive nxt = null;
962    
963                if (cycleList.size() <= 1) {
964                    // no real cycling, just return one-element collection with nearest primitive in it
965                    return cycleList;
966                }
967    //          updateKeyModifiers(e); // already called before !
968    
969                DataSet ds = getCurrentDataSet();
970                OsmPrimitive first = cycleList.iterator().next(), foundInDS = null;
971                nxt = first;
972    
973                if (cyclePrims && shift) {
974                    for (Iterator<OsmPrimitive> i = cycleList.iterator(); i.hasNext();) {
975                        nxt = i.next();
976                        if (!nxt.isSelected()) {
977                            break; // take first primitive in cycleList not in sel
978                        }
979                    }
980                    // if primitives 1,2,3 are under cursor, [Alt-press] [Shift-release] gives 1 -> 12 -> 123
981                } else {
982                    for (Iterator<OsmPrimitive> i = cycleList.iterator(); i.hasNext();) {
983                        nxt = i.next();
984                        if (nxt.isSelected()) {
985                            foundInDS = nxt;
986                            // first selected primitive in cycleList is found
987                            if (cyclePrims || ctrl) {
988                                ds.clearSelection(foundInDS); // deselect it 
989                                nxt = i.hasNext() ? i.next() : first;
990                                // return next one in cycle list (last->first)
991                            }
992                            break; // take next primitive in cycleList
993                        }
994                    }
995                }
996                
997                // if "no-alt-cycling" is enabled, Ctrl-Click arrives here.
998                if (ctrl) {
999                    // a member of cycleList was found in the current dataset selection
1000                    if (foundInDS != null) {
1001                        // mouse was moved to a different selection group w/ a previous sel
1002                        if (!cycleList.contains(cycleStart)) {
1003                            ds.clearSelection(cycleList);
1004                            cycleStart = foundInDS;
1005                        } else if (cycleStart.equals(nxt)) {
1006                            // loop detected, insert deselect step
1007                            ds.addSelected(nxt);
1008                        }
1009                    } else {
1010                        // setup for iterating a sel group again or a new, different one..
1011                        nxt = (cycleList.contains(cycleStart)) ? cycleStart : first;
1012                        cycleStart = nxt;
1013                    }
1014                } else {
1015                    cycleStart = null;
1016                }
1017                // return one-element collection with one element to be selected (or added  to selection)
1018                return MapView.asColl(nxt);
1019            }
1020        }
1021        
1022        private class VirtualManager {
1023    
1024            private Node virtualNode = null;
1025            private Collection<WaySegment> virtualWays = new LinkedList<WaySegment>();
1026            private int nodeVirtualSize;
1027            private int virtualSnapDistSq2;
1028            private int virtualSpace;
1029            
1030            private void init() {
1031                nodeVirtualSize = Main.pref.getInteger("mappaint.node.virtual-size", 8);
1032                int virtualSnapDistSq = Main.pref.getInteger("mappaint.node.virtual-snap-distance", 8);
1033                virtualSnapDistSq2 = virtualSnapDistSq*virtualSnapDistSq;
1034                virtualSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
1035            }
1036            
1037            /**
1038             * Calculate a virtual node if there is enough visual space to draw a
1039             * crosshair node and the middle of a way segment is clicked. If the
1040             * user drags the crosshair node, it will be added to all ways in
1041             * <code>virtualWays</code>.
1042             *
1043             * @param e contains the point clicked
1044             * @return whether
1045             * <code>virtualNode</code> and
1046             * <code>virtualWays</code> were setup.
1047             */
1048            private boolean activateVirtualNodeNearPoint(Point p) {
1049                if (nodeVirtualSize > 0) {
1050    
1051                    Collection<WaySegment> selVirtualWays = new LinkedList<WaySegment>();
1052                    Pair<Node, Node> vnp = null, wnp = new Pair<Node, Node>(null, null);
1053    
1054                    Way w = null;
1055                    for (WaySegment ws : mv.getNearestWaySegments(p, OsmPrimitive.isSelectablePredicate)) {
1056                        w = ws.way;
1057    
1058                        Point2D p1 = mv.getPoint2D(wnp.a = w.getNode(ws.lowerIndex));
1059                        Point2D p2 = mv.getPoint2D(wnp.b = w.getNode(ws.lowerIndex + 1));
1060                        if (WireframeMapRenderer.isLargeSegment(p1, p2, virtualSpace)) {
1061                            Point2D pc = new Point2D.Double((p1.getX() + p2.getX()) / 2, (p1.getY() + p2.getY()) / 2);
1062                            if (p.distanceSq(pc) < virtualSnapDistSq2) {
1063                                // Check that only segments on top of each other get added to the
1064                                // virtual ways list. Otherwise ways that coincidentally have their
1065                                // virtual node at the same spot will be joined which is likely unwanted
1066                                Pair.sort(wnp);
1067                                if (vnp == null) {
1068                                    vnp = new Pair<Node, Node>(wnp.a, wnp.b);
1069                                    virtualNode = new Node(mv.getLatLon(pc.getX(), pc.getY()));
1070                                }
1071                                if (vnp.equals(wnp)) {
1072                                    // if mutiple line segments have the same points,
1073                                    // add all segments to be splitted to virtualWays list
1074                                    // if some lines are selected, only their segments will go to virtualWays
1075                                    (w.isSelected() ? selVirtualWays : virtualWays).add(ws);
1076                                }
1077                            }
1078                        }
1079                    }
1080    
1081                    if (!selVirtualWays.isEmpty()) {
1082                        virtualWays = selVirtualWays;
1083                    }
1084                }
1085    
1086                return !virtualWays.isEmpty();
1087            }
1088    
1089            private void createMiddleNodeFromVirtual(EastNorth currentEN) {
1090                Collection<Command> virtualCmds = new LinkedList<Command>();
1091                virtualCmds.add(new AddCommand(virtualNode));
1092                for (WaySegment virtualWay : virtualWays) {
1093                    Way w = virtualWay.way;
1094                    Way wnew = new Way(w);
1095                    wnew.addNode(virtualWay.lowerIndex + 1, virtualNode);
1096                    virtualCmds.add(new ChangeCommand(w, wnew));
1097                }
1098                virtualCmds.add(new MoveCommand(virtualNode, startEN, currentEN));
1099                String text = trn("Add and move a virtual new node to way",
1100                        "Add and move a virtual new node to {0} ways", virtualWays.size(),
1101                        virtualWays.size());
1102                Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
1103                getCurrentDataSet().setSelected(Collections.singleton((OsmPrimitive) virtualNode));
1104                clear();
1105            }
1106    
1107            private void clear() {
1108                virtualWays.clear();
1109                virtualNode = null;
1110            }
1111    
1112            private boolean hasVirtualNode() {
1113                return virtualNode != null;
1114            }
1115    
1116            private boolean hasVirtualWaysToBeConstructed() {
1117                return !virtualWays.isEmpty();
1118            }
1119        }
1120    }