001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.mapmode;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007
008import java.awt.BasicStroke;
009import java.awt.Color;
010import java.awt.Cursor;
011import java.awt.Graphics2D;
012import java.awt.Point;
013import java.awt.Rectangle;
014import java.awt.Stroke;
015import java.awt.event.ActionEvent;
016import java.awt.event.KeyEvent;
017import java.awt.event.MouseEvent;
018import java.awt.geom.AffineTransform;
019import java.awt.geom.GeneralPath;
020import java.awt.geom.Line2D;
021import java.awt.geom.NoninvertibleTransformException;
022import java.awt.geom.Point2D;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.LinkedList;
026import java.util.List;
027
028import javax.swing.JCheckBoxMenuItem;
029
030import org.openstreetmap.josm.actions.JosmAction;
031import org.openstreetmap.josm.actions.MergeNodesAction;
032import org.openstreetmap.josm.command.AddCommand;
033import org.openstreetmap.josm.command.ChangeNodesCommand;
034import org.openstreetmap.josm.command.Command;
035import org.openstreetmap.josm.command.MoveCommand;
036import org.openstreetmap.josm.command.SequenceCommand;
037import org.openstreetmap.josm.data.Bounds;
038import org.openstreetmap.josm.data.UndoRedoHandler;
039import org.openstreetmap.josm.data.coor.EastNorth;
040import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
041import org.openstreetmap.josm.data.osm.DataSet;
042import org.openstreetmap.josm.data.osm.Node;
043import org.openstreetmap.josm.data.osm.OsmPrimitive;
044import org.openstreetmap.josm.data.osm.Way;
045import org.openstreetmap.josm.data.osm.WaySegment;
046import org.openstreetmap.josm.data.preferences.NamedColorProperty;
047import org.openstreetmap.josm.data.projection.ProjectionRegistry;
048import org.openstreetmap.josm.gui.MainApplication;
049import org.openstreetmap.josm.gui.MainMenu;
050import org.openstreetmap.josm.gui.MapFrame;
051import org.openstreetmap.josm.gui.MapView;
052import org.openstreetmap.josm.gui.draw.MapViewPath;
053import org.openstreetmap.josm.gui.draw.SymbolShape;
054import org.openstreetmap.josm.gui.layer.Layer;
055import org.openstreetmap.josm.gui.layer.MapViewPaintable;
056import org.openstreetmap.josm.gui.util.GuiHelper;
057import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
058import org.openstreetmap.josm.gui.util.ModifierExListener;
059import org.openstreetmap.josm.spi.preferences.Config;
060import org.openstreetmap.josm.tools.Geometry;
061import org.openstreetmap.josm.tools.ImageProvider;
062import org.openstreetmap.josm.tools.Logging;
063import org.openstreetmap.josm.tools.Shortcut;
064
065/**
066 * Makes a rectangle from a line, or modifies a rectangle.
067 */
068public class ExtrudeAction extends MapMode implements MapViewPaintable, KeyPressReleaseListener, ModifierExListener {
069
070    enum Mode { extrude, translate, select, create_new, translate_node }
071
072    private Mode mode = Mode.select;
073
074    /**
075     * If {@code true}, when extruding create new node(s) even if segments are parallel.
076     */
077    private boolean alwaysCreateNodes;
078    private boolean nodeDragWithoutCtrl;
079
080    private long mouseDownTime;
081    private transient WaySegment selectedSegment;
082    private transient Node selectedNode;
083    private Color mainColor;
084    private transient Stroke mainStroke;
085
086    /** settings value whether shared nodes should be ignored or not */
087    private boolean ignoreSharedNodes;
088
089    private boolean keepSegmentDirection;
090
091    /**
092     * drawing settings for helper lines
093     */
094    private Color helperColor;
095    private transient Stroke helperStrokeDash;
096    private transient Stroke helperStrokeRA;
097
098    private transient Stroke oldLineStroke;
099    private double symbolSize;
100    /**
101     * Possible directions to move to.
102     */
103    private transient List<ReferenceSegment> possibleMoveDirections;
104
105
106    /**
107     * Collection of nodes that is moved
108     */
109    private transient List<Node> movingNodeList;
110
111    /**
112     * The direction that is currently active.
113     */
114    private transient ReferenceSegment activeMoveDirection;
115
116    /**
117     * The position of the mouse cursor when the drag action was initiated.
118     */
119    private Point initialMousePos;
120    /**
121     * The time which needs to pass between click and release before something
122     * counts as a move, in milliseconds
123     */
124    private int initialMoveDelay = 200;
125    /**
126     * The minimal shift of mouse (in pixels) before something counts as move
127     */
128    private int initialMoveThreshold = 1;
129
130    /**
131     * The initial EastNorths of node1 and node2
132     */
133    private EastNorth initialN1en;
134    private EastNorth initialN2en;
135    /**
136     * The new EastNorths of node1 and node2
137     */
138    private EastNorth newN1en;
139    private EastNorth newN2en;
140
141    /**
142     * the command that performed last move.
143     */
144    private transient 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 transient MoveCommand moveCommand2;
150
151    /** The cursor for the 'create_new' mode. */
152    private final Cursor cursorCreateNew;
153
154    /** The cursor for the 'translate' mode. */
155    private final Cursor cursorTranslate;
156
157    /** The cursor for the 'alwaysCreateNodes' submode. */
158    private final Cursor cursorCreateNodes;
159
160    private static class ReferenceSegment {
161        public final EastNorth en;
162        public final EastNorth p1;
163        public final EastNorth p2;
164        public final boolean perpendicular;
165
166        ReferenceSegment(EastNorth en, EastNorth p1, EastNorth p2, boolean perpendicular) {
167            this.en = en;
168            this.p1 = p1;
169            this.p2 = p2;
170            this.perpendicular = perpendicular;
171        }
172
173        @Override
174        public String toString() {
175            return "ReferenceSegment[en=" + en + ", p1=" + p1 + ", p2=" + p2 + ", perp=" + perpendicular + ']';
176        }
177    }
178
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.
183     * Treat {@link #mode} (extrude/translate/create_new) as dual aligned. */
184    private boolean dualAlignActive;
185    /** Dual alignment reference segments */
186    private transient ReferenceSegment dualAlignSegment1, dualAlignSegment2;
187    /** {@code true}, if new segment was collapsed */
188    private boolean dualAlignSegmentCollapsed;
189    // Dual alignment UI stuff
190    private final DualAlignChangeAction dualAlignChangeAction;
191    private final JCheckBoxMenuItem dualAlignCheckboxMenuItem;
192    private final transient Shortcut dualAlignShortcut;
193    private boolean useRepeatedShortcut;
194    private boolean ignoreNextKeyRelease;
195
196    private class DualAlignChangeAction extends JosmAction {
197        DualAlignChangeAction() {
198            super(tr("Dual alignment"), /* ICON() */ "mapmode/extrude/dualalign",
199                    tr("Switch dual alignment mode while extruding"), null, false);
200            setHelpId(ht("/Action/Extrude#DualAlign"));
201        }
202
203        @Override
204        public void actionPerformed(ActionEvent e) {
205            toggleDualAlign();
206        }
207
208        @Override
209        protected void updateEnabledState() {
210            MapFrame map = MainApplication.getMap();
211            setEnabled(map != null && map.mapMode instanceof ExtrudeAction);
212        }
213    }
214
215    /**
216     * Creates a new ExtrudeAction
217     * @since 11713
218     */
219    public ExtrudeAction() {
220        super(tr("Extrude"), /* ICON(mapmode/) */ "extrude/extrude", tr("Create areas"),
221                Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT),
222                ImageProvider.getCursor("normal", "rectangle"));
223        setHelpId(ht("/Action/Extrude"));
224        cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
225        cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
226        cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
227
228        dualAlignEnabled = false;
229        dualAlignChangeAction = new DualAlignChangeAction();
230        dualAlignCheckboxMenuItem = addDualAlignMenuItem();
231        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
232        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
233        dualAlignShortcut = Shortcut.registerShortcut("mapmode:extrudedualalign",
234                tr("Edit: {0}", tr("Extrude Dual alignment")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
235        readPreferences(); // to show prefernces in table before entering the mode
236    }
237
238    @Override
239    public void destroy() {
240        super.destroy();
241        MainApplication.getMenu().editMenu.remove(dualAlignCheckboxMenuItem);
242        dualAlignChangeAction.destroy();
243    }
244
245    private JCheckBoxMenuItem addDualAlignMenuItem() {
246        int n = MainApplication.getMenu().editMenu.getItemCount();
247        return MainMenu.addWithCheckbox(MainApplication.getMenu().editMenu, dualAlignChangeAction, n >= 5 ? n-5 : -1, false);
248    }
249
250    // -------------------------------------------------------------------------
251    // Mode methods
252    // -------------------------------------------------------------------------
253
254    @Override
255    public String getModeHelpText() {
256        StringBuilder rv;
257        if (mode == Mode.select) {
258            rv = new StringBuilder(tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
259                "Alt-drag to create a new rectangle, double click to add a new node."));
260            if (dualAlignEnabled) {
261                rv.append(' ').append(tr("Dual alignment active."));
262                if (dualAlignSegmentCollapsed)
263                    rv.append(' ').append(tr("Segment collapsed due to its direction reversing."));
264            }
265        } else {
266            if (mode == Mode.translate)
267                rv = new StringBuilder(tr("Move a segment along its normal, then release the mouse button."));
268            else if (mode == Mode.translate_node)
269                rv = new StringBuilder(tr("Move the node along one of the segments, then release the mouse button."));
270            else if (mode == Mode.extrude || mode == Mode.create_new)
271                rv = new StringBuilder(tr("Draw a rectangle of the desired size, then release the mouse button."));
272            else {
273                Logging.warn("Extrude: unknown mode " + mode);
274                rv = new StringBuilder();
275            }
276            if (dualAlignActive) {
277                rv.append(' ').append(tr("Dual alignment active."));
278                if (dualAlignSegmentCollapsed) {
279                    rv.append(' ').append(tr("Segment collapsed due to its direction reversing."));
280                }
281            }
282        }
283        return rv.toString();
284    }
285
286    @Override
287    public boolean layerIsSupported(Layer l) {
288        return isEditableDataLayer(l);
289    }
290
291    @Override
292    public void enterMode() {
293        super.enterMode();
294        MapFrame map = MainApplication.getMap();
295        map.mapView.addMouseListener(this);
296        map.mapView.addMouseMotionListener(this);
297        map.statusLine.setAutoLength(false);
298        ignoreNextKeyRelease = true;
299        map.keyDetector.addKeyListener(this);
300        map.keyDetector.addModifierExListener(this);
301    }
302
303    @Override
304    protected void readPreferences() {
305        initialMoveDelay = Config.getPref().getInt("edit.initial-move-delay", 200);
306        initialMoveThreshold = Config.getPref().getInt("extrude.initial-move-threshold", 1);
307        mainColor = new NamedColorProperty(marktr("Extrude: main line"), Color.RED).get();
308        helperColor = new NamedColorProperty(marktr("Extrude: helper line"), Color.ORANGE).get();
309        helperStrokeDash = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.helper-line", "1 4"));
310        helperStrokeRA = new BasicStroke(1);
311        symbolSize = Config.getPref().getDouble("extrude.angle-symbol-radius", 8);
312        nodeDragWithoutCtrl = Config.getPref().getBoolean("extrude.drag-nodes-without-ctrl", false);
313        oldLineStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.ctrl.stroke.old-line", "1"));
314        mainStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.main", "3"));
315
316        ignoreSharedNodes = Config.getPref().getBoolean("extrude.ignore-shared-nodes", true);
317        dualAlignCheckboxMenuItem.getAction().setEnabled(true);
318        useRepeatedShortcut = Config.getPref().getBoolean("extrude.dualalign.toggleOnRepeatedX", true);
319        keepSegmentDirection = Config.getPref().getBoolean("extrude.dualalign.keep-segment-direction", true);
320    }
321
322    @Override
323    public void exitMode() {
324        MapFrame map = MainApplication.getMap();
325        map.mapView.removeMouseListener(this);
326        map.mapView.removeMouseMotionListener(this);
327        map.mapView.removeTemporaryLayer(this);
328        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
329        map.keyDetector.removeKeyListener(this);
330        map.keyDetector.removeModifierExListener(this);
331        this.selectedNode = null;
332        this.selectedSegment = null;
333        super.exitMode();
334    }
335
336    // -------------------------------------------------------------------------
337    // Event handlers
338    // -------------------------------------------------------------------------
339
340    /**
341     * This method is called to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed,
342     */
343    @Override
344    public void modifiersExChanged(int modifiers) {
345        MapFrame map = MainApplication.getMap();
346        if (!MainApplication.isDisplayingMapView() || !map.mapView.isActiveLayerDrawable())
347            return;
348        updateKeyModifiersEx(modifiers);
349        if (mode == Mode.select) {
350            map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
351        }
352    }
353
354    @Override
355    public void doKeyPressed(KeyEvent e) {
356        // Do nothing
357    }
358
359    @Override
360    public void doKeyReleased(KeyEvent e) {
361        if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
362             return;
363        if (ignoreNextKeyRelease) {
364            ignoreNextKeyRelease = false;
365        } else {
366            toggleDualAlign();
367        }
368    }
369
370    /**
371     * Toggles dual alignment mode.
372     */
373    private void toggleDualAlign() {
374        dualAlignEnabled = !dualAlignEnabled;
375        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
376        updateStatusLine();
377    }
378
379    /**
380     * If the left mouse button is pressed over a segment or a node, switches
381     * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and
382     * {@link #dualAlignEnabled}.
383     * @param e current mouse event
384     */
385    @Override
386    public void mousePressed(MouseEvent e) {
387        MapFrame map = MainApplication.getMap();
388        if (!map.mapView.isActiveLayerVisible())
389            return;
390        if (!(Boolean) this.getValue("active"))
391            return;
392        if (e.getButton() != MouseEvent.BUTTON1)
393            return;
394
395        requestFocusInMapView();
396        updateKeyModifiers(e);
397
398        selectedNode = map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
399        selectedSegment = map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
400
401        // If nothing gets caught, stay in select mode
402        if (selectedSegment == null && selectedNode == null) return;
403
404        if (selectedNode != null) {
405            if (ctrl || nodeDragWithoutCtrl) {
406                movingNodeList = new ArrayList<>();
407                movingNodeList.add(selectedNode);
408                calculatePossibleDirectionsByNode();
409                if (possibleMoveDirections.isEmpty()) {
410                    // if no directions fould, do not enter dragging mode
411                    return;
412                }
413                mode = Mode.translate_node;
414                dualAlignActive = false;
415            }
416        } else {
417            // Otherwise switch to another mode
418            if (dualAlignEnabled && checkDualAlignConditions()) {
419                dualAlignActive = true;
420                calculatePossibleDirectionsForDualAlign();
421                dualAlignSegmentCollapsed = false;
422            } else {
423                dualAlignActive = false;
424                calculatePossibleDirectionsBySegment();
425            }
426            if (ctrl) {
427                mode = Mode.translate;
428                movingNodeList = new ArrayList<>();
429                movingNodeList.add(selectedSegment.getFirstNode());
430                movingNodeList.add(selectedSegment.getSecondNode());
431            } else if (alt) {
432                mode = Mode.create_new;
433                // create a new segment and then select and extrude the new segment
434                getLayerManager().getEditDataSet().setSelected(selectedSegment.getWay());
435                alwaysCreateNodes = true;
436            } else {
437                mode = Mode.extrude;
438                getLayerManager().getEditDataSet().setSelected(selectedSegment.getWay());
439                alwaysCreateNodes = shift;
440            }
441        }
442
443        // Signifies that nothing has happened yet
444        newN1en = null;
445        newN2en = null;
446        moveCommand = null;
447        moveCommand2 = null;
448
449        map.mapView.addTemporaryLayer(this);
450
451        updateStatusLine();
452        map.mapView.repaint();
453
454        // Make note of time pressed
455        mouseDownTime = System.currentTimeMillis();
456
457        // Make note of mouse position
458        initialMousePos = e.getPoint();
459   }
460
461    /**
462     * Performs action depending on what {@link #mode} we're in.
463     * @param e current mouse event
464     */
465    @Override
466    public void mouseDragged(MouseEvent e) {
467        MapView mapView = MainApplication.getMap().mapView;
468        if (!mapView.isActiveLayerVisible())
469            return;
470
471        // do not count anything as a drag if it lasts less than 100 milliseconds.
472        if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
473            return;
474
475        if (mode == Mode.select) {
476            // Just sit tight and wait for mouse to be released.
477        } else {
478            //move, create new and extrude mode - move the selected segment
479
480            EastNorth mouseEn = mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
481            EastNorth bestMovement = calculateBestMovementAndNewNodes(mouseEn);
482
483            mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
484
485            if (dualAlignActive) {
486                if (mode == Mode.extrude || mode == Mode.create_new) {
487                    // nothing here
488                } else if (mode == Mode.translate) {
489                    EastNorth movement1 = newN1en.subtract(initialN1en);
490                    EastNorth movement2 = newN2en.subtract(initialN2en);
491                    // move nodes to new position
492                    if (moveCommand == null || moveCommand2 == null) {
493                        // make a new move commands
494                        moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
495                        moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
496                        Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
497                        UndoRedoHandler.getInstance().add(c);
498                    } else {
499                        // reuse existing move commands
500                        moveCommand.moveAgainTo(movement1.getX(), movement1.getY());
501                        moveCommand2.moveAgainTo(movement2.getX(), movement2.getY());
502                    }
503                }
504            } else if (bestMovement != null) {
505                if (mode == Mode.extrude || mode == Mode.create_new) {
506                    //nothing here
507                } else if (mode == Mode.translate_node || mode == Mode.translate) {
508                    //move nodes to new position
509                    if (moveCommand == null) {
510                        //make a new move command
511                        moveCommand = new MoveCommand(new ArrayList<OsmPrimitive>(movingNodeList), bestMovement);
512                        UndoRedoHandler.getInstance().add(moveCommand);
513                    } else {
514                        //reuse existing move command
515                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
516                    }
517                }
518            }
519
520            mapView.repaint();
521        }
522    }
523
524    /**
525     * Does anything that needs to be done, then switches back to select mode.
526     * @param e current mouse event
527     */
528    @Override
529    public void mouseReleased(MouseEvent e) {
530
531        MapView mapView = MainApplication.getMap().mapView;
532        if (!mapView.isActiveLayerVisible())
533            return;
534
535        if (mode == Mode.select) {
536            // Nothing to be done
537        } else {
538            if (mode == Mode.create_new) {
539                if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null) {
540                    createNewRectangle();
541                }
542            } else if (mode == Mode.extrude) {
543                if (e.getClickCount() == 2 && e.getPoint().equals(initialMousePos)) {
544                    // double click adds a new node
545                    addNewNode(e);
546                } else if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null && selectedSegment != null) {
547                    try {
548                        // main extrusion commands
549                        performExtrusion();
550                    } catch (DataIntegrityProblemException ex) {
551                        // Can occur if calling undo while extruding, see #12870
552                        Logging.error(ex);
553                    }
554                }
555            } else if (mode == Mode.translate || mode == Mode.translate_node) {
556                //Commit translate
557                //the move command is already committed in mouseDragged
558                joinNodesIfCollapsed(movingNodeList);
559            }
560            MainApplication.getMap().statusLine.setDist(getLayerManager().getEditDataSet().getSelectedWays());
561            MainApplication.getMap().statusLine.repaint();
562
563            updateKeyModifiers(e);
564            // Switch back into select mode
565            mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
566            mapView.removeTemporaryLayer(this);
567            selectedSegment = null;
568            moveCommand = null;
569            mode = Mode.select;
570            dualAlignSegmentCollapsed = false;
571            updateStatusLine();
572            mapView.repaint();
573        }
574    }
575
576    // -------------------------------------------------------------------------
577    // Custom methods
578    // -------------------------------------------------------------------------
579
580    /**
581     * Inserts node into nearby segment.
582     * @param e current mouse point
583     */
584    private static void addNewNode(MouseEvent e) {
585        // Should maybe do the same as in DrawAction and fetch all nearby segments?
586        MapView mapView = MainApplication.getMap().mapView;
587        WaySegment ws = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
588        if (ws != null) {
589            Node n = new Node(mapView.getLatLon(e.getX(), e.getY()));
590            EastNorth a = ws.getFirstNode().getEastNorth();
591            EastNorth b = ws.getSecondNode().getEastNorth();
592            n.setEastNorth(Geometry.closestPointToSegment(a, b, n.getEastNorth()));
593            Way wnew = new Way(ws.getWay());
594            wnew.addNode(ws.getLowerIndex() +1, n);
595            DataSet ds = ws.getWay().getDataSet();
596            UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Add a new node to an existing way"),
597                    new AddCommand(ds, n), new ChangeNodesCommand(ds, ws.getWay(), wnew.getNodes())));
598            wnew.setNodes(null); // see #19885
599
600        }
601    }
602
603    /**
604     * Creates a new way that shares segment with selected way.
605     */
606    private void createNewRectangle() {
607        if (selectedSegment == null) return;
608        DataSet ds = getLayerManager().getEditDataSet();
609        // create a new rectangle
610        Collection<Command> cmds = new LinkedList<>();
611        Node third = new Node(newN2en);
612        Node fourth = new Node(newN1en);
613        Way wnew = new Way();
614        wnew.addNode(selectedSegment.getFirstNode());
615        wnew.addNode(selectedSegment.getSecondNode());
616        wnew.addNode(third);
617        if (!dualAlignSegmentCollapsed) {
618            // rectangle can degrade to triangle for dual alignment after collapsing
619            wnew.addNode(fourth);
620        }
621        // ... and close the way
622        wnew.addNode(selectedSegment.getFirstNode());
623        // undo support
624        cmds.add(new AddCommand(ds, third));
625        if (!dualAlignSegmentCollapsed) {
626            cmds.add(new AddCommand(ds, fourth));
627        }
628        cmds.add(new AddCommand(ds, wnew));
629        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
630        UndoRedoHandler.getInstance().add(c);
631        ds.setSelected(wnew);
632    }
633
634    /**
635     * Does actual extrusion of {@link #selectedSegment}.
636     * Uses {@link #initialN1en}, {@link #initialN2en} saved in calculatePossibleDirections* call
637     * Uses {@link #newN1en}, {@link #newN2en} calculated by {@link #calculateBestMovementAndNewNodes}
638     */
639    private void performExtrusion() {
640        DataSet ds = getLayerManager().getEditDataSet();
641        // create extrusion
642        Collection<Command> cmds = new LinkedList<>();
643        Way wnew = new Way(selectedSegment.getWay());
644        boolean wayWasModified = false;
645        boolean wayWasSingleSegment = wnew.getNodesCount() == 2;
646        int insertionPoint = selectedSegment.getUpperIndex();
647
648        //find if the new points overlap existing segments (in case of 90 degree angles)
649        Node prevNode = getPreviousNode(selectedSegment.getLowerIndex());
650        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
651        // segmentAngleZero marks subset of nodeOverlapsSegment.
652        // nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0
653        boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5;
654        boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.getWay());
655        List<Node> changedNodes = new ArrayList<>();
656        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
657            //move existing node
658            Node n1Old = selectedSegment.getFirstNode();
659            cmds.add(new MoveCommand(n1Old, ProjectionRegistry.getProjection().eastNorth2latlon(newN1en)));
660            changedNodes.add(n1Old);
661        } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) {
662            // replace shared node with new one
663            Node n1Old = selectedSegment.getFirstNode();
664            Node n1New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN1en));
665            wnew.addNode(insertionPoint, n1New);
666            wnew.removeNode(n1Old);
667            wayWasModified = true;
668            cmds.add(new AddCommand(ds, n1New));
669            changedNodes.add(n1New);
670        } else {
671            //introduce new node
672            Node n1New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN1en));
673            wnew.addNode(insertionPoint, n1New);
674            wayWasModified = true;
675            insertionPoint++;
676            cmds.add(new AddCommand(ds, n1New));
677            changedNodes.add(n1New);
678        }
679
680        //find if the new points overlap existing segments (in case of 90 degree angles)
681        Node nextNode = getNextNode(selectedSegment.getUpperIndex());
682        nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
683        segmentAngleZero = nextNode != null && Math.abs(Geometry.getCornerAngle(nextNode.getEastNorth(), initialN2en, newN2en)) < 1e-5;
684        hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.getWay());
685
686        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
687            //move existing node
688            Node n2Old = selectedSegment.getSecondNode();
689            cmds.add(new MoveCommand(n2Old, ProjectionRegistry.getProjection().eastNorth2latlon(newN2en)));
690            changedNodes.add(n2Old);
691        } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) {
692            // replace shared node with new one
693            Node n2Old = selectedSegment.getSecondNode();
694            Node n2New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN2en));
695            wnew.addNode(insertionPoint, n2New);
696            wnew.removeNode(n2Old);
697            wayWasModified = true;
698            cmds.add(new AddCommand(ds, n2New));
699            changedNodes.add(n2New);
700        } else {
701            //introduce new node
702            Node n2New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN2en));
703            wnew.addNode(insertionPoint, n2New);
704            wayWasModified = true;
705            cmds.add(new AddCommand(ds, n2New));
706            changedNodes.add(n2New);
707        }
708
709        //the way was a single segment, close the way
710        if (wayWasSingleSegment) {
711            wnew.addNode(selectedSegment.getFirstNode());
712            wayWasModified = true;
713        }
714        if (wayWasModified) {
715            // we only need to change the way if its node list was really modified
716            cmds.add(new ChangeNodesCommand(selectedSegment.getWay(), wnew.getNodes()));
717        }
718        wnew.setNodes(null); // see #19885
719        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
720        UndoRedoHandler.getInstance().add(c);
721        joinNodesIfCollapsed(changedNodes);
722    }
723
724    private void joinNodesIfCollapsed(List<Node> changedNodes) {
725        if (!dualAlignActive || newN1en == null || newN2en == null) return;
726        if (newN1en.distance(newN2en) > 1e-6) return;
727        // If the dual alignment moved two nodes to the same point, merge them
728        Node targetNode = MergeNodesAction.selectTargetNode(changedNodes);
729        Node locNode = MergeNodesAction.selectTargetLocationNode(changedNodes);
730        Command mergeCmd = MergeNodesAction.mergeNodes(changedNodes, targetNode, locNode);
731        if (mergeCmd != null) {
732            UndoRedoHandler.getInstance().add(mergeCmd);
733        } else {
734            // undo extruding command itself
735            UndoRedoHandler.getInstance().undo();
736        }
737    }
738
739    /**
740     * This method tests if {@code node} has other ways apart from the given one.
741     * @param node node to test
742     * @param myWay way known to contain this node
743     * @return {@code true} if {@code node} belongs only to {@code myWay}, false if there are more ways.
744     */
745    private static boolean hasNodeOtherWays(Node node, Way myWay) {
746        return node.getReferrers().stream()
747                .anyMatch(p -> p instanceof Way && p.isUsable() && p != myWay);
748    }
749
750    /**
751     * Determines best movement from {@link #initialMousePos} to current mouse position,
752     * choosing one of the directions from {@link #possibleMoveDirections}.
753     * @param mouseEn current mouse position
754     * @return movement vector
755     */
756    private EastNorth calculateBestMovement(EastNorth mouseEn) {
757
758        EastNorth initialMouseEn = MainApplication.getMap().mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
759        EastNorth mouseMovement = mouseEn.subtract(initialMouseEn);
760
761        double bestDistance = Double.POSITIVE_INFINITY;
762        EastNorth bestMovement = null;
763        activeMoveDirection = null;
764
765        //find the best movement direction and vector
766        for (ReferenceSegment direction : possibleMoveDirections) {
767            EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn);
768            if (movement == null) {
769                //if direction parallel to segment.
770                continue;
771            }
772
773            double distanceFromMouseMovement = movement.distance(mouseMovement);
774            if (bestDistance > distanceFromMouseMovement) {
775                bestDistance = distanceFromMouseMovement;
776                activeMoveDirection = direction;
777                bestMovement = movement;
778            }
779        }
780        return bestMovement;
781    }
782
783    /***
784     * This method calculates offset amount by which to move the given segment
785     * perpendicularly for it to be in line with mouse position.
786     * @param segmentP1 segment's first point
787     * @param segmentP2 segment's second point
788     * @param moveDirection direction of movement
789     * @param targetPos mouse position
790     * @return offset amount of P1 and P2.
791     */
792    private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
793            EastNorth targetPos) {
794        EastNorth intersectionPoint;
795        if (segmentP1.distanceSq(segmentP2) > 1e-7) {
796            intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection));
797        } else {
798            intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1);
799        }
800
801        if (intersectionPoint == null)
802            return null;
803        else
804            //return distance form base to target position
805            return targetPos.subtract(intersectionPoint);
806    }
807
808    /**
809     * Gathers possible move directions - perpendicular to the selected segment
810     * and parallel to neighboring segments.
811     */
812    private void calculatePossibleDirectionsBySegment() {
813        // remember initial positions for segment nodes.
814        initialN1en = selectedSegment.getFirstNode().getEastNorth();
815        initialN2en = selectedSegment.getSecondNode().getEastNorth();
816
817        //add direction perpendicular to the selected segment
818        possibleMoveDirections = new ArrayList<>();
819        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
820                initialN1en.getY() - initialN2en.getY(),
821                initialN2en.getX() - initialN1en.getX()
822                ), initialN1en, initialN2en, true));
823
824
825        //add directions parallel to neighbor segments
826        Node prevNode = getPreviousNode(selectedSegment.getLowerIndex());
827        if (prevNode != null) {
828            EastNorth en = prevNode.getEastNorth();
829            possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
830                    initialN1en.getX() - en.getX(),
831                    initialN1en.getY() - en.getY()
832                    ), initialN1en, en, false));
833        }
834
835        Node nextNode = getNextNode(selectedSegment.getUpperIndex());
836        if (nextNode != null) {
837            EastNorth en = nextNode.getEastNorth();
838            possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
839                    initialN2en.getX() - en.getX(),
840                    initialN2en.getY() - en.getY()
841                    ), initialN2en, en, false));
842        }
843    }
844
845    /**
846     * Gathers possible move directions - along all adjacent segments.
847     */
848    private void calculatePossibleDirectionsByNode() {
849        // remember initial positions for segment nodes.
850        initialN1en = selectedNode.getEastNorth();
851        initialN2en = initialN1en;
852        possibleMoveDirections = new ArrayList<>();
853        for (OsmPrimitive p: selectedNode.getReferrers()) {
854            if (p instanceof Way && p.isUsable()) {
855                for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) {
856                    EastNorth en = neighbor.getEastNorth();
857                    possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
858                        initialN1en.getX() - en.getX(),
859                        initialN1en.getY() - en.getY()
860                    ), initialN1en, en, false));
861                }
862            }
863        }
864    }
865
866    /**
867     * Checks dual alignment conditions:
868     *  1. selected segment has both neighboring segments,
869     *  2. selected segment is not parallel with neighboring segments.
870     * @return {@code true} if dual alignment conditions are satisfied
871     */
872    private boolean checkDualAlignConditions() {
873        Node prevNode = getPreviousNode(selectedSegment.getLowerIndex());
874        Node nextNode = getNextNode(selectedSegment.getUpperIndex());
875        if (prevNode == null || nextNode == null) {
876            return false;
877        }
878
879        EastNorth n1en = selectedSegment.getFirstNode().getEastNorth();
880        EastNorth n2en = selectedSegment.getSecondNode().getEastNorth();
881        if (n1en.distance(prevNode.getEastNorth()) < 1e-4 ||
882            n2en.distance(nextNode.getEastNorth()) < 1e-4) {
883            return false;
884        }
885
886        boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en);
887        boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en);
888        return !prevSegmentParallel && !nextSegmentParallel;
889    }
890
891    /**
892     * Gathers possible move directions - perpendicular to the selected segment only.
893     * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}.
894     */
895    private void calculatePossibleDirectionsForDualAlign() {
896        // remember initial positions for segment nodes.
897        initialN1en = selectedSegment.getFirstNode().getEastNorth();
898        initialN2en = selectedSegment.getSecondNode().getEastNorth();
899
900        // add direction perpendicular to the selected segment
901        possibleMoveDirections = new ArrayList<>();
902        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
903                initialN1en.getY() - initialN2en.getY(),
904                initialN2en.getX() - initialN1en.getX()
905                ), initialN1en, initialN2en, true));
906
907        // set neighboring segments
908        Node prevNode = getPreviousNode(selectedSegment.getLowerIndex());
909        if (prevNode != null) {
910            EastNorth prevNodeEn = prevNode.getEastNorth();
911            dualAlignSegment1 = new ReferenceSegment(new EastNorth(
912                initialN1en.getX() - prevNodeEn.getX(),
913                initialN1en.getY() - prevNodeEn.getY()
914                ), initialN1en, prevNodeEn, false);
915        }
916
917        Node nextNode = getNextNode(selectedSegment.getUpperIndex());
918        if (nextNode != null) {
919            EastNorth nextNodeEn = nextNode.getEastNorth();
920            dualAlignSegment2 = new ReferenceSegment(new EastNorth(
921                initialN2en.getX() - nextNodeEn.getX(),
922                initialN2en.getY() - nextNodeEn.getY()
923                ), initialN2en, nextNodeEn, false);
924        }
925    }
926
927    /**
928     * Calculate newN1en, newN2en best suitable for given mouse coordinates
929     * For dual align, calculates positions of new nodes, aligning them to neighboring segments.
930     * Elsewhere, just adds the vetor returned by calculateBestMovement to {@link #initialN1en},  {@link #initialN2en}.
931     * @param mouseEn mouse coordinates
932     * @return best movement vector
933     */
934    private EastNorth calculateBestMovementAndNewNodes(EastNorth mouseEn) {
935        EastNorth bestMovement = calculateBestMovement(mouseEn);
936        EastNorth n1movedEn = initialN1en.add(bestMovement), n2movedEn;
937
938        // find out the movement distance, in metres
939        double distance = ProjectionRegistry.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(
940                ProjectionRegistry.getProjection().eastNorth2latlon(n1movedEn));
941        MainApplication.getMap().statusLine.setDist(distance);
942        updateStatusLine();
943
944        if (dualAlignActive) {
945            // new positions of selected segment's nodes, without applying dual alignment
946            n1movedEn = initialN1en.add(bestMovement);
947            n2movedEn = initialN2en.add(bestMovement);
948
949            // calculate intersections of parallel shifted segment and the adjacent lines
950            newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2);
951            newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2);
952            if (newN1en == null || newN2en == null) return bestMovement;
953            if (keepSegmentDirection && isOppositeDirection(newN1en, newN2en, initialN1en, initialN2en)) {
954                EastNorth collapsedSegmentPosition = Geometry.getLineLineIntersection(dualAlignSegment1.p1, dualAlignSegment1.p2,
955                        dualAlignSegment2.p1, dualAlignSegment2.p2);
956                newN1en = collapsedSegmentPosition;
957                newN2en = collapsedSegmentPosition;
958                dualAlignSegmentCollapsed = true;
959            } else {
960                dualAlignSegmentCollapsed = false;
961            }
962        } else {
963            newN1en = n1movedEn;
964            newN2en = initialN2en.add(bestMovement);
965        }
966        return bestMovement;
967    }
968
969    /**
970     * Gets a node index from selected way before given index.
971     * @param index  index of current node
972     * @return index of previous node or <code>-1</code> if there are no nodes there.
973     */
974    private int getPreviousNodeIndex(int index) {
975        if (index > 0)
976            return index - 1;
977        else if (selectedSegment.getWay().isClosed())
978            return selectedSegment.getWay().getNodesCount() - 2;
979        else
980            return -1;
981    }
982
983    /**
984     * Gets a node from selected way before given index.
985     * @param index  index of current node
986     * @return previous node or <code>null</code> if there are no nodes there.
987     */
988    private Node getPreviousNode(int index) {
989        int indexPrev = getPreviousNodeIndex(index);
990        if (indexPrev >= 0)
991            return selectedSegment.getWay().getNode(indexPrev);
992        else
993            return null;
994    }
995
996    /**
997     * Gets a node index from selected way after given index.
998     * @param index index of current node
999     * @return index of next node or <code>-1</code> if there are no nodes there.
1000     */
1001    private int getNextNodeIndex(int index) {
1002        int count = selectedSegment.getWay().getNodesCount();
1003        if (index < count - 1)
1004            return index + 1;
1005        else if (selectedSegment.getWay().isClosed())
1006            return 1;
1007        else
1008            return -1;
1009    }
1010
1011    /**
1012     * Gets a node from selected way after given index.
1013     * @param index index of current node
1014     * @return next node or <code>null</code> if there are no nodes there.
1015     */
1016    private Node getNextNode(int index) {
1017        int indexNext = getNextNodeIndex(index);
1018        if (indexNext >= 0)
1019            return selectedSegment.getWay().getNode(indexNext);
1020        else
1021            return null;
1022    }
1023
1024    // -------------------------------------------------------------------------
1025    // paint methods
1026    // -------------------------------------------------------------------------
1027
1028    @Override
1029    public void paint(Graphics2D g, MapView mv, Bounds box) {
1030        Graphics2D g2 = g;
1031        if (mode == Mode.select) {
1032            // Nothing to do
1033        } else {
1034            if (newN1en != null) {
1035
1036                EastNorth p1 = initialN1en;
1037                EastNorth p2 = initialN2en;
1038                EastNorth p3 = newN1en;
1039                EastNorth p4 = newN2en;
1040
1041                Point2D normalUnitVector = activeMoveDirection != null ? getNormalUniVector() : null;
1042
1043                if (mode == Mode.extrude || mode == Mode.create_new) {
1044                    g2.setColor(mainColor);
1045                    g2.setStroke(mainStroke);
1046                    // Draw rectangle around new area.
1047                    MapViewPath b = new MapViewPath(mv);
1048                    b.moveTo(p1);
1049                    b.lineTo(p3);
1050                    b.lineTo(p4);
1051                    b.lineTo(p2);
1052                    b.lineTo(p1);
1053                    g2.draw(b);
1054
1055                    if (dualAlignActive) {
1056                        // Draw reference ways
1057                        drawReferenceSegment(g2, mv, dualAlignSegment1);
1058                        drawReferenceSegment(g2, mv, dualAlignSegment2);
1059                    } else if (activeMoveDirection != null && normalUnitVector != null) {
1060                        // Draw reference way
1061                        drawReferenceSegment(g2, mv, activeMoveDirection);
1062
1063                        // Draw right angle marker on first node position, only when moving at right angle
1064                        if (activeMoveDirection.perpendicular) {
1065                            // mirror RightAngle marker, so it is inside the extrude
1066                            double headingRefWS = activeMoveDirection.p1.heading(activeMoveDirection.p2);
1067                            double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX());
1068                            double headingDiff = headingRefWS - headingMoveDir;
1069                            if (headingDiff < 0)
1070                                headingDiff += 2 * Math.PI;
1071                            boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
1072                            Point pr1 = mv.getPoint(activeMoveDirection.p1);
1073                            drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
1074                        }
1075                    }
1076                } else if (mode == Mode.translate || mode == Mode.translate_node) {
1077                    g2.setColor(mainColor);
1078                    if (p1.distance(p2) < 3) {
1079                        g2.setStroke(mainStroke);
1080                        g2.draw(new MapViewPath(mv).shapeAround(p1, SymbolShape.CIRCLE, symbolSize));
1081                    } else {
1082                        g2.setStroke(oldLineStroke);
1083                        g2.draw(new MapViewPath(mv).moveTo(p1).lineTo(p2));
1084                    }
1085
1086                    if (dualAlignActive) {
1087                        // Draw reference ways
1088                        drawReferenceSegment(g2, mv, dualAlignSegment1);
1089                        drawReferenceSegment(g2, mv, dualAlignSegment2);
1090                    } else if (activeMoveDirection != null) {
1091
1092                        g2.setColor(helperColor);
1093                        g2.setStroke(helperStrokeDash);
1094                        // Draw a guideline along the normal.
1095                        Point2D centerpoint = mv.getPoint2D(p1.interpolate(p2, .5));
1096                        g2.draw(createSemiInfiniteLine(centerpoint, normalUnitVector, g2));
1097                        // Draw right angle marker on initial position, only when moving at right angle
1098                        if (activeMoveDirection.perpendicular) {
1099                            // EastNorth units per pixel
1100                            g2.setStroke(helperStrokeRA);
1101                            g2.setColor(mainColor);
1102                            drawAngleSymbol(g2, centerpoint, normalUnitVector, false);
1103                        }
1104                    }
1105                }
1106            }
1107            g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings
1108        }
1109    }
1110
1111    private Point2D getNormalUniVector() {
1112        double fac = 1.0 / activeMoveDirection.en.length();
1113        // mult by factor to get unit vector.
1114        Point2D normalUnitVector = new Point2D.Double(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac);
1115
1116        // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
1117        // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
1118        if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) {
1119            // If not, use a sign-flipped version of the normalUnitVector.
1120            normalUnitVector = new Point2D.Double(-normalUnitVector.getX(), -normalUnitVector.getY());
1121        }
1122
1123        //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
1124        //This is normally done by MapView.getPoint, but it does not work on vectors.
1125        normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
1126        return normalUnitVector;
1127    }
1128
1129    /**
1130     * Determines if from1-to1 and from2-to2 vectors directions are opposite
1131     * @param from1 vector1 start
1132     * @param to1 vector1 end
1133     * @param from2 vector2 start
1134     * @param to2 vector2 end
1135     * @return true if from1-to1 and from2-to2 vectors directions are opposite
1136     */
1137    private static boolean isOppositeDirection(EastNorth from1, EastNorth to1, EastNorth from2, EastNorth to2) {
1138        return (from1.getX()-to1.getX())*(from2.getX()-to2.getX())
1139              +(from1.getY()-to1.getY())*(from2.getY()-to2.getY()) < 0;
1140    }
1141
1142    /**
1143     * Draws right angle symbol at specified position.
1144     * @param g2 the Graphics2D object used to draw on
1145     * @param center center point of angle
1146     * @param normal vector of normal
1147     * @param mirror {@code true} if symbol should be mirrored by the normal
1148     */
1149    private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) {
1150        // EastNorth units per pixel
1151        double factor = 1.0/g2.getTransform().getScaleX();
1152        double raoffsetx = symbolSize*factor*normal.getX();
1153        double raoffsety = symbolSize*factor*normal.getY();
1154
1155        double cx = center.getX(), cy = center.getY();
1156        double k = mirror ? -1 : 1;
1157        Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety);
1158        Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k);
1159        Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k);
1160
1161        GeneralPath ra = new GeneralPath();
1162        ra.moveTo((float) ra1.getX(), (float) ra1.getY());
1163        ra.lineTo((float) ra2.getX(), (float) ra2.getY());
1164        ra.lineTo((float) ra3.getX(), (float) ra3.getY());
1165        g2.setStroke(helperStrokeRA);
1166        g2.draw(ra);
1167    }
1168
1169    /**
1170     * Draws given reference segment.
1171     * @param g2 the Graphics2D object used to draw on
1172     * @param mv map view
1173     * @param seg the reference segment
1174     */
1175    private void drawReferenceSegment(Graphics2D g2, MapView mv, ReferenceSegment seg) {
1176        g2.setColor(helperColor);
1177        g2.setStroke(helperStrokeDash);
1178        g2.draw(new MapViewPath(mv).moveTo(seg.p1).lineTo(seg.p2));
1179    }
1180
1181    /**
1182     * Creates a new Line that extends off the edge of the viewport in one direction
1183     * @param start The start point of the line
1184     * @param unitvector A unit vector denoting the direction of the line
1185     * @param g the Graphics2D object  it will be used on
1186     * @return created line
1187     */
1188    private static Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
1189        Rectangle bounds = g.getClipBounds();
1190        try {
1191            AffineTransform invtrans = g.getTransform().createInverse();
1192            Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width, 0), null);
1193            Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0, bounds.height), null);
1194
1195            // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
1196            // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
1197            // This can be used as a safe length of line to generate which will always go off-viewport.
1198            double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY())
1199                    + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
1200
1201            return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength), start.getY()
1202                    + (unitvector.getY() * linelength)));
1203        } catch (NoninvertibleTransformException e) {
1204            Logging.debug(e);
1205            return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10), start.getY()
1206                    + (unitvector.getY() * 10)));
1207        }
1208    }
1209}