Ticket #7991: 7991-DualAlign.r7206.patch

File 7991-DualAlign.r7206.patch, 34.2 KB (added by AlfonZ, 12 years ago)

patch updated to r7206
new checkbox menuitem (in Edit menu)
new subclass DualAlignChangeAction that toggles the mode
key event handling copied from DrawAction
MoveCommands: when using Ctrl modifier with dual alignment disabled, only moveCommand is used; with dual alignment enabled, moveCommand and new moveCommand2 is used, due to two nodes moving in different directions, commands are then grouped into SequenceCommand
javadoc updates
replaced string concatenation by StringBuilder - see r6289
loose coupling - see r6317
Needs dualalign.png.

  • src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java

     
    88import java.awt.AWTEvent;
    99import java.awt.BasicStroke;
    1010import java.awt.Color;
     11import java.awt.Component;
    1112import java.awt.Cursor;
    1213import java.awt.Graphics2D;
     14import java.awt.KeyboardFocusManager;
    1315import java.awt.Point;
    1416import java.awt.Rectangle;
    1517import java.awt.Stroke;
     
    1618import java.awt.Toolkit;
    1719import java.awt.event.AWTEventListener;
    1820import java.awt.event.ActionEvent;
     21import java.awt.event.ActionListener;
    1922import java.awt.event.InputEvent;
    2023import java.awt.event.KeyEvent;
    2124import java.awt.event.MouseEvent;
     
    2831import java.util.Collection;
    2932import java.util.LinkedList;
    3033import java.util.List;
     34import java.util.Set;
     35import java.util.TreeSet;
     36import javax.swing.JCheckBoxMenuItem;
     37import javax.swing.JFrame;
     38import javax.swing.JMenuItem;
     39import javax.swing.SwingUtilities;
     40import javax.swing.Timer;
    3141
    3242import org.openstreetmap.josm.Main;
     43import org.openstreetmap.josm.actions.JosmAction;
    3344import org.openstreetmap.josm.command.AddCommand;
    3445import org.openstreetmap.josm.command.ChangeCommand;
    3546import org.openstreetmap.josm.command.Command;
     
    4253import org.openstreetmap.josm.data.osm.Way;
    4354import org.openstreetmap.josm.data.osm.WaySegment;
    4455import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
     56import org.openstreetmap.josm.gui.MainMenu;
    4557import org.openstreetmap.josm.gui.MapFrame;
    4658import org.openstreetmap.josm.gui.MapView;
    4759import org.openstreetmap.josm.gui.layer.Layer;
     
    6274    private Mode mode = Mode.select;
    6375
    6476    /**
    65      * If true, when extruding create new node even if segments parallel.
     77     * If {@code true}, when extruding create new node(s) even if segments are parallel.
    6678     */
    6779    private boolean alwaysCreateNodes = false;
    6880    private boolean nodeDragWithoutCtrl;
     
    94106    /**
    95107     * Collection of nodes that is moved
    96108     */
    97     private Collection<OsmPrimitive> movingNodeList;
     109    private ArrayList<OsmPrimitive> movingNodeList;
    98110
    99111    /**
    100112     * The direction that is currently active.
     
    130142     * the command that performed last move.
    131143     */
    132144    private MoveCommand moveCommand;
     145    /**
     146     *  The command used for dual alignment movement.
     147     *  Needs to be separate, due to two nodes moving in different directions.
     148     */
     149    private MoveCommand moveCommand2;
    133150
    134151    /** The cursor for the 'create_new' mode. */
    135152    private final Cursor cursorCreateNew;
     
    152169            this.p2 = p2;
    153170            this.perpendicular = perpendicular;
    154171        }
     172
     173        @Override
     174        public String toString() {
     175            return "ReferenceSegment[en=" + en + ", p1=" + p1 + ", p2=" + p2 + ", perp=" + perpendicular + "]";
     176        }
    155177    }
    156178
    157     /**
    158      * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
    159      */
    160     private final AWTEventListener altKeyListener = new AWTEventListener() {
     179    // Dual alignment mode stuff
     180    /** {@code true}, if dual alignment mode is enabled. User wants following extrude to be dual aligned. */
     181    private boolean dualAlignEnabled;
     182    /** {@code true}, if dual alignment is active. User is dragging the mouse, required conditions are met. Treat {@link #mode} (extrude/translate/create_new) as dual aligned. */
     183    private boolean dualAlignActive;
     184    /** Dual alignment reference segments */
     185    private ReferenceSegment dualAlignSegment1, dualAlignSegment2;
     186    // Dual alignment UI stuff
     187    private final DualAlignChangeAction dualAlignChangeAction;
     188    private final JCheckBoxMenuItem dualAlignCheckboxMenuItem;
     189    private final Shortcut dualAlignShortcut;
     190    private boolean useRepeatedShortcut;
     191
     192    private class DualAlignChangeAction extends JosmAction {
     193        public DualAlignChangeAction() {
     194            super(tr("Dual alignment"), "mapmode/extrude/dualalign",
     195                    tr("Switch dual alignment mode while extruding"), null, false);
     196            putValue("help", ht("/Action/Extrude#DualAlign"));
     197        }
     198
    161199        @Override
    162         public void eventDispatched(AWTEvent e) {
    163             if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable())
    164                 return;
    165             InputEvent ie = (InputEvent) e;
    166             boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
    167             boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
    168             boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
    169             if (mode == Mode.select) {
    170                 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
    171             }
     200        public void actionPerformed(ActionEvent e) {
     201            toggleDualAlign();
    172202        }
    173     };
     203    }
    174204
    175205    /**
    176      * Create a new SelectAction
     206     * Creates a new ExtrudeAction
    177207     * @param mapFrame The MapFrame this action belongs to.
    178208     */
    179209    public ExtrudeAction(MapFrame mapFrame) {
     
    185215        cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
    186216        cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
    187217        cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
     218
     219        dualAlignEnabled = false;
     220        dualAlignChangeAction = new DualAlignChangeAction();
     221        dualAlignCheckboxMenuItem = addDualAlignMenuItem();
     222        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
     223        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
     224        dualAlignShortcut = Shortcut.registerShortcut("mapmode:extrudedualalign",
     225                tr("Mode: {0}", tr("Extrude Dual alignment")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
     226        useRepeatedShortcut = Main.pref.getBoolean("extrude.dualalign.toggleOnRepeatedX", true);
     227        timer = new Timer(0, new ActionListener() {
     228            @Override
     229            public void actionPerformed(ActionEvent ae) {
     230                timer.stop();
     231                if (set.remove(releaseEvent.getKeyCode())) {
     232                    doKeyReleaseEvent(releaseEvent);
     233                }
     234            }
     235        });
    188236    }
    189237
    190     @Override public String getModeHelpText() {
    191         if (mode == Mode.translate)
    192             return tr("Move a segment along its normal, then release the mouse button.");
    193         else if (mode == Mode.extrude)
    194             return tr("Draw a rectangle of the desired size, then release the mouse button.");
    195         else if (mode == Mode.create_new)
    196             return tr("Draw a rectangle of the desired size, then release the mouse button.");
    197         else
    198             return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
    199             "Alt-drag to create a new rectangle, double click to add a new node.");
     238    @Override
     239    public void destroy() {
     240        super.destroy();
     241        dualAlignChangeAction.destroy();
    200242    }
    201243
    202     @Override public boolean layerIsSupported(Layer l) {
     244    private JCheckBoxMenuItem addDualAlignMenuItem() {
     245        int n = Main.main.menu.editMenu.getItemCount();
     246        for (int i = n-1; i>0; i--) {
     247            JMenuItem item = Main.main.menu.editMenu.getItem(i);
     248            if (item != null && item.getAction() != null && item.getAction() instanceof DualAlignChangeAction) {
     249                Main.main.menu.editMenu.remove(i);
     250            }
     251        }
     252        return MainMenu.addWithCheckbox(Main.main.menu.editMenu, dualAlignChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
     253    }
     254
     255    // -------------------------------------------------------------------------
     256    // Mode methods
     257    // -------------------------------------------------------------------------
     258
     259    @Override
     260    public String getModeHelpText() {
     261        StringBuilder rv;
     262        if (mode == Mode.select) {
     263            rv = new StringBuilder(tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
     264                "Alt-drag to create a new rectangle, double click to add a new node."));
     265            if (dualAlignEnabled)
     266                rv.append(" ").append(tr("Dual alignment active."));
     267        } else {
     268            if (mode == Mode.translate)
     269                rv = new StringBuilder(tr("Move a segment along its normal, then release the mouse button."));
     270            else if (mode == Mode.translate_node)
     271                rv = new StringBuilder(tr("Move the node along one of the segments, then release the mouse button."));
     272            else if (mode == Mode.extrude)
     273                rv = new StringBuilder(tr("Draw a rectangle of the desired size, then release the mouse button."));
     274            else if (mode == Mode.create_new)
     275                rv = new StringBuilder(tr("Draw a rectangle of the desired size, then release the mouse button."));
     276            else {
     277                Main.warn("Extrude: unknown mode " + mode);
     278                rv = new StringBuilder();
     279            }
     280            if (dualAlignActive)
     281                rv.append(" ").append(tr("Dual alignment active."));
     282        }
     283        return rv.toString();
     284    }
     285
     286    @Override
     287    public boolean layerIsSupported(Layer l) {
    203288        return l instanceof OsmDataLayer;
    204289    }
    205290
    206     @Override public void enterMode() {
     291    @Override
     292    public void enterMode() {
    207293        super.enterMode();
    208294        Main.map.mapView.addMouseListener(this);
    209295        Main.map.mapView.addMouseMotionListener(this);
     
    225311        mainStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.main", "3"));
    226312
    227313        ignoreSharedNodes = Main.pref.getBoolean("extrude.ignore-shared-nodes", true);
     314        dualAlignCheckboxMenuItem.getAction().setEnabled(true);
    228315    }
    229316
    230     @Override public void exitMode() {
     317    @Override
     318    public void exitMode() {
    231319        Main.map.mapView.removeMouseListener(this);
    232320        Main.map.mapView.removeMouseMotionListener(this);
    233321        Main.map.mapView.removeTemporaryLayer(this);
     322        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
    234323        try {
    235324            Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
    236325        } catch (SecurityException ex) {
     
    239328        super.exitMode();
    240329    }
    241330
     331    // -------------------------------------------------------------------------
     332    // Event handlers
     333    // -------------------------------------------------------------------------
     334
    242335    /**
    243      * If the left mouse button is pressed over a segment, switch
    244      * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
     336     * This listener is used to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed,
     337     * and for listening to dual alignment shortcuts.
    245338     */
    246     @Override public void mousePressed(MouseEvent e) {
     339    private final AWTEventListener altKeyListener = new AWTEventListener() {
     340        @Override
     341        public void eventDispatched(AWTEvent e) {
     342            if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable())
     343                return;
     344            InputEvent ie = (InputEvent) e;
     345            boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
     346            boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
     347            boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
     348            if (mode == Mode.select) {
     349                Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
     350            }
     351            if (e instanceof KeyEvent) {
     352                KeyEvent ke = (KeyEvent) e;
     353                if (dualAlignShortcut.isEvent(ke) || (useRepeatedShortcut && getShortcut().isEvent(ke))) {
     354                    Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
     355                    if (SwingUtilities.getWindowAncestor(focused) instanceof JFrame) {
     356                        processKeyEvent(ke);
     357                    }
     358                }
     359            }
     360        }
     361    };
     362
     363    // events for crossplatform key holding processing
     364    // thanks to http://www.arco.in-berlin.de/keyevent.html
     365    private final Set<Integer> set = new TreeSet<Integer>();
     366    private KeyEvent releaseEvent;
     367    private Timer timer;
     368    private void processKeyEvent(KeyEvent e) {
     369        if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
     370            return;
     371
     372        if (e.getID() == KeyEvent.KEY_PRESSED) {
     373            if (timer.isRunning()) {
     374                timer.stop();
     375            } else if (set.add((e.getKeyCode()))) {
     376                doKeyPressEvent(e);
     377            }
     378        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
     379            if (timer.isRunning()) {
     380                timer.stop();
     381                if (set.remove(e.getKeyCode())) {
     382                    doKeyReleaseEvent(e);
     383                }
     384            } else {
     385                releaseEvent = e;
     386                timer.restart();
     387            }
     388        }
     389    }
     390
     391    private void doKeyPressEvent(KeyEvent e) {
     392    }
     393
     394    private void doKeyReleaseEvent(KeyEvent e) {
     395        toggleDualAlign();
     396    }
     397
     398    /**
     399     * Toggles dual alignment mode.
     400     */
     401    private void toggleDualAlign() {
     402        dualAlignEnabled = !dualAlignEnabled;
     403        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
     404        updateStatusLine();
     405    }
     406
     407    /**
     408     * If the left mouse button is pressed over a segment or a node, switches
     409     * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and
     410     * {@link #dualAlignEnabled}.
     411     * @param e
     412     */
     413    @Override
     414    public void mousePressed(MouseEvent e) {
    247415        if(!Main.map.mapView.isActiveLayerVisible())
    248416            return;
    249417        if (!(Boolean)this.getValue("active"))
     
    270438                    return;
    271439                }
    272440                mode = Mode.translate_node;
     441                dualAlignActive = false;
    273442            }
    274443        } else {
    275444            // Otherwise switch to another mode
     445            if (dualAlignEnabled && checkDualAlignConditions()) {
     446                dualAlignActive = true;
     447                calculatePossibleDirectionsForDualAlign();
     448            } else {
     449                dualAlignActive = false;
     450                calculatePossibleDirectionsBySegment();
     451            }
    276452            if (ctrl) {
    277453                mode = Mode.translate;
    278454                movingNodeList = new ArrayList<>();
     
    288464                getCurrentDataSet().setSelected(selectedSegment.way);
    289465                alwaysCreateNodes = shift;
    290466            }
    291             calculatePossibleDirectionsBySegment();
    292467        }
    293468
    294469        // Signifies that nothing has happened yet
     
    295470        newN1en = null;
    296471        newN2en = null;
    297472        moveCommand = null;
     473        moveCommand2 = null;
    298474
    299475        Main.map.mapView.addTemporaryLayer(this);
    300476
     
    309485   }
    310486
    311487    /**
    312      * Perform action depending on what mode we're in.
     488     * Performs action depending on what {@link #mode} we're in.
     489     * @param e
    313490     */
    314     @Override public void mouseDragged(MouseEvent e) {
     491    @Override
     492    public void mouseDragged(MouseEvent e) {
    315493        if(!Main.map.mapView.isActiveLayerVisible())
    316494            return;
    317495
     
    326504
    327505            EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
    328506            EastNorth bestMovement = calculateBestMovement(mouseEn);
     507            EastNorth n1movedEn = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
    329508
    330             newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
    331             newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
    332 
    333509            // find out the movement distance, in metres
    334             double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
     510            double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(n1movedEn));
    335511            Main.map.statusLine.setDist(distance);
    336512            updateStatusLine();
    337513
    338514            Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
    339515
    340             if (mode == Mode.extrude || mode == Mode.create_new) {
    341                 //nothing here
    342             } else if (mode == Mode.translate_node || mode == Mode.translate) {
    343                 //move nodes to new position
    344                 if (moveCommand == null) {
    345                     //make a new move command
    346                     moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
    347                     Main.main.undoRedo.add(moveCommand);
    348                 } else {
    349                     //reuse existing move command
    350                     moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
     516            if (dualAlignActive) {
     517                calculateDualAlignNodesPositions(bestMovement);
     518
     519                if (mode == Mode.extrude || mode == Mode.create_new) {
     520                    // nothing here
     521                } else if (mode == Mode.translate) {
     522                    EastNorth movement1 = initialN1en.sub(newN1en);
     523                    EastNorth movement2 = initialN2en.sub(newN2en);
     524                    // move nodes to new position
     525                    if (moveCommand == null || moveCommand2 == null) {
     526                        // make a new move commands
     527                        moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
     528                        moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
     529                        Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
     530                        Main.main.undoRedo.add(c);
     531                    } else {
     532                        // reuse existing move commands
     533                        moveCommand.moveAgainTo(movement1.getX(), movement1.getY());
     534                        moveCommand2.moveAgainTo(movement2.getX(), movement2.getY());
     535                    }
    351536                }
     537            } else {
     538                newN1en = n1movedEn;
     539                newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
     540
     541                if (mode == Mode.extrude || mode == Mode.create_new) {
     542                    //nothing here
     543                } else if (mode == Mode.translate_node || mode == Mode.translate) {
     544                    //move nodes to new position
     545                    if (moveCommand == null) {
     546                        //make a new move command
     547                        moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
     548                        Main.main.undoRedo.add(moveCommand);
     549                    } else {
     550                        //reuse existing move command
     551                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
     552                    }
     553                }
    352554            }
    353555
    354556            Main.map.mapView.repaint();
     
    356558    }
    357559
    358560    /**
    359      * Do anything that needs to be done, then switch back to select mode
     561     * Does anything that needs to be done, then switches back to select mode.
     562     * @param e
    360563     */
    361     @Override public void mouseReleased(MouseEvent e) {
     564    @Override
     565    public void mouseReleased(MouseEvent e) {
    362566
    363567        if(!Main.map.mapView.isActiveLayerVisible())
    364568            return;
     
    384588                //the move command is already committed in mouseDragged
    385589            }
    386590
    387             boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
    388             boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
    389             boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
     591            updateKeyModifiers(e);
    390592            // Switch back into select mode
    391593            Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
    392594            Main.map.mapView.removeTemporaryLayer(this);
     
    399601        }
    400602    }
    401603
     604    // -------------------------------------------------------------------------
     605    // Custom methods
     606    // -------------------------------------------------------------------------
     607
    402608    /**
    403      * Insert node into nearby segment
    404      * @param e - current mouse point
     609     * Inserts node into nearby segment.
     610     * @param e current mouse point
    405611     */
    406612    private void addNewNode(MouseEvent e) {
    407613        // Should maybe do the same as in DrawAction and fetch all nearby segments?
     
    419625        }
    420626    }
    421627
     628    /**
     629     * Creates a new way that shares segment with selected way.
     630     */
    422631    private void createNewRectangle() {
    423632        if (selectedSegment == null) return;
    424633        // crete a new rectangle
     
    442651    }
    443652
    444653    /**
    445      * Do actual extrusion of @field selectedSegment
     654     * Does actual extrusion of {@link #selectedSegment}.
    446655     */
    447656    private void performExtrusion() {
    448657        // create extrusion
     
    457666        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
    458667        // segmentAngleZero marks subset of nodeOverlapsSegment. nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0
    459668        boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5;
    460         boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
     669        boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
    461670
    462671        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
    463672            //move existing node
     
    521730    }
    522731
    523732    /**
    524      * This method tests if a node has other ways apart from the given one.
     733     * This method tests if {@code node} has other ways apart from the given one.
    525734     * @param node
    526735     * @param myWay
    527      * @return true of node belongs only to myWay, false if there are more ways.
     736     * @return {@code true} if {@code node} belongs only to {@code myWay}, false if there are more ways.
    528737     */
    529     private boolean hasNodeOtherWays(Node node, Way myWay) {
     738    private static boolean hasNodeOtherWays(Node node, Way myWay) {
    530739        for (OsmPrimitive p : node.getReferrers()) {
    531740            if (p instanceof Way && p.isUsable() && p != myWay)
    532741                return true;
     
    535744    }
    536745
    537746    /**
    538      * Determine best movenemnt from initialMousePos  to current position @param mouseEn,
    539      * choosing one of the directions @field possibleMoveDirections
     747     * Determines best movement from {@link #initialMousePos} to current mouse position,
     748     * choosing one of the directions from {@link #possibleMoveDirections}.
     749     * @param mouseEn current mouse position
    540750     * @return movement vector
    541751     */
    542752    private EastNorth calculateBestMovement(EastNorth mouseEn) {
     
    564774            }
    565775        }
    566776        return bestMovement;
     777
     778
    567779    }
    568780
    569781    /***
    570      * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
    571      * @param segmentP1
    572      * @param segmentP2
    573      * @param targetPos
     782     * This method calculates offset amount by which to move the given segment
     783     * perpendicularly for it to be in line with mouse position.
     784     * @param segmentP1 segment's first point
     785     * @param segmentP2 segment's second point
     786     * @param moveDirection direction of movement
     787     * @param targetPos mouse position
    574788     * @return offset amount of P1 and P2.
    575789     */
    576790    private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
     
    590804    }
    591805
    592806    /**
    593      * Gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
     807     * Gathers possible move directions - perpendicular to the selected segment
     808     * and parallel to neighboring segments.
    594809     */
    595810    private void calculatePossibleDirectionsBySegment() {
    596811        // remember initial positions for segment nodes.
     
    626841    }
    627842
    628843    /**
    629      * Gather possible move directions - along all adjacent segments
     844     * Gathers possible move directions - along all adjacent segments.
    630845     */
    631846    private void calculatePossibleDirectionsByNode() {
    632847        // remember initial positions for segment nodes.
     
    647862    }
    648863
    649864    /**
    650      * Gets a node from selected way before given index.
     865     * Checks dual alignment conditions:
     866     *  1. selected segment has both neighboring segments,
     867     *  2. selected segment is not parallel with neighboring segments.
     868     * @return {@code true} if dual alignment conditions are satisfied
     869     */
     870    private boolean checkDualAlignConditions() {
     871        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
     872        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
     873        if (prevNode == null || nextNode == null) {
     874            return false;
     875        }
     876
     877        EastNorth n1en = selectedSegment.getFirstNode().getEastNorth();
     878        EastNorth n2en = selectedSegment.getSecondNode().getEastNorth();
     879        boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en);
     880        boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en);
     881        if (prevSegmentParallel || nextSegmentParallel) {
     882            return false;
     883        }
     884
     885        return true;
     886    }
     887
     888    /**
     889     * Gathers possible move directions - perpendicular to the selected segment only.
     890     * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}.
     891     */
     892    private void calculatePossibleDirectionsForDualAlign() {
     893        // remember initial positions for segment nodes.
     894        initialN1en = selectedSegment.getFirstNode().getEastNorth();
     895        initialN2en = selectedSegment.getSecondNode().getEastNorth();
     896
     897        // add direction perpendicular to the selected segment
     898        possibleMoveDirections = new ArrayList<ReferenceSegment>();
     899        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
     900                initialN1en.getY() - initialN2en.getY(),
     901                initialN2en.getX() - initialN1en.getX()
     902                ), initialN1en, initialN2en, true));
     903
     904        // set neighboring segments
     905        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
     906        EastNorth prevNodeEn = prevNode.getEastNorth();
     907        dualAlignSegment1 = new ReferenceSegment(new EastNorth(
     908            initialN1en.getX() - prevNodeEn.getX(),
     909            initialN1en.getY() - prevNodeEn.getY()
     910            ), initialN1en, prevNodeEn, false);
     911       
     912        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
     913        EastNorth nextNodeEn = nextNode.getEastNorth();
     914        dualAlignSegment2 = new ReferenceSegment(new EastNorth(
     915            initialN2en.getX() - nextNodeEn.getX(),
     916            initialN2en.getY() - nextNodeEn.getY()
     917            ), initialN2en,  nextNodeEn, false);
     918    }
     919
     920    /**
     921     * Calculates positions of new nodes, aligning them to neighboring segments.
     922     * @param movement movement to be used
     923     */
     924    private void calculateDualAlignNodesPositions(EastNorth movement) {
     925        // new positions of selected segment's nodes, without applying dual alignment
     926        EastNorth n1movedEn = new EastNorth(initialN1en.getX() + movement.getX(), initialN1en.getY() + movement.getY());
     927        EastNorth n2movedEn = new EastNorth(initialN2en.getX() + movement.getX(), initialN2en.getY() + movement.getY());
     928
     929        // calculate intersections
     930        newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2);
     931        newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2);
     932    }
     933
     934    /**
     935     * Gets a node index from selected way before given index.
    651936     * @param index  index of current node
    652      * @return index of previous node or -1 if there are no nodes there.
     937     * @return index of previous node or <code>-1</code> if there are no nodes there.
    653938     */
    654939    private int getPreviousNodeIndex(int index) {
    655940        if (index > 0)
     
    663948    /**
    664949     * Gets a node from selected way before given index.
    665950     * @param index  index of current node
    666      * @return previous node or null if there are no nodes there.
     951     * @return previous node or <code>null</code> if there are no nodes there.
    667952     */
    668953    private Node getPreviousNode(int index) {
    669954        int indexPrev = getPreviousNodeIndex(index);
     
    675960
    676961
    677962    /**
    678      * Gets a node from selected way after given index.
     963     * Gets a node index from selected way after given index.
    679964     * @param index index of current node
    680      * @return index of next node or -1 if there are no nodes there.
     965     * @return index of next node or <code>-1</code> if there are no nodes there.
    681966     */
    682967    private int getNextNodeIndex(int index) {
    683968        int count = selectedSegment.way.getNodesCount();
     
    692977    /**
    693978     * Gets a node from selected way after given index.
    694979     * @param index index of current node
    695      * @return next node or null if there are no nodes there.
     980     * @return next node or <code>null</code> if there are no nodes there.
    696981     */
    697982    private Node getNextNode(int index) {
    698983        int indexNext = getNextNodeIndex(index);
     
    702987            return null;
    703988    }
    704989
     990    // -------------------------------------------------------------------------
     991    // paint methods
     992    // -------------------------------------------------------------------------
     993
    705994    @Override
    706995    public void paint(Graphics2D g, MapView mv, Bounds box) {
    707996        Graphics2D g2 = g;
     
    7271016                    b.lineTo(p1.x, p1.y);
    7281017                    g2.draw(b);
    7291018
    730                     if (activeMoveDirection != null) {
     1019                    if (dualAlignActive) {
     1020                        // Draw reference ways
     1021                        drawReferenceSegment(g2, mv, dualAlignSegment1.p1, dualAlignSegment1.p2);
     1022                        drawReferenceSegment(g2, mv, dualAlignSegment2.p1, dualAlignSegment2.p2);
     1023                    } else if (activeMoveDirection != null) {
    7311024                        // Draw reference way
    732                         Point pr1 = mv.getPoint(activeMoveDirection.p1);
    733                         Point pr2 = mv.getPoint(activeMoveDirection.p2);
    734                         b = new GeneralPath();
    735                         b.moveTo(pr1.x, pr1.y);
    736                         b.lineTo(pr2.x, pr2.y);
    737                         g2.setColor(helperColor);
    738                         g2.setStroke(helperStrokeDash);
    739                         g2.draw(b);
     1025                        drawReferenceSegment(g2, mv, activeMoveDirection.p1, activeMoveDirection.p2);
    7401026
    7411027                        // Draw right angle marker on first node position, only when moving at right angle
    7421028                        if (activeMoveDirection.perpendicular) {
     
    7461032                            double headingDiff = headingRefWS - headingMoveDir;
    7471033                            if (headingDiff < 0) headingDiff += 2 * Math.PI;
    7481034                            boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
     1035                            Point pr1 = mv.getPoint(activeMoveDirection.p1);
    7491036                            drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
    7501037                        }
    7511038                    }
     
    7611048                        g2.draw(oldline);
    7621049                    }
    7631050
    764                     if (activeMoveDirection != null) {
     1051                    if (dualAlignActive) {
     1052                        // Draw reference ways
     1053                        drawReferenceSegment(g2, mv, dualAlignSegment1.p1, dualAlignSegment1.p2);
     1054                        drawReferenceSegment(g2, mv, dualAlignSegment2.p1, dualAlignSegment2.p2);
     1055                    } else if (activeMoveDirection != null) {
    7651056
    7661057                        g2.setColor(helperColor);
    7671058                        g2.setStroke(helperStrokeDash);
     
    8021093        return normalUnitVector;
    8031094    }
    8041095
     1096    /**
     1097     * Draws right angle symbol at specified position.
     1098     * @param g2 the Graphics2D object used to draw on
     1099     * @param center center point of angle
     1100     * @param normal vector of normal
     1101     * @param mirror {@code true} if symbol should be mirrored by the normal
     1102     */
    8051103    private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) {
    8061104        // EastNorth units per pixel
    8071105        double factor = 1.0/g2.getTransform().getScaleX();
     
    8231121    }
    8241122
    8251123    /**
    826      * Create a new Line that extends off the edge of the viewport in one direction
     1124     * Draws given reference segment.
     1125     * @param g2 the Graphics2D object used to draw on
     1126     * @param mv
     1127     * @param p1en segment's first point
     1128     * @param p2en segment's second point
     1129     */
     1130    private void drawReferenceSegment(Graphics2D g2, MapView mv, EastNorth p1en, EastNorth p2en)
     1131    {
     1132        Point p1 = mv.getPoint(p1en);
     1133        Point p2 = mv.getPoint(p2en);
     1134        GeneralPath b = new GeneralPath();
     1135        b.moveTo(p1.x, p1.y);
     1136        b.lineTo(p2.x, p2.y);
     1137        g2.setColor(helperColor);
     1138        g2.setStroke(helperStrokeDash);
     1139        g2.draw(b);
     1140    }
     1141
     1142    /**
     1143     * Creates a new Line that extends off the edge of the viewport in one direction
    8271144     * @param start The start point of the line
    8281145     * @param unitvector A unit vector denoting the direction of the line
    8291146     * @param g the Graphics2D object  it will be used on
     1147     * @return created line
    8301148     */
    8311149    private static Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
    8321150        Rectangle bounds = g.getDeviceConfiguration().getBounds();