Ticket #7991: 7991-DualAlign.r6252.patch

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

patch updated to r6252
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}, when extruding create new node(s) even if segments are parallel.
    6677     */
    6778    private boolean alwaysCreateNodes = false;
    6879    private boolean nodeDragWithoutCtrl;
     
    94105    /**
    95106     * Collection of nodes that is moved
    96107     */
    97     private Collection<OsmPrimitive> movingNodeList;
     108    private ArrayList<OsmPrimitive> movingNodeList;
    98109
    99110    /**
    100111     * The direction that is currently active.
     
    125136     * the command that performed last move.
    126137     */
    127138    private MoveCommand moveCommand;
     139    /**
     140     *  The command used for dual alignment movement.
     141     *  Needs to be separate, due to two nodes moving in different directions.
     142     */
     143    private MoveCommand moveCommand2;
    128144
    129145    /** The cursor for the 'create_new' mode. */
    130146    private final Cursor cursorCreateNew;
     
    135151    /** The cursor for the 'alwaysCreateNodes' submode. */
    136152    private final Cursor cursorCreateNodes;
    137153
    138     private class ReferenceSegment {
     154    private static class ReferenceSegment {
    139155        public final EastNorth en;
    140156        public final EastNorth p1;
    141157        public final EastNorth p2;
     
    147163            this.p2 = p2;
    148164            this.perpendicular = perpendicular;
    149165        }
     166
     167        @Override
     168        public String toString() {
     169            return "ReferenceSegment[en=" + en + ", p1=" + p1 + ", p2=" + p2 + ", perp=" + perpendicular + "]";
     170        }
    150171    }
    151172
    152     /**
    153      * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
    154      */
    155     private final AWTEventListener altKeyListener = new AWTEventListener() {
     173    // Dual alignment mode stuff
     174    /** {@code true}, if dual alignment mode is enabled. User wants following extrude to be dual aligned. */
     175    private boolean dualAlignEnabled;
     176    /** {@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. */
     177    private boolean dualAlignActive;
     178    /** Dual alignment reference segments */
     179    private ReferenceSegment dualAlignSegment1, dualAlignSegment2;
     180    // Dual alignment UI stuff
     181    private final DualAlignChangeAction dualAlignChangeAction;
     182    private final JCheckBoxMenuItem dualAlignCheckboxMenuItem;
     183    private final Shortcut dualAlignShortcut;
     184    private boolean useRepeatedShortcut;
     185
     186    private class DualAlignChangeAction extends JosmAction {
     187        public DualAlignChangeAction() {
     188            super(tr("Dual alignment"), "mapmode/extrude/dualalign",
     189                    tr("Switch dual alignment mode while extruding"), null, false);
     190            putValue("help", ht("/Action/Extrude/DualAlign"));
     191        }
     192
    156193        @Override
    157         public void eventDispatched(AWTEvent e) {
    158             if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
    159                 return;
    160             InputEvent ie = (InputEvent) e;
    161             boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
    162             boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
    163             boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
    164             if (mode == Mode.select) {
    165                 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
    166             }
     194        public void actionPerformed(ActionEvent e) {
     195            toggleDualAlign();
    167196        }
    168     };
     197    }
    169198
    170199    /**
    171      * Create a new SelectAction
     200     * Creates a new ExtrudeAction
    172201     * @param mapFrame The MapFrame this action belongs to.
    173202     */
    174203    public ExtrudeAction(MapFrame mapFrame) {
     
    180209        cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
    181210        cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
    182211        cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
     212
     213        dualAlignEnabled = false;
     214        dualAlignChangeAction = new DualAlignChangeAction();
     215        dualAlignCheckboxMenuItem = addDualAlignMenuItem();
     216        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
     217        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
     218        dualAlignShortcut = Shortcut.registerShortcut("mapmode:extrudedualalign",
     219                tr("Mode: {0}", tr("Extrude Dual alignment")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
     220        useRepeatedShortcut = Main.pref.getBoolean("extrude.dualalign.toggleOnRepeatedX", true);
     221        timer = new Timer(0, new ActionListener() {
     222            @Override
     223            public void actionPerformed(ActionEvent ae) {
     224                timer.stop();
     225                if (set.remove(releaseEvent.getKeyCode())) {
     226                    doKeyReleaseEvent(releaseEvent);
     227                }
     228            }
     229        });
    183230    }
    184231
    185     @Override public String getModeHelpText() {
    186         if (mode == Mode.translate)
    187             return tr("Move a segment along its normal, then release the mouse button.");
    188         else if (mode == Mode.extrude)
    189             return tr("Draw a rectangle of the desired size, then release the mouse button.");
    190         else if (mode == Mode.create_new)
    191             return tr("Draw a rectangle of the desired size, then release the mouse button.");
    192         else
    193             return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
    194             "Alt-drag to create a new rectangle, double click to add a new node.");
     232    @Override
     233    public void destroy() {
     234        super.destroy();
     235        dualAlignChangeAction.destroy();
    195236    }
    196237
    197     @Override public boolean layerIsSupported(Layer l) {
     238    private JCheckBoxMenuItem addDualAlignMenuItem() {
     239        int n = Main.main.menu.editMenu.getItemCount();
     240        for (int i = n-1; i>0; i--) {
     241            JMenuItem item = Main.main.menu.editMenu.getItem(i);
     242            if (item != null && item.getAction() != null && item.getAction() instanceof DualAlignChangeAction) {
     243                Main.main.menu.editMenu.remove(i);
     244            }
     245        }
     246        return MainMenu.addWithCheckbox(Main.main.menu.editMenu, dualAlignChangeAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
     247    }
     248
     249    // -------------------------------------------------------------------------
     250    // Mode methods
     251    // -------------------------------------------------------------------------
     252
     253    @Override
     254    public String getModeHelpText() {
     255        String rv;
     256        if (mode == Mode.select) {
     257            rv = tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
     258                "Alt-drag to create a new rectangle, double click to add a new node.");
     259            if (dualAlignEnabled)
     260                rv += " " + tr("Dual alignment active.");
     261        } else {
     262            if (mode == Mode.translate)
     263                rv = tr("Move a segment along its normal, then release the mouse button.");
     264            else if (mode == Mode.translate_node)
     265                rv = tr("Move the node along one of the segments, then release the mouse button.");
     266            else if (mode == Mode.extrude)
     267                rv = tr("Draw a rectangle of the desired size, then release the mouse button.");
     268            else if (mode == Mode.create_new)
     269                rv = tr("Draw a rectangle of the desired size, then release the mouse button.");
     270            else {
     271                Main.warn("Extrude: unknown mode " + mode);
     272                rv = "";
     273            }
     274            if (dualAlignActive)
     275                rv += " " + tr("Dual alignment active.");
     276        }
     277        return rv;
     278    }
     279
     280    @Override
     281    public boolean layerIsSupported(Layer l) {
    198282        return l instanceof OsmDataLayer;
    199283    }
    200284
    201     @Override public void enterMode() {
     285    @Override
     286    public void enterMode() {
    202287        super.enterMode();
    203288        Main.map.mapView.addMouseListener(this);
    204289        Main.map.mapView.addMouseMotionListener(this);
     
    218303        mainStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.main", "3"));
    219304
    220305        ignoreSharedNodes = Main.pref.getBoolean("extrude.ignore-shared-nodes", true);
     306        dualAlignCheckboxMenuItem.getAction().setEnabled(true);
    221307    }
    222308
    223     @Override public void exitMode() {
     309    @Override
     310    public void exitMode() {
    224311        Main.map.mapView.removeMouseListener(this);
    225312        Main.map.mapView.removeMouseMotionListener(this);
    226313        Main.map.mapView.removeTemporaryLayer(this);
     314        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
    227315        try {
    228316            Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
    229317        } catch (SecurityException ex) {
     
    231319        super.exitMode();
    232320    }
    233321
     322    // -------------------------------------------------------------------------
     323    // Event handlers
     324    // -------------------------------------------------------------------------
     325
    234326    /**
    235      * If the left mouse button is pressed over a segment, switch
    236      * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
     327     * This listener is used to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed,
     328     * and for listening to dual alignment shortcuts.
    237329     */
    238     @Override public void mousePressed(MouseEvent e) {
     330    private final AWTEventListener altKeyListener = new AWTEventListener() {
     331        @Override
     332        public void eventDispatched(AWTEvent e) {
     333            if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
     334                return;
     335            InputEvent ie = (InputEvent) e;
     336            boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
     337            boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
     338            boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
     339            if (mode == Mode.select) {
     340                Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
     341            }
     342            if (e instanceof KeyEvent) {
     343                KeyEvent ke = (KeyEvent) e;
     344                if (dualAlignShortcut.isEvent(ke) || (useRepeatedShortcut && getShortcut().isEvent(ke))) {
     345                    Component focused = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
     346                    if (SwingUtilities.getWindowAncestor(focused) instanceof JFrame) {
     347                        processKeyEvent(ke);
     348                    }
     349                }
     350            }
     351        }
     352    };
     353
     354    // events for crossplatform key holding processing
     355    // thanks to http://www.arco.in-berlin.de/keyevent.html
     356    private final TreeSet<Integer> set = new TreeSet<Integer>();
     357    private KeyEvent releaseEvent;
     358    private Timer timer;
     359    private void processKeyEvent(KeyEvent e) {
     360        if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
     361            return;
     362
     363        if (e.getID() == KeyEvent.KEY_PRESSED) {
     364            if (timer.isRunning()) {
     365                timer.stop();
     366            } else if (set.add((e.getKeyCode()))) {
     367                doKeyPressEvent(e);
     368            }
     369        } else if (e.getID() == KeyEvent.KEY_RELEASED) {
     370            if (timer.isRunning()) {
     371                timer.stop();
     372                if (set.remove(e.getKeyCode())) {
     373                    doKeyReleaseEvent(e);
     374                }
     375            } else {
     376                releaseEvent = e;
     377                timer.restart();
     378            }
     379        }
     380    }
     381
     382    private void doKeyPressEvent(KeyEvent e) {
     383    }
     384
     385    private void doKeyReleaseEvent(KeyEvent e) {
     386        toggleDualAlign();
     387    }
     388
     389    /**
     390     * Toggles dual alignment mode.
     391     */
     392    private void toggleDualAlign() {
     393        dualAlignEnabled = !dualAlignEnabled;
     394        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
     395        updateStatusLine();
     396    }
     397
     398    /**
     399     * If the left mouse button is pressed over a segment or a node, switches
     400     * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and
     401     * {@link #dualAlignEnabled}.
     402     * @param e
     403     */
     404    @Override
     405    public void mousePressed(MouseEvent e) {
    239406        if(!Main.map.mapView.isActiveLayerVisible())
    240407            return;
    241408        if (!(Boolean)this.getValue("active"))
     
    262429                    return;
    263430                }
    264431                mode = Mode.translate_node;
     432                dualAlignActive = false;
    265433            }
    266434        } else {
    267435            // Otherwise switch to another mode
     436            if (dualAlignEnabled && checkDualAlignConditions()) {
     437                dualAlignActive = true;
     438                calculatePossibleDirectionsForDualAlign();
     439            } else {
     440                dualAlignActive = false;
     441                calculatePossibleDirectionsBySegment();
     442            }
    268443            if (ctrl) {
    269444                mode = Mode.translate;
    270445                movingNodeList = new ArrayList<OsmPrimitive>();
     
    280455                getCurrentDataSet().setSelected(selectedSegment.way);
    281456                alwaysCreateNodes = shift;
    282457            }
    283             calculatePossibleDirectionsBySegment();
    284458        }
    285459
    286460        // Signifies that nothing has happened yet
     
    287461        newN1en = null;
    288462        newN2en = null;
    289463        moveCommand = null;
     464        moveCommand2 = null;
    290465
    291466        Main.map.mapView.addTemporaryLayer(this);
    292467
     
    301476   }
    302477
    303478    /**
    304      * Perform action depending on what mode we're in.
     479     * Performs action depending on what {@link #mode} we're in.
     480     * @param e
    305481     */
    306     @Override public void mouseDragged(MouseEvent e) {
     482    @Override
     483    public void mouseDragged(MouseEvent e) {
    307484        if(!Main.map.mapView.isActiveLayerVisible())
    308485            return;
    309486
     
    318495
    319496            EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
    320497            EastNorth bestMovement = calculateBestMovement(mouseEn);
     498            EastNorth n1movedEn = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
    321499
    322             newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
    323             newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
    324 
    325500            // find out the movement distance, in metres
    326             double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
     501            double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(n1movedEn));
    327502            Main.map.statusLine.setDist(distance);
    328503            updateStatusLine();
    329504
    330505            Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
    331506
    332             if (mode == Mode.extrude || mode == Mode.create_new) {
    333                 //nothing here
    334             } else if (mode == Mode.translate_node || mode == Mode.translate) {
    335                 //move nodes to new position
    336                 if (moveCommand == null) {
    337                     //make a new move command
    338                     moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
    339                     Main.main.undoRedo.add(moveCommand);
    340                 } else {
    341                     //reuse existing move command
    342                     moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
     507            if (dualAlignActive) {
     508                calculateDualAlignNodesPositions(bestMovement);
     509
     510                if (mode == Mode.extrude || mode == Mode.create_new) {
     511                    // nothing here
     512                } else if (mode == Mode.translate) {
     513                    EastNorth movement1 = initialN1en.sub(newN1en);
     514                    EastNorth movement2 = initialN2en.sub(newN2en);
     515                    // move nodes to new position
     516                    if (moveCommand == null || moveCommand2 == null) {
     517                        // make a new move commands
     518                        moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
     519                        moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
     520                        Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
     521                        Main.main.undoRedo.add(c);
     522                    } else {
     523                        // reuse existing move commands
     524                        moveCommand.moveAgainTo(movement1.getX(), movement1.getY());
     525                        moveCommand2.moveAgainTo(movement2.getX(), movement2.getY());
     526                    }
    343527                }
     528            } else {
     529                newN1en = n1movedEn;
     530                newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
     531
     532                if (mode == Mode.extrude || mode == Mode.create_new) {
     533                    //nothing here
     534                } else if (mode == Mode.translate_node || mode == Mode.translate) {
     535                    //move nodes to new position
     536                    if (moveCommand == null) {
     537                        //make a new move command
     538                        moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY());
     539                        Main.main.undoRedo.add(moveCommand);
     540                    } else {
     541                        //reuse existing move command
     542                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
     543                    }
     544                }
    344545            }
    345546
    346547            Main.map.mapView.repaint();
     
    348549    }
    349550
    350551    /**
    351      * Do anything that needs to be done, then switch back to select mode
     552     * Does anything that needs to be done, then switches back to select mode.
     553     * @param e
    352554     */
    353     @Override public void mouseReleased(MouseEvent e) {
     555    @Override
     556    public void mouseReleased(MouseEvent e) {
    354557
    355558        if(!Main.map.mapView.isActiveLayerVisible())
    356559            return;
     
    376579                //the move command is already committed in mouseDragged
    377580            }
    378581
    379             boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
    380             boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
    381             boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
     582            updateKeyModifiers(e);
    382583            // Switch back into select mode
    383584            Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
    384585            Main.map.mapView.removeTemporaryLayer(this);
     
    391592        }
    392593    }
    393594
     595    // -------------------------------------------------------------------------
     596    // Custom methods
     597    // -------------------------------------------------------------------------
     598
    394599    /**
    395      * Insert node into nearby segment
    396      * @param e - current mouse point
     600     * Inserts node into nearby segment.
     601     * @param e current mouse point
    397602     */
    398603    private void addNewNode(MouseEvent e) {
    399604        // Should maybe do the same as in DrawAction and fetch all nearby segments?
     
    411616        }
    412617    }
    413618
     619    /**
     620     * Creates a new way that shares segment with selected way.
     621     */
    414622    private void createNewRectangle() {
    415623        if (selectedSegment == null) return;
    416624        // crete a new rectangle
     
    434642    }
    435643
    436644    /**
    437      * Do actual extrusion of @field selectedSegment
     645     * Does actual extrusion of {@link #selectedSegment}.
    438646     */
    439647    private void performExtrusion() {
    440648        // create extrusion
     
    449657        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
    450658        // segmentAngleZero marks subset of nodeOverlapsSegment. nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0
    451659        boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5;
    452         boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
     660        boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
    453661
    454662        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
    455663            //move existing node
     
    513721    }
    514722
    515723    /**
    516      * This method tests if a node has other ways apart from the given one.
     724     * This method tests if {@code node} has other ways apart from the given one.
    517725     * @param node
    518726     * @param myWay
    519      * @return true of node belongs only to myWay, false if there are more ways.
     727     * @return {@code true} if {@code node} belongs only to {@code myWay}, false if there are more ways.
    520728     */
    521     private boolean hasNodeOtherWays(Node node, Way myWay) {
     729    private static boolean hasNodeOtherWays(Node node, Way myWay) {
    522730        for (OsmPrimitive p : node.getReferrers()) {
    523731            if (p instanceof Way && p.isUsable() && p != myWay)
    524732                return true;
     
    527735    }
    528736
    529737    /**
    530      * Determine best movenemnt from initialMousePos  to current position @param mouseEn,
    531      * choosing one of the directions @field possibleMoveDirections
     738     * Determines best movement from {@link #initialMousePos} to current mouse position,
     739     * choosing one of the directions from {@link #possibleMoveDirections}.
     740     * @param mouseEn current mouse position
    532741     * @return movement vector
    533742     */
    534743    private EastNorth calculateBestMovement(EastNorth mouseEn) {
     
    556765            }
    557766        }
    558767        return bestMovement;
     768
     769
    559770    }
    560771
    561772    /***
    562      * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
    563      * @param segmentP1
    564      * @param segmentP2
    565      * @param targetPos
     773     * This method calculates offset amount by which to move the given segment
     774     * perpendicularly for it to be in line with mouse position.
     775     * @param segmentP1 segment's first point
     776     * @param segmentP2 segment's second point
     777     * @param moveDirection direction of movement
     778     * @param targetPos mouse position
    566779     * @return offset amount of P1 and P2.
    567780     */
    568781    private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
     
    582795    }
    583796
    584797    /**
    585      * Gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
     798     * Gathers possible move directions - perpendicular to the selected segment
     799     * and parallel to neighboring segments.
    586800     */
    587801    private void calculatePossibleDirectionsBySegment() {
    588802        // remember initial positions for segment nodes.
     
    618832    }
    619833
    620834    /**
    621      * Gather possible move directions - along all adjacent segments
     835     * Gathers possible move directions - along all adjacent segments.
    622836     */
    623837    private void calculatePossibleDirectionsByNode() {
    624838        // remember initial positions for segment nodes.
     
    639853    }
    640854
    641855    /**
    642      * Gets a node from selected way before given index.
     856     * Checks dual alignment conditions:
     857     *  1. selected segment has both neighboring segments,
     858     *  2. selected segment is not parallel with neighboring segments.
     859     * @return {@code true} if dual alignment conditions are satisfied
     860     */
     861    private boolean checkDualAlignConditions() {
     862        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
     863        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
     864        if (prevNode == null || nextNode == null) {
     865            return false;
     866        }
     867
     868        EastNorth n1en = selectedSegment.getFirstNode().getEastNorth();
     869        EastNorth n2en = selectedSegment.getSecondNode().getEastNorth();
     870        boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en);
     871        boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en);
     872        if (prevSegmentParallel || nextSegmentParallel) {
     873            return false;
     874        }
     875
     876        return true;
     877    }
     878
     879    /**
     880     * Gathers possible move directions - perpendicular to the selected segment only.
     881     * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}.
     882     */
     883    private void calculatePossibleDirectionsForDualAlign() {
     884        // remember initial positions for segment nodes.
     885        initialN1en = selectedSegment.getFirstNode().getEastNorth();
     886        initialN2en = selectedSegment.getSecondNode().getEastNorth();
     887
     888        // add direction perpendicular to the selected segment
     889        possibleMoveDirections = new ArrayList<ReferenceSegment>();
     890        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
     891                initialN1en.getY() - initialN2en.getY(),
     892                initialN2en.getX() - initialN1en.getX()
     893                ), initialN1en, initialN2en, true));
     894
     895        // set neighboring segments
     896        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
     897        EastNorth prevNodeEn = prevNode.getEastNorth();
     898        dualAlignSegment1 = new ReferenceSegment(new EastNorth(
     899            initialN1en.getX() - prevNodeEn.getX(),
     900            initialN1en.getY() - prevNodeEn.getY()
     901            ), initialN1en, prevNodeEn, false);
     902       
     903        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
     904        EastNorth nextNodeEn = nextNode.getEastNorth();
     905        dualAlignSegment2 = new ReferenceSegment(new EastNorth(
     906            initialN2en.getX() - nextNodeEn.getX(),
     907            initialN2en.getY() - nextNodeEn.getY()
     908            ), initialN2en,  nextNodeEn, false);
     909    }
     910
     911    /**
     912     * Calculates positions of new nodes, aligning them to neighboring segments.
     913     * @param movement movement to be used
     914     */
     915    private void calculateDualAlignNodesPositions(EastNorth movement) {
     916        // new positions of selected segment's nodes, without applying dual alignment
     917        EastNorth n1movedEn = new EastNorth(initialN1en.getX() + movement.getX(), initialN1en.getY() + movement.getY());
     918        EastNorth n2movedEn = new EastNorth(initialN2en.getX() + movement.getX(), initialN2en.getY() + movement.getY());
     919
     920        // calculate intersections
     921        newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2);
     922        newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2);
     923    }
     924
     925    /**
     926     * Gets a node index from selected way before given index.
    643927     * @param index  index of current node
    644      * @return index of previous node or -1 if there are no nodes there.
     928     * @return index of previous node or <code>-1</code> if there are no nodes there.
    645929     */
    646930    private int getPreviousNodeIndex(int index) {
    647931        if (index > 0)
     
    655939    /**
    656940     * Gets a node from selected way before given index.
    657941     * @param index  index of current node
    658      * @return previous node or null if there are no nodes there.
     942     * @return previous node or <code>null</code> if there are no nodes there.
    659943     */
    660944    private Node getPreviousNode(int index) {
    661945        int indexPrev = getPreviousNodeIndex(index);
     
    667951
    668952
    669953    /**
    670      * Gets a node from selected way after given index.
     954     * Gets a node index from selected way after given index.
    671955     * @param index index of current node
    672      * @return index of next node or -1 if there are no nodes there.
     956     * @return index of next node or <code>-1</code> if there are no nodes there.
    673957     */
    674958    private int getNextNodeIndex(int index) {
    675959        int count = selectedSegment.way.getNodesCount();
     
    684968    /**
    685969     * Gets a node from selected way after given index.
    686970     * @param index index of current node
    687      * @return next node or null if there are no nodes there.
     971     * @return next node or <code>null</code> if there are no nodes there.
    688972     */
    689973    private Node getNextNode(int index) {
    690974        int indexNext = getNextNodeIndex(index);
     
    694978            return null;
    695979    }
    696980
     981    // -------------------------------------------------------------------------
     982    // paint methods
     983    // -------------------------------------------------------------------------
     984
    697985    @Override
    698986    public void paint(Graphics2D g, MapView mv, Bounds box) {
    699987        Graphics2D g2 = g;
     
    7191007                    b.lineTo(p1.x, p1.y);
    7201008                    g2.draw(b);
    7211009
    722                     if (activeMoveDirection != null) {
     1010                    if (dualAlignActive) {
     1011                        // Draw reference ways
     1012                        drawReferenceSegment(g2, mv, dualAlignSegment1.p1, dualAlignSegment1.p2);
     1013                        drawReferenceSegment(g2, mv, dualAlignSegment2.p1, dualAlignSegment2.p2);
     1014                    } else if (activeMoveDirection != null) {
    7231015                        // Draw reference way
    724                         Point pr1 = mv.getPoint(activeMoveDirection.p1);
    725                         Point pr2 = mv.getPoint(activeMoveDirection.p2);
    726                         b = new GeneralPath();
    727                         b.moveTo(pr1.x, pr1.y);
    728                         b.lineTo(pr2.x, pr2.y);
    729                         g2.setColor(helperColor);
    730                         g2.setStroke(helperStrokeDash);
    731                         g2.draw(b);
     1016                        drawReferenceSegment(g2, mv, activeMoveDirection.p1, activeMoveDirection.p2);
    7321017
    7331018                        // Draw right angle marker on first node position, only when moving at right angle
    7341019                        if (activeMoveDirection.perpendicular) {
     
    7381023                            double headingDiff = headingRefWS - headingMoveDir;
    7391024                            if (headingDiff < 0) headingDiff += 2 * Math.PI;
    7401025                            boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
     1026                            Point pr1 = mv.getPoint(activeMoveDirection.p1);
    7411027                            drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
    7421028                        }
    7431029                    }
     
    7531039                        g2.draw(oldline);
    7541040                    }
    7551041
    756                     if (activeMoveDirection != null) {
     1042                    if (dualAlignActive) {
     1043                        // Draw reference ways
     1044                        drawReferenceSegment(g2, mv, dualAlignSegment1.p1, dualAlignSegment1.p2);
     1045                        drawReferenceSegment(g2, mv, dualAlignSegment2.p1, dualAlignSegment2.p2);
     1046                    } else if (activeMoveDirection != null) {
    7571047
    7581048                        g2.setColor(helperColor);
    7591049                        g2.setStroke(helperStrokeDash);
     
    7941084        return normalUnitVector;
    7951085    }
    7961086
     1087    /**
     1088     * Draws right angle symbol at specified position.
     1089     * @param g2 the Graphics2D object used to draw on
     1090     * @param center center point of angle
     1091     * @param normal vector of normal
     1092     * @param mirror {@code true} if symbol should be mirrored by the normal
     1093     */
    7971094    private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) {
    7981095        // EastNorth units per pixel
    7991096        double factor = 1.0/g2.getTransform().getScaleX();
     
    8151112    }
    8161113
    8171114    /**
    818      * Create a new Line that extends off the edge of the viewport in one direction
     1115     * Draws given reference segment.
     1116     * @param g2 the Graphics2D object used to draw on
     1117     * @param mv
     1118     * @param p1en segment's first point
     1119     * @param p2en segment's second point
     1120     */
     1121    private void drawReferenceSegment(Graphics2D g2, MapView mv, EastNorth p1en, EastNorth p2en)
     1122    {
     1123        Point p1 = mv.getPoint(p1en);
     1124        Point p2 = mv.getPoint(p2en);
     1125        GeneralPath b = new GeneralPath();
     1126        b.moveTo(p1.x, p1.y);
     1127        b.lineTo(p2.x, p2.y);
     1128        g2.setColor(helperColor);
     1129        g2.setStroke(helperStrokeDash);
     1130        g2.draw(b);
     1131    }
     1132
     1133    /**
     1134     * Creates a new Line that extends off the edge of the viewport in one direction
    8191135     * @param start The start point of the line
    8201136     * @param unitvector A unit vector denoting the direction of the line
    8211137     * @param g the Graphics2D object  it will be used on
     1138     * @return created line
    8221139     */
    8231140    static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
    8241141        Rectangle bounds = g.getDeviceConfiguration().getBounds();