Ticket #7991: 7991-DualAlign.patch

File 7991-DualAlign.patch, 34.4 KB (added by AlfonZ, 11 years ago)

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

  • 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.TreeSet;
     35import javax.swing.JCheckBoxMenuItem;
     36import javax.swing.JFrame;
     37import javax.swing.JMenuItem;
     38import javax.swing.SwingUtilities;
     39import javax.swing.Timer;
    3140
    3241import org.openstreetmap.josm.Main;
     42import org.openstreetmap.josm.actions.JosmAction;
    3343import org.openstreetmap.josm.command.AddCommand;
    3444import org.openstreetmap.josm.command.ChangeCommand;
    3545import org.openstreetmap.josm.command.Command;
     
    4252import org.openstreetmap.josm.data.osm.Way;
    4353import org.openstreetmap.josm.data.osm.WaySegment;
    4454import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
     55import org.openstreetmap.josm.gui.MainMenu;
    4556import org.openstreetmap.josm.gui.MapFrame;
    4657import org.openstreetmap.josm.gui.MapView;
    4758import org.openstreetmap.josm.gui.layer.Layer;
     
    6273    private Mode mode = Mode.select;
    6374
    6475    /**
    65      * If true, when extruding create new node even if segments parallel.
     76     * If <code>true</code>, when extruding create new node(s) even if segments are parallel.
    6677     */
    6778    private boolean alwaysCreateNodes = false;
    6879    private boolean nodeDragWithoutCtrl;
     
    91102    /**
    92103     * Collection of nodes that is moved
    93104     */
    94     private Collection<OsmPrimitive> movingNodeList;
     105    private ArrayList<OsmPrimitive> movingNodeList;
    95106
    96107    /**
    97108     * The direction that is currently active.
     
    122133     * the command that performed last move.
    123134     */
    124135    private MoveCommand moveCommand;
     136    /**
     137     *  The command used for dual alignment movement.
     138     *  Needs to be separate, due to two nodes moving in different directions.
     139     */
     140    private MoveCommand moveCommand2;
    125141
    126142    /** The cursor for the 'create_new' mode. */
    127143    private final Cursor cursorCreateNew;
     
    132148    /** The cursor for the 'alwaysCreateNodes' submode. */
    133149    private final Cursor cursorCreateNodes;
    134150
    135     private class ReferenceSegment {
     151    private static class ReferenceSegment {
    136152        public final EastNorth en;
    137153        public final EastNorth p1;
    138154        public final EastNorth p2;
     
    144160            this.p2 = p2;
    145161            this.perpendicular = perpendicular;
    146162        }
     163
     164        @Override
     165        public String toString() {
     166            return "ReferenceSegment[en=" + en + ", p1=" + p1 + ", p2=" + p2 + ", perp=" + perpendicular + "]";
     167        }
    147168    }
    148169
    149     /**
    150      * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
    151      */
    152     private final AWTEventListener altKeyListener = new AWTEventListener() {
     170    // Dual alignment mode stuff
     171    /** <code>true</code>, if dual alignment mode is enabled. User wants following extrude to be dual aligned. */
     172    private boolean dualAlignEnabled;
     173    /** <code>true</code>, if dual alignment is active. User is dragging the mouse, required conditions are met. Treat {@link #mode} (extrude/translate/create_new) as dual aligned. */
     174    private boolean dualAlignActive;
     175    /** Dual alignment reference segments */
     176    private ReferenceSegment dualAlignSegment1, dualAlignSegment2;
     177    // Dual alignment UI stuff
     178    private final DualAlignChangeAction dualAlignChangeAction;
     179    private final JCheckBoxMenuItem dualAlignCheckboxMenuItem;
     180    private final Shortcut dualAlignShortcut;
     181    private boolean useRepeatedShortcut;
     182
     183    private class DualAlignChangeAction extends JosmAction {
     184        public DualAlignChangeAction() {
     185            super(tr("Dual alignment"), "mapmode/extrude/dualalign",
     186                    tr("Switch dual alignment mode while extruding"), null, false);
     187            putValue("help", ht("/Action/Extrude/DualAlign"));
     188        }
     189
    153190        @Override
    154         public void eventDispatched(AWTEvent e) {
    155             if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
    156                 return;
    157             InputEvent ie = (InputEvent) e;
    158             boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
    159             boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
    160             boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
    161             if (mode == Mode.select) {
    162                 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
    163             }
     191        public void actionPerformed(ActionEvent e) {
     192            toggleDualAlign();
    164193        }
    165     };
     194    }
    166195
    167196    /**
    168      * Create a new SelectAction
     197     * Creates a new ExtrudeAction
    169198     * @param mapFrame The MapFrame this action belongs to.
    170199     */
    171200    public ExtrudeAction(MapFrame mapFrame) {
     
    177206        cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
    178207        cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
    179208        cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
     209
     210        dualAlignEnabled = false;
     211        dualAlignChangeAction = new DualAlignChangeAction();
     212        dualAlignCheckboxMenuItem = addDualAlignMenuItem();
     213        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
     214        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
     215        dualAlignShortcut = Shortcut.registerShortcut("mapmode:extrudedualalign",
     216                tr("Mode: {0}", tr("Extrude Dual alignment")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
     217        useRepeatedShortcut = Main.pref.getBoolean("extrude.dualalign.toggleOnRepeatedX", true);
     218        timer = new Timer(0, new ActionListener() {
     219            @Override
     220            public void actionPerformed(ActionEvent ae) {
     221                timer.stop();
     222                if (set.remove(releaseEvent.getKeyCode())) {
     223                    doKeyReleaseEvent(releaseEvent);
     224                }
     225            }
     226        });
    180227    }
    181228
    182     @Override public String getModeHelpText() {
    183         if (mode == Mode.translate)
    184             return tr("Move a segment along its normal, then release the mouse button.");
    185         else if (mode == Mode.extrude)
    186             return tr("Draw a rectangle of the desired size, then release the mouse button.");
    187         else if (mode == Mode.create_new)
    188             return tr("Draw a rectangle of the desired size, then release the mouse button.");
    189         else
    190             return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
    191             "Alt-drag to create a new rectangle, double click to add a new node.");
     229    @Override
     230    public void destroy() {
     231        super.destroy();
     232        dualAlignChangeAction.destroy();
    192233    }
    193234
    194     @Override public boolean layerIsSupported(Layer l) {
     235    private JCheckBoxMenuItem addDualAlignMenuItem() {
     236        int n = Main.main.menu.editMenu.getItemCount();
     237        for (int i = n-1; i>0; i--) {
     238            JMenuItem item = Main.main.menu.editMenu.getItem(i);
     239            if (item != null && item.getAction() != null && item.getAction() instanceof DualAlignChangeAction) {
     240                Main.main.menu.editMenu.remove(i);
     241            }
     242        }
     243        return MainMenu.addWithCheckbox(Main.main.menu.editMenu, dualAlignChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
     244    }
     245
     246    // -------------------------------------------------------------------------
     247    // Mode methods
     248    // -------------------------------------------------------------------------
     249
     250    @Override
     251    public String getModeHelpText() {
     252        String rv;
     253        if (mode == Mode.select) {
     254            rv = tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
     255                "Alt-drag to create a new rectangle, double click to add a new node.");
     256            if (dualAlignEnabled)
     257                rv += " " + tr("Dual alignment active.");
     258        } else {
     259            if (mode == Mode.translate)
     260                rv = tr("Move a segment along its normal, then release the mouse button.");
     261            else if (mode == Mode.translate_node)
     262                rv = tr("Move the node along one of the segments, then release the mouse button.");
     263            else if (mode == Mode.extrude)
     264                rv = tr("Draw a rectangle of the desired size, then release the mouse button.");
     265            else if (mode == Mode.create_new)
     266                rv = tr("Draw a rectangle of the desired size, then release the mouse button.");
     267            else {
     268                Main.warn("Extrude: unknown mode " + mode);
     269                rv = "";
     270            }
     271            if (dualAlignActive)
     272                rv += " " + tr("Dual alignment active.");
     273        }
     274        return rv;
     275    }
     276
     277    @Override
     278    public boolean layerIsSupported(Layer l) {
    195279        return l instanceof OsmDataLayer;
    196280    }
    197281
    198     @Override public void enterMode() {
     282    @Override
     283    public void enterMode() {
    199284        super.enterMode();
    200285        Main.map.mapView.addMouseListener(this);
    201286        Main.map.mapView.addMouseMotionListener(this);
     
    213298        nodeDragWithoutCtrl = Main.pref.getBoolean("extrude.drag-nodes-without-ctrl", false);
    214299        oldLineStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.ctrl.stroke.old-line", "1"));
    215300        mainStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.main", "3"));
     301        dualAlignCheckboxMenuItem.getAction().setEnabled(true);
    216302    }
    217303
    218     @Override public void exitMode() {
     304    @Override
     305    public void exitMode() {
    219306        Main.map.mapView.removeMouseListener(this);
    220307        Main.map.mapView.removeMouseMotionListener(this);
    221308        Main.map.mapView.removeTemporaryLayer(this);
     309        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
    222310        try {
    223311            Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
    224312        } catch (SecurityException ex) {
     
    226314        super.exitMode();
    227315    }
    228316
     317    // -------------------------------------------------------------------------
     318    // Event handlers
     319    // -------------------------------------------------------------------------
     320
    229321    /**
    230      * If the left mouse button is pressed over a segment, switch
    231      * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
     322     * This listener is used to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed,
     323     * and for listening to dual alignment shortcuts.
    232324     */
    233     @Override public void mousePressed(MouseEvent e) {
     325    private final AWTEventListener altKeyListener = new AWTEventListener() {
     326        @Override
     327        public void eventDispatched(AWTEvent e) {
     328            if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
     329                return;
     330            InputEvent ie = (InputEvent) e;
     331            boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
     332            boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
     333            boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
     334            if (mode == Mode.select) {
     335                Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
     336            }
     337            if (e instanceof KeyEvent) {
     338                KeyEvent ke = (KeyEvent) e;
     339                if (dualAlignShortcut.isEvent(ke) || (useRepeatedShortcut && getShortcut().isEvent(ke))) {
     340                    Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
     341                    if (SwingUtilities.getWindowAncestor(focused) instanceof JFrame) {
     342                        processKeyEvent(ke);
     343                    }
     344                }
     345            }
     346        }
     347    };
     348
     349    // events for crossplatform key holding processing
     350    // thanks to http://www.arco.in-berlin.de/keyevent.html
     351    private final TreeSet<Integer> set = new TreeSet<Integer>();
     352    private KeyEvent releaseEvent;
     353    private Timer timer;
     354    private void processKeyEvent(KeyEvent e) {
     355        if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
     356            return;
     357
     358        if (e.getID() == KeyEvent.KEY_PRESSED) {
     359            if (timer.isRunning()) {
     360                timer.stop();
     361            } else if (set.add((e.getKeyCode()))) {
     362                doKeyPressEvent(e);
     363            }
     364        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
     365            if (timer.isRunning()) {
     366                timer.stop();
     367                if (set.remove(e.getKeyCode())) {
     368                    doKeyReleaseEvent(e);
     369                }
     370            } else {
     371                releaseEvent = e;
     372                timer.restart();
     373            }
     374        }
     375    }
     376
     377    private void doKeyPressEvent(KeyEvent e) {
     378    }
     379
     380    private void doKeyReleaseEvent(KeyEvent e) {
     381        toggleDualAlign();
     382    }
     383
     384    /**
     385     * Toggles dual alignment mode.
     386     */
     387    private void toggleDualAlign() {
     388        dualAlignEnabled = !dualAlignEnabled;
     389        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
     390        updateStatusLine();
     391    }
     392
     393    /**
     394     * If the left mouse button is pressed over a segment or a node, switches
     395     * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and
     396     * {@link #dualAlignEnabled}.
     397     * @param e
     398     */
     399    @Override
     400    public void mousePressed(MouseEvent e) {
    234401        if(!Main.map.mapView.isActiveLayerVisible())
    235402            return;
    236403        if (!(Boolean)this.getValue("active"))
     
    257424                    return;
    258425                }
    259426                mode = Mode.translate_node;
     427                dualAlignActive = false;
    260428            }
    261429        } else {
    262430            // Otherwise switch to another mode
     431            if (dualAlignEnabled && checkDualAlignConditions()) {
     432                dualAlignActive = true;
     433                calculatePossibleDirectionsForDualAlign();
     434            } else {
     435                dualAlignActive = false;
     436                calculatePossibleDirectionsBySegment();
     437            }
    263438            if (ctrl) {
    264439                mode = Mode.translate;
    265440                movingNodeList = new ArrayList<OsmPrimitive>();
     
    275450                getCurrentDataSet().setSelected(selectedSegment.way);
    276451                alwaysCreateNodes = shift;
    277452            }
    278             calculatePossibleDirectionsBySegment();
    279453        }
    280454
    281455        // Signifies that nothing has happened yet
     
    282456        newN1en = null;
    283457        newN2en = null;
    284458        moveCommand = null;
     459        moveCommand2 = null;
    285460
    286461        Main.map.mapView.addTemporaryLayer(this);
    287462
     
    296471   }
    297472
    298473    /**
    299      * Perform action depending on what mode we're in.
     474     * Performs action depending on what {@link #mode} we're in.
     475     * @param e
    300476     */
    301     @Override public void mouseDragged(MouseEvent e) {
     477    @Override
     478    public void mouseDragged(MouseEvent e) {
    302479        if(!Main.map.mapView.isActiveLayerVisible())
    303480            return;
    304481
     
    313490
    314491            EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
    315492            EastNorth bestMovement = calculateBestMovement(mouseEn);
     493            EastNorth n1movedEn = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
    316494
    317             newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
    318             newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
    319 
    320495            // find out the movement distance, in metres
    321             double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
     496            double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(n1movedEn));
    322497            Main.map.statusLine.setDist(distance);
    323498            updateStatusLine();
    324499
    325500            Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
    326501
    327             if (mode == Mode.extrude || mode == Mode.create_new) {
    328                 //nothing here
    329             } else if (mode == Mode.translate_node || mode == Mode.translate) {
    330                 //move nodes to new position
    331                 if (moveCommand == null) {
    332                     //make a new move command
    333                     moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
    334                     Main.main.undoRedo.add(moveCommand);
    335                 } else {
    336                     //reuse existing move command
    337                     moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
     502            if (dualAlignActive) {
     503                calculateDualAlignNodesPositions(bestMovement);
     504
     505                if (mode == Mode.extrude || mode == Mode.create_new) {
     506                    // nothing here
     507                } else if (mode == Mode.translate) {
     508                    EastNorth movement1 = initialN1en.sub(newN1en);
     509                    EastNorth movement2 = initialN2en.sub(newN2en);
     510                    // move nodes to new position
     511                    if (moveCommand == null || moveCommand2 == null) {
     512                        // make a new move commands
     513                        moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
     514                        moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
     515                        Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
     516                        Main.main.undoRedo.add(c);
     517                    } else {
     518                        // reuse existing move commands
     519                        moveCommand.moveAgainTo(movement1.getX(), movement1.getY());
     520                        moveCommand2.moveAgainTo(movement2.getX(), movement2.getY());
     521                    }
    338522                }
     523            } else {
     524                newN1en = n1movedEn;
     525                newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
     526
     527                if (mode == Mode.extrude || mode == Mode.create_new) {
     528                    //nothing here
     529                } else if (mode == Mode.translate_node || mode == Mode.translate) {
     530                    //move nodes to new position
     531                    if (moveCommand == null) {
     532                        //make a new move command
     533                        moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
     534                        Main.main.undoRedo.add(moveCommand);
     535                    } else {
     536                        //reuse existing move command
     537                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
     538                    }
     539                }
    339540            }
    340541
    341542            Main.map.mapView.repaint();
     
    343544    }
    344545
    345546    /**
    346      * Do anything that needs to be done, then switch back to select mode
     547     * Does anything that needs to be done, then switches back to select mode.
     548     * @param e
    347549     */
    348     @Override public void mouseReleased(MouseEvent e) {
     550    @Override
     551    public void mouseReleased(MouseEvent e) {
    349552
    350553        if(!Main.map.mapView.isActiveLayerVisible())
    351554            return;
     
    371574                //the move command is already committed in mouseDragged
    372575            }
    373576
    374             boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
    375             boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
    376             boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
     577            updateKeyModifiers(e);
    377578            // Switch back into select mode
    378579            Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
    379580            Main.map.mapView.removeTemporaryLayer(this);
     
    386587        }
    387588    }
    388589
     590    // -------------------------------------------------------------------------
     591    // Custom methods
     592    // -------------------------------------------------------------------------
     593
    389594    /**
    390      * Insert node into nearby segment
    391      * @param e - current mouse point
     595     * Inserts node into nearby segment.
     596     * @param e current mouse point
    392597     */
    393598    private void addNewNode(MouseEvent e) {
    394599        // Should maybe do the same as in DrawAction and fetch all nearby segments?
     
    406611        }
    407612    }
    408613
     614    /**
     615     * Creates a new way that shares segment with selected way.
     616     */
    409617    private void createNewRectangle() {
    410618        if (selectedSegment == null) return;
    411619        // crete a new rectangle
     
    429637    }
    430638
    431639    /**
    432      * Do actual extrusion of @field selectedSegment
     640     * Does actual extrusion of {@link #selectedSegment}.
    433641     */
    434642    private void performExtrusion() {
    435643        // create extrusion
     
    442650        //find if the new points overlap existing segments (in case of 90 degree angles)
    443651        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
    444652        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
    445         boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
     653        boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
    446654
    447655        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
    448656            //move existing node
     
    489697    }
    490698
    491699    /**
    492      * This method tests if a node has other ways apart from the given one.
     700     * This method tests if <code>node</code> has other ways apart from the given one.
    493701     * @param node
    494702     * @param myWay
    495      * @return true of node belongs only to myWay, false if there are more ways.
     703     * @return <code>true</code> if <code>node</code> belongs only to <code>myWay</code>, false if there are more ways.
    496704     */
    497     private boolean hasNodeOtherWays(Node node, Way myWay) {
     705    private static boolean hasNodeOtherWays(Node node, Way myWay) {
    498706        for (OsmPrimitive p : node.getReferrers()) {
    499707            if (p instanceof Way && p.isUsable() && p != myWay)
    500708                return true;
     
    503711    }
    504712
    505713    /**
    506      * Determine best movenemnt from initialMousePos  to current position @param mouseEn,
    507      * choosing one of the directions @field possibleMoveDirections
     714     * Determines best movement from {@link #initialMousePos} to current mouse position,
     715     * choosing one of the directions from {@link #possibleMoveDirections}.
     716     * @param mouseEn current mouse position
    508717     * @return movement vector
    509718     */
    510719    private EastNorth calculateBestMovement(EastNorth mouseEn) {
     
    532741            }
    533742        }
    534743        return bestMovement;
     744
     745
    535746    }
    536747
    537748    /***
    538      * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
    539      * @param segmentP1
    540      * @param segmentP2
    541      * @param targetPos
     749     * This method calculates offset amount by which to move the given segment
     750     * perpendicularly for it to be in line with mouse position.
     751     * @param segmentP1 segment's first point
     752     * @param segmentP2 segment's second point
     753     * @param moveDirection direction of movement
     754     * @param targetPos mouse position
    542755     * @return offset amount of P1 and P2.
    543756     */
    544757    private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
     
    558771    }
    559772
    560773    /**
    561      * Gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
     774     * Gathers possible move directions - perpendicular to the selected segment
     775     * and parallel to neighboring segments.
    562776     */
    563777    private void calculatePossibleDirectionsBySegment() {
    564778        // remember initial positions for segment nodes.
     
    594808    }
    595809
    596810    /**
    597      * Gather possible move directions - along all adjacent segments
     811     * Gathers possible move directions - along all adjacent segments.
    598812     */
    599813    private void calculatePossibleDirectionsByNode() {
    600814        // remember initial positions for segment nodes.
     
    615829    }
    616830
    617831    /**
    618      * Gets a node from selected way before given index.
     832     * Checks dual alignment conditions:
     833     *  1. selected segment has both neighboring segments,
     834     *  2. selected segment is not parallel with neighboring segments.
     835     * @return <code>true</code> if dual alignment conditions are satisfied
     836     */
     837    private boolean checkDualAlignConditions() {
     838        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
     839        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
     840        if (prevNode == null || nextNode == null) {
     841            return false;
     842        }
     843
     844        EastNorth n1en = selectedSegment.getFirstNode().getEastNorth();
     845        EastNorth n2en = selectedSegment.getSecondNode().getEastNorth();
     846        boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en);
     847        boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en);
     848        if (prevSegmentParallel || nextSegmentParallel) {
     849            return false;
     850        }
     851
     852        return true;
     853    }
     854
     855    /**
     856     * Gathers possible move directions - perpendicular to the selected segment only.
     857     * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}.
     858     */
     859    private void calculatePossibleDirectionsForDualAlign() {
     860        // remember initial positions for segment nodes.
     861        initialN1en = selectedSegment.getFirstNode().getEastNorth();
     862        initialN2en = selectedSegment.getSecondNode().getEastNorth();
     863
     864        // add direction perpendicular to the selected segment
     865        possibleMoveDirections = new ArrayList<ReferenceSegment>();
     866        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
     867                initialN1en.getY() - initialN2en.getY(),
     868                initialN2en.getX() - initialN1en.getX()
     869                ), initialN1en, initialN2en, true));
     870
     871        // set neighboring segments
     872        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
     873        EastNorth prevNodeEn = prevNode.getEastNorth();
     874        dualAlignSegment1 = new ReferenceSegment(new EastNorth(
     875            initialN1en.getX() - prevNodeEn.getX(),
     876            initialN1en.getY() - prevNodeEn.getY()
     877            ), initialN1en, prevNodeEn, false);
     878       
     879        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
     880        EastNorth nextNodeEn = nextNode.getEastNorth();
     881        dualAlignSegment2 = new ReferenceSegment(new EastNorth(
     882            initialN2en.getX() - nextNodeEn.getX(),
     883            initialN2en.getY() - nextNodeEn.getY()
     884            ), initialN2en,  nextNodeEn, false);
     885    }
     886
     887    /**
     888     * Calculates positions of new nodes, aligning them to neighboring segments.
     889     * @param movement movement to be used
     890     */
     891    private void calculateDualAlignNodesPositions(EastNorth movement) {
     892        // new positions of selected segment's nodes, without applying dual alignment
     893        EastNorth n1movedEn = new EastNorth(initialN1en.getX() + movement.getX(), initialN1en.getY() + movement.getY());
     894        EastNorth n2movedEn = new EastNorth(initialN2en.getX() + movement.getX(), initialN2en.getY() + movement.getY());
     895
     896        // calculate intersections
     897        newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2);
     898        newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2);
     899    }
     900
     901    /**
     902     * Gets a node index from selected way before given index.
    619903     * @param index  index of current node
    620      * @return index of previous node or -1 if there are no nodes there.
     904     * @return index of previous node or <code>-1</code> if there are no nodes there.
    621905     */
    622906    private int getPreviousNodeIndex(int index) {
    623907        if (index > 0)
     
    631915    /**
    632916     * Gets a node from selected way before given index.
    633917     * @param index  index of current node
    634      * @return previous node or null if there are no nodes there.
     918     * @return previous node or <code>null</code> if there are no nodes there.
    635919     */
    636920    private Node getPreviousNode(int index) {
    637921        int indexPrev = getPreviousNodeIndex(index);
     
    643927
    644928
    645929    /**
    646      * Gets a node from selected way after given index.
     930     * Gets a node index from selected way after given index.
    647931     * @param index index of current node
    648      * @return index of next node or -1 if there are no nodes there.
     932     * @return index of next node or <code>-1</code> if there are no nodes there.
    649933     */
    650934    private int getNextNodeIndex(int index) {
    651935        int count = selectedSegment.way.getNodesCount();
     
    660944    /**
    661945     * Gets a node from selected way after given index.
    662946     * @param index index of current node
    663      * @return next node or null if there are no nodes there.
     947     * @return next node or <code>null</code> if there are no nodes there.
    664948     */
    665949    private Node getNextNode(int index) {
    666950        int indexNext = getNextNodeIndex(index);
     
    670954            return null;
    671955    }
    672956
     957    // -------------------------------------------------------------------------
     958    // paint methods
     959    // -------------------------------------------------------------------------
     960
    673961    @Override
    674962    public void paint(Graphics2D g, MapView mv, Bounds box) {
    675963        Graphics2D g2 = g;
     
    695983                    b.lineTo(p1.x, p1.y);
    696984                    g2.draw(b);
    697985
    698                     if (activeMoveDirection != null) {
     986                    if (dualAlignActive) {
     987                        // Draw reference ways
     988                        drawReferenceSegment(g2, mv, dualAlignSegment1.p1, dualAlignSegment1.p2);
     989                        drawReferenceSegment(g2, mv, dualAlignSegment2.p1, dualAlignSegment2.p2);
     990                    } else if (activeMoveDirection != null) {
    699991                        // Draw reference way
    700                         Point pr1 = mv.getPoint(activeMoveDirection.p1);
    701                         Point pr2 = mv.getPoint(activeMoveDirection.p2);
    702                         b = new GeneralPath();
    703                         b.moveTo(pr1.x, pr1.y);
    704                         b.lineTo(pr2.x, pr2.y);
    705                         g2.setColor(helperColor);
    706                         g2.setStroke(helperStrokeDash);
    707                         g2.draw(b);
     992                        drawReferenceSegment(g2, mv, activeMoveDirection.p1, activeMoveDirection.p2);
    708993
    709994                        // Draw right angle marker on first node position, only when moving at right angle
    710995                        if (activeMoveDirection.perpendicular) {
     
    714999                            double headingDiff = headingRefWS - headingMoveDir;
    7151000                            if (headingDiff < 0) headingDiff += 2 * Math.PI;
    7161001                            boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
     1002                            Point pr1 = mv.getPoint(activeMoveDirection.p1);
    7171003                            drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
    7181004                        }
    7191005                    }
     
    7291015                        g2.draw(oldline);
    7301016                    }
    7311017
    732                     if (activeMoveDirection != null) {
     1018                    if (dualAlignActive) {
     1019                        // Draw reference ways
     1020                        drawReferenceSegment(g2, mv, dualAlignSegment1.p1, dualAlignSegment1.p2);
     1021                        drawReferenceSegment(g2, mv, dualAlignSegment2.p1, dualAlignSegment2.p2);
     1022                    } else if (activeMoveDirection != null) {
    7331023
    7341024                        g2.setColor(helperColor);
    7351025                        g2.setStroke(helperStrokeDash);
     
    7701060        return normalUnitVector;
    7711061    }
    7721062
     1063    /**
     1064     * Draws right angle symbol at specified position.
     1065     * @param g2 the Graphics2D object used to draw on
     1066     * @param center center point of angle
     1067     * @param normal vector of normal
     1068     * @param mirror <code>true</code> if symbol should be mirrored by the normal
     1069     */
    7731070    private void drawAngleSymbol(Graphics2D g2, Point2D center, EastNorth normal, boolean mirror) {
    7741071        // EastNorth units per pixel
    7751072        double factor = 1.0/g2.getTransform().getScaleX();
     
    7911088    }
    7921089
    7931090    /**
    794      * Create a new Line that extends off the edge of the viewport in one direction
     1091     * Draws given reference segment.
     1092     * @param g2 the Graphics2D object used to draw on
     1093     * @param mv
     1094     * @param p1en segment's first point
     1095     * @param p2en segment's second point
     1096     */
     1097    private void drawReferenceSegment(Graphics2D g2, MapView mv, EastNorth p1en, EastNorth p2en)
     1098    {
     1099        Point p1 = mv.getPoint(p1en);
     1100        Point p2 = mv.getPoint(p2en);
     1101        GeneralPath b = new GeneralPath();
     1102        b.moveTo(p1.x, p1.y);
     1103        b.lineTo(p2.x, p2.y);
     1104        g2.setColor(helperColor);
     1105        g2.setStroke(helperStrokeDash);
     1106        g2.draw(b);
     1107    }
     1108
     1109    /**
     1110     * Creates a new Line that extends off the edge of the viewport in one direction
    7951111     * @param start The start point of the line
    7961112     * @param unitvector A unit vector denoting the direction of the line
    7971113     * @param g the Graphics2D object  it will be used on
     1114     * @return created line
    7981115     */
    7991116    static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
    8001117        Rectangle bounds = g.getDeviceConfiguration().getBounds();