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.ChangeCommand;
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) befire 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("Mode: {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        ignoreNextKeyRelease = true;
298        map.keyDetector.addKeyListener(this);
299        map.keyDetector.addModifierExListener(this);
300    }
301
302    @Override
303    protected void readPreferences() {
304        initialMoveDelay = Config.getPref().getInt("edit.initial-move-delay", 200);
305        initialMoveThreshold = Config.getPref().getInt("extrude.initial-move-threshold", 1);
306        mainColor = new NamedColorProperty(marktr("Extrude: main line"), Color.RED).get();
307        helperColor = new NamedColorProperty(marktr("Extrude: helper line"), Color.ORANGE).get();
308        helperStrokeDash = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.helper-line", "1 4"));
309        helperStrokeRA = new BasicStroke(1);
310        symbolSize = Config.getPref().getDouble("extrude.angle-symbol-radius", 8);
311        nodeDragWithoutCtrl = Config.getPref().getBoolean("extrude.drag-nodes-without-ctrl", false);
312        oldLineStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.ctrl.stroke.old-line", "1"));
313        mainStroke = GuiHelper.getCustomizedStroke(Config.getPref().get("extrude.stroke.main", "3"));
314
315        ignoreSharedNodes = Config.getPref().getBoolean("extrude.ignore-shared-nodes", true);
316        dualAlignCheckboxMenuItem.getAction().setEnabled(true);
317        useRepeatedShortcut = Config.getPref().getBoolean("extrude.dualalign.toggleOnRepeatedX", true);
318        keepSegmentDirection = Config.getPref().getBoolean("extrude.dualalign.keep-segment-direction", true);
319    }
320
321    @Override
322    public void exitMode() {
323        MapFrame map = MainApplication.getMap();
324        map.mapView.removeMouseListener(this);
325        map.mapView.removeMouseMotionListener(this);
326        map.mapView.removeTemporaryLayer(this);
327        dualAlignCheckboxMenuItem.getAction().setEnabled(false);
328        map.keyDetector.removeKeyListener(this);
329        map.keyDetector.removeModifierExListener(this);
330        super.exitMode();
331    }
332
333    // -------------------------------------------------------------------------
334    // Event handlers
335    // -------------------------------------------------------------------------
336
337    /**
338     * This method is called to indicate different modes via cursor when the Alt/Ctrl/Shift modifier is pressed,
339     */
340    @Override
341    public void modifiersExChanged(int modifiers) {
342        MapFrame map = MainApplication.getMap();
343        if (!MainApplication.isDisplayingMapView() || !map.mapView.isActiveLayerDrawable())
344            return;
345        updateKeyModifiersEx(modifiers);
346        if (mode == Mode.select) {
347            map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
348        }
349    }
350
351    @Override
352    public void doKeyPressed(KeyEvent e) {
353        // Do nothing
354    }
355
356    @Override
357    public void doKeyReleased(KeyEvent e) {
358        if (!dualAlignShortcut.isEvent(e) && !(useRepeatedShortcut && getShortcut().isEvent(e)))
359             return;
360        if (ignoreNextKeyRelease) {
361            ignoreNextKeyRelease = false;
362        } else {
363            toggleDualAlign();
364        }
365    }
366
367    /**
368     * Toggles dual alignment mode.
369     */
370    private void toggleDualAlign() {
371        dualAlignEnabled = !dualAlignEnabled;
372        dualAlignCheckboxMenuItem.setState(dualAlignEnabled);
373        updateStatusLine();
374    }
375
376    /**
377     * If the left mouse button is pressed over a segment or a node, switches
378     * to appropriate {@link #mode}, depending on Ctrl/Alt/Shift modifiers and
379     * {@link #dualAlignEnabled}.
380     * @param e current mouse event
381     */
382    @Override
383    public void mousePressed(MouseEvent e) {
384        MapFrame map = MainApplication.getMap();
385        if (!map.mapView.isActiveLayerVisible())
386            return;
387        if (!(Boolean) this.getValue("active"))
388            return;
389        if (e.getButton() != MouseEvent.BUTTON1)
390            return;
391
392        requestFocusInMapView();
393        updateKeyModifiers(e);
394
395        selectedNode = map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
396        selectedSegment = map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
397
398        // If nothing gets caught, stay in select mode
399        if (selectedSegment == null && selectedNode == null) return;
400
401        if (selectedNode != null) {
402            if (ctrl || nodeDragWithoutCtrl) {
403                movingNodeList = new ArrayList<>();
404                movingNodeList.add(selectedNode);
405                calculatePossibleDirectionsByNode();
406                if (possibleMoveDirections.isEmpty()) {
407                    // if no directions fould, do not enter dragging mode
408                    return;
409                }
410                mode = Mode.translate_node;
411                dualAlignActive = false;
412            }
413        } else {
414            // Otherwise switch to another mode
415            if (dualAlignEnabled && checkDualAlignConditions()) {
416                dualAlignActive = true;
417                calculatePossibleDirectionsForDualAlign();
418                dualAlignSegmentCollapsed = false;
419            } else {
420                dualAlignActive = false;
421                calculatePossibleDirectionsBySegment();
422            }
423            if (ctrl) {
424                mode = Mode.translate;
425                movingNodeList = new ArrayList<>();
426                movingNodeList.add(selectedSegment.getFirstNode());
427                movingNodeList.add(selectedSegment.getSecondNode());
428            } else if (alt) {
429                mode = Mode.create_new;
430                // create a new segment and then select and extrude the new segment
431                getLayerManager().getEditDataSet().setSelected(selectedSegment.way);
432                alwaysCreateNodes = true;
433            } else {
434                mode = Mode.extrude;
435                getLayerManager().getEditDataSet().setSelected(selectedSegment.way);
436                alwaysCreateNodes = shift;
437            }
438        }
439
440        // Signifies that nothing has happened yet
441        newN1en = null;
442        newN2en = null;
443        moveCommand = null;
444        moveCommand2 = null;
445
446        map.mapView.addTemporaryLayer(this);
447
448        updateStatusLine();
449        map.mapView.repaint();
450
451        // Make note of time pressed
452        mouseDownTime = System.currentTimeMillis();
453
454        // Make note of mouse position
455        initialMousePos = e.getPoint();
456   }
457
458    /**
459     * Performs action depending on what {@link #mode} we're in.
460     * @param e current mouse event
461     */
462    @Override
463    public void mouseDragged(MouseEvent e) {
464        MapView mapView = MainApplication.getMap().mapView;
465        if (!mapView.isActiveLayerVisible())
466            return;
467
468        // do not count anything as a drag if it lasts less than 100 milliseconds.
469        if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
470            return;
471
472        if (mode == Mode.select) {
473            // Just sit tight and wait for mouse to be released.
474        } else {
475            //move, create new and extrude mode - move the selected segment
476
477            EastNorth mouseEn = mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
478            EastNorth bestMovement = calculateBestMovementAndNewNodes(mouseEn);
479
480            mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
481
482            if (dualAlignActive) {
483                if (mode == Mode.extrude || mode == Mode.create_new) {
484                    // nothing here
485                } else if (mode == Mode.translate) {
486                    EastNorth movement1 = newN1en.subtract(initialN1en);
487                    EastNorth movement2 = newN2en.subtract(initialN2en);
488                    // move nodes to new position
489                    if (moveCommand == null || moveCommand2 == null) {
490                        // make a new move commands
491                        moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
492                        moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
493                        Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
494                        UndoRedoHandler.getInstance().add(c);
495                    } else {
496                        // reuse existing move commands
497                        moveCommand.moveAgainTo(movement1.getX(), movement1.getY());
498                        moveCommand2.moveAgainTo(movement2.getX(), movement2.getY());
499                    }
500                }
501            } else if (bestMovement != null) {
502                if (mode == Mode.extrude || mode == Mode.create_new) {
503                    //nothing here
504                } else if (mode == Mode.translate_node || mode == Mode.translate) {
505                    //move nodes to new position
506                    if (moveCommand == null) {
507                        //make a new move command
508                        moveCommand = new MoveCommand(new ArrayList<OsmPrimitive>(movingNodeList), bestMovement);
509                        UndoRedoHandler.getInstance().add(moveCommand);
510                    } else {
511                        //reuse existing move command
512                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
513                    }
514                }
515            }
516
517            mapView.repaint();
518        }
519    }
520
521    /**
522     * Does anything that needs to be done, then switches back to select mode.
523     * @param e current mouse event
524     */
525    @Override
526    public void mouseReleased(MouseEvent e) {
527
528        MapView mapView = MainApplication.getMap().mapView;
529        if (!mapView.isActiveLayerVisible())
530            return;
531
532        if (mode == Mode.select) {
533            // Nothing to be done
534        } else {
535            if (mode == Mode.create_new) {
536                if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null) {
537                    createNewRectangle();
538                }
539            } else if (mode == Mode.extrude) {
540                if (e.getClickCount() == 2 && e.getPoint().equals(initialMousePos)) {
541                    // double click adds a new node
542                    addNewNode(e);
543                } else if (e.getPoint().distance(initialMousePos) > initialMoveThreshold && newN1en != null && selectedSegment != null) {
544                    try {
545                        // main extrusion commands
546                        performExtrusion();
547                    } catch (DataIntegrityProblemException ex) {
548                        // Can occur if calling undo while extruding, see #12870
549                        Logging.error(ex);
550                    }
551                }
552            } else if (mode == Mode.translate || mode == Mode.translate_node) {
553                //Commit translate
554                //the move command is already committed in mouseDragged
555                joinNodesIfCollapsed(movingNodeList);
556            }
557
558            updateKeyModifiers(e);
559            // Switch back into select mode
560            mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
561            mapView.removeTemporaryLayer(this);
562            selectedSegment = null;
563            moveCommand = null;
564            mode = Mode.select;
565            dualAlignSegmentCollapsed = false;
566            updateStatusLine();
567            mapView.repaint();
568        }
569    }
570
571    // -------------------------------------------------------------------------
572    // Custom methods
573    // -------------------------------------------------------------------------
574
575    /**
576     * Inserts node into nearby segment.
577     * @param e current mouse point
578     */
579    private static void addNewNode(MouseEvent e) {
580        // Should maybe do the same as in DrawAction and fetch all nearby segments?
581        MapView mapView = MainApplication.getMap().mapView;
582        WaySegment ws = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
583        if (ws != null) {
584            Node n = new Node(mapView.getLatLon(e.getX(), e.getY()));
585            EastNorth a = ws.getFirstNode().getEastNorth();
586            EastNorth b = ws.getSecondNode().getEastNorth();
587            n.setEastNorth(Geometry.closestPointToSegment(a, b, n.getEastNorth()));
588            Way wnew = new Way(ws.way);
589            wnew.addNode(ws.lowerIndex+1, n);
590            DataSet ds = ws.way.getDataSet();
591            UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Add a new node to an existing way"),
592                    new AddCommand(ds, n), new ChangeCommand(ds, ws.way, wnew)));
593        }
594    }
595
596    /**
597     * Creates a new way that shares segment with selected way.
598     */
599    private void createNewRectangle() {
600        if (selectedSegment == null) return;
601        DataSet ds = getLayerManager().getEditDataSet();
602        // create a new rectangle
603        Collection<Command> cmds = new LinkedList<>();
604        Node third = new Node(newN2en);
605        Node fourth = new Node(newN1en);
606        Way wnew = new Way();
607        wnew.addNode(selectedSegment.getFirstNode());
608        wnew.addNode(selectedSegment.getSecondNode());
609        wnew.addNode(third);
610        if (!dualAlignSegmentCollapsed) {
611            // rectangle can degrade to triangle for dual alignment after collapsing
612            wnew.addNode(fourth);
613        }
614        // ... and close the way
615        wnew.addNode(selectedSegment.getFirstNode());
616        // undo support
617        cmds.add(new AddCommand(ds, third));
618        if (!dualAlignSegmentCollapsed) {
619            cmds.add(new AddCommand(ds, fourth));
620        }
621        cmds.add(new AddCommand(ds, wnew));
622        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
623        UndoRedoHandler.getInstance().add(c);
624        ds.setSelected(wnew);
625    }
626
627    /**
628     * Does actual extrusion of {@link #selectedSegment}.
629     * Uses {@link #initialN1en}, {@link #initialN2en} saved in calculatePossibleDirections* call
630     * Uses {@link #newN1en}, {@link #newN2en} calculated by {@link #calculateBestMovementAndNewNodes}
631     */
632    private void performExtrusion() {
633        DataSet ds = getLayerManager().getEditDataSet();
634        // create extrusion
635        Collection<Command> cmds = new LinkedList<>();
636        Way wnew = new Way(selectedSegment.way);
637        boolean wayWasModified = false;
638        boolean wayWasSingleSegment = wnew.getNodesCount() == 2;
639        int insertionPoint = selectedSegment.lowerIndex + 1;
640
641        //find if the new points overlap existing segments (in case of 90 degree angles)
642        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
643        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
644        // segmentAngleZero marks subset of nodeOverlapsSegment.
645        // nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0
646        boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5;
647        boolean hasOtherWays = hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
648        List<Node> changedNodes = new ArrayList<>();
649        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
650            //move existing node
651            Node n1Old = selectedSegment.getFirstNode();
652            cmds.add(new MoveCommand(n1Old, ProjectionRegistry.getProjection().eastNorth2latlon(newN1en)));
653            changedNodes.add(n1Old);
654        } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) {
655            // replace shared node with new one
656            Node n1Old = selectedSegment.getFirstNode();
657            Node n1New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN1en));
658            wnew.addNode(insertionPoint, n1New);
659            wnew.removeNode(n1Old);
660            wayWasModified = true;
661            cmds.add(new AddCommand(ds, n1New));
662            changedNodes.add(n1New);
663        } else {
664            //introduce new node
665            Node n1New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN1en));
666            wnew.addNode(insertionPoint, n1New);
667            wayWasModified = true;
668            insertionPoint++;
669            cmds.add(new AddCommand(ds, n1New));
670            changedNodes.add(n1New);
671        }
672
673        //find if the new points overlap existing segments (in case of 90 degree angles)
674        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
675        nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
676        segmentAngleZero = nextNode != null && Math.abs(Geometry.getCornerAngle(nextNode.getEastNorth(), initialN2en, newN2en)) < 1e-5;
677        hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way);
678
679        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
680            //move existing node
681            Node n2Old = selectedSegment.getSecondNode();
682            cmds.add(new MoveCommand(n2Old, ProjectionRegistry.getProjection().eastNorth2latlon(newN2en)));
683            changedNodes.add(n2Old);
684        } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) {
685            // replace shared node with new one
686            Node n2Old = selectedSegment.getSecondNode();
687            Node n2New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN2en));
688            wnew.addNode(insertionPoint, n2New);
689            wnew.removeNode(n2Old);
690            wayWasModified = true;
691            cmds.add(new AddCommand(ds, n2New));
692            changedNodes.add(n2New);
693        } else {
694            //introduce new node
695            Node n2New = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(newN2en));
696            wnew.addNode(insertionPoint, n2New);
697            wayWasModified = true;
698            cmds.add(new AddCommand(ds, n2New));
699            changedNodes.add(n2New);
700        }
701
702        //the way was a single segment, close the way
703        if (wayWasSingleSegment) {
704            wnew.addNode(selectedSegment.getFirstNode());
705            wayWasModified = true;
706        }
707        if (wayWasModified) {
708            // we only need to change the way if its node list was really modified
709            cmds.add(new ChangeCommand(selectedSegment.way, wnew));
710        }
711        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
712        UndoRedoHandler.getInstance().add(c);
713        joinNodesIfCollapsed(changedNodes);
714    }
715
716    private void joinNodesIfCollapsed(List<Node> changedNodes) {
717        if (!dualAlignActive || newN1en == null || newN2en == null) return;
718        if (newN1en.distance(newN2en) > 1e-6) return;
719        // If the dual alignment moved two nodes to the same point, merge them
720        Node targetNode = MergeNodesAction.selectTargetNode(changedNodes);
721        Node locNode = MergeNodesAction.selectTargetLocationNode(changedNodes);
722        Command mergeCmd = MergeNodesAction.mergeNodes(changedNodes, targetNode, locNode);
723        if (mergeCmd != null) {
724            UndoRedoHandler.getInstance().add(mergeCmd);
725        } else {
726            // undo extruding command itself
727            UndoRedoHandler.getInstance().undo();
728        }
729    }
730
731    /**
732     * This method tests if {@code node} has other ways apart from the given one.
733     * @param node node to test
734     * @param myWay way known to contain this node
735     * @return {@code true} if {@code node} belongs only to {@code myWay}, false if there are more ways.
736     */
737    private static boolean hasNodeOtherWays(Node node, Way myWay) {
738        for (OsmPrimitive p : node.getReferrers()) {
739            if (p instanceof Way && p.isUsable() && p != myWay)
740                return true;
741        }
742        return false;
743    }
744
745    /**
746     * Determines best movement from {@link #initialMousePos} to current mouse position,
747     * choosing one of the directions from {@link #possibleMoveDirections}.
748     * @param mouseEn current mouse position
749     * @return movement vector
750     */
751    private EastNorth calculateBestMovement(EastNorth mouseEn) {
752
753        EastNorth initialMouseEn = MainApplication.getMap().mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
754        EastNorth mouseMovement = mouseEn.subtract(initialMouseEn);
755
756        double bestDistance = Double.POSITIVE_INFINITY;
757        EastNorth bestMovement = null;
758        activeMoveDirection = null;
759
760        //find the best movement direction and vector
761        for (ReferenceSegment direction : possibleMoveDirections) {
762            EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn);
763            if (movement == null) {
764                //if direction parallel to segment.
765                continue;
766            }
767
768            double distanceFromMouseMovement = movement.distance(mouseMovement);
769            if (bestDistance > distanceFromMouseMovement) {
770                bestDistance = distanceFromMouseMovement;
771                activeMoveDirection = direction;
772                bestMovement = movement;
773            }
774        }
775        return bestMovement;
776    }
777
778    /***
779     * This method calculates offset amount by which to move the given segment
780     * perpendicularly for it to be in line with mouse position.
781     * @param segmentP1 segment's first point
782     * @param segmentP2 segment's second point
783     * @param moveDirection direction of movement
784     * @param targetPos mouse position
785     * @return offset amount of P1 and P2.
786     */
787    private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
788            EastNorth targetPos) {
789        EastNorth intersectionPoint;
790        if (segmentP1.distanceSq(segmentP2) > 1e-7) {
791            intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection));
792        } else {
793            intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1);
794        }
795
796        if (intersectionPoint == null)
797            return null;
798        else
799            //return distance form base to target position
800            return targetPos.subtract(intersectionPoint);
801    }
802
803    /**
804     * Gathers possible move directions - perpendicular to the selected segment
805     * and parallel to neighboring segments.
806     */
807    private void calculatePossibleDirectionsBySegment() {
808        // remember initial positions for segment nodes.
809        initialN1en = selectedSegment.getFirstNode().getEastNorth();
810        initialN2en = selectedSegment.getSecondNode().getEastNorth();
811
812        //add direction perpendicular to the selected segment
813        possibleMoveDirections = new ArrayList<>();
814        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
815                initialN1en.getY() - initialN2en.getY(),
816                initialN2en.getX() - initialN1en.getX()
817                ), initialN1en, initialN2en, true));
818
819
820        //add directions parallel to neighbor segments
821        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
822        if (prevNode != null) {
823            EastNorth en = prevNode.getEastNorth();
824            possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
825                    initialN1en.getX() - en.getX(),
826                    initialN1en.getY() - en.getY()
827                    ), initialN1en, en, false));
828        }
829
830        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
831        if (nextNode != null) {
832            EastNorth en = nextNode.getEastNorth();
833            possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
834                    initialN2en.getX() - en.getX(),
835                    initialN2en.getY() - en.getY()
836                    ), initialN2en, en, false));
837        }
838    }
839
840    /**
841     * Gathers possible move directions - along all adjacent segments.
842     */
843    private void calculatePossibleDirectionsByNode() {
844        // remember initial positions for segment nodes.
845        initialN1en = selectedNode.getEastNorth();
846        initialN2en = initialN1en;
847        possibleMoveDirections = new ArrayList<>();
848        for (OsmPrimitive p: selectedNode.getReferrers()) {
849            if (p instanceof Way && p.isUsable()) {
850                for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) {
851                    EastNorth en = neighbor.getEastNorth();
852                    possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
853                        initialN1en.getX() - en.getX(),
854                        initialN1en.getY() - en.getY()
855                    ), initialN1en, en, false));
856                }
857            }
858        }
859    }
860
861    /**
862     * Checks dual alignment conditions:
863     *  1. selected segment has both neighboring segments,
864     *  2. selected segment is not parallel with neighboring segments.
865     * @return {@code true} if dual alignment conditions are satisfied
866     */
867    private boolean checkDualAlignConditions() {
868        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
869        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
870        if (prevNode == null || nextNode == null) {
871            return false;
872        }
873
874        EastNorth n1en = selectedSegment.getFirstNode().getEastNorth();
875        EastNorth n2en = selectedSegment.getSecondNode().getEastNorth();
876        if (n1en.distance(prevNode.getEastNorth()) < 1e-4 ||
877            n2en.distance(nextNode.getEastNorth()) < 1e-4) {
878            return false;
879        }
880
881        boolean prevSegmentParallel = Geometry.segmentsParallel(n1en, prevNode.getEastNorth(), n1en, n2en);
882        boolean nextSegmentParallel = Geometry.segmentsParallel(n2en, nextNode.getEastNorth(), n1en, n2en);
883        return !prevSegmentParallel && !nextSegmentParallel;
884    }
885
886    /**
887     * Gathers possible move directions - perpendicular to the selected segment only.
888     * Neighboring segments go to {@link #dualAlignSegment1} and {@link #dualAlignSegment2}.
889     */
890    private void calculatePossibleDirectionsForDualAlign() {
891        // remember initial positions for segment nodes.
892        initialN1en = selectedSegment.getFirstNode().getEastNorth();
893        initialN2en = selectedSegment.getSecondNode().getEastNorth();
894
895        // add direction perpendicular to the selected segment
896        possibleMoveDirections = new ArrayList<>();
897        possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
898                initialN1en.getY() - initialN2en.getY(),
899                initialN2en.getX() - initialN1en.getX()
900                ), initialN1en, initialN2en, true));
901
902        // set neighboring segments
903        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
904        if (prevNode != null) {
905            EastNorth prevNodeEn = prevNode.getEastNorth();
906            dualAlignSegment1 = new ReferenceSegment(new EastNorth(
907                initialN1en.getX() - prevNodeEn.getX(),
908                initialN1en.getY() - prevNodeEn.getY()
909                ), initialN1en, prevNodeEn, false);
910        }
911
912        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
913        if (nextNode != null) {
914            EastNorth nextNodeEn = nextNode.getEastNorth();
915            dualAlignSegment2 = new ReferenceSegment(new EastNorth(
916                initialN2en.getX() - nextNodeEn.getX(),
917                initialN2en.getY() - nextNodeEn.getY()
918                ), initialN2en, nextNodeEn, false);
919        }
920    }
921
922    /**
923     * Calculate newN1en, newN2en best suitable for given mouse coordinates
924     * For dual align, calculates positions of new nodes, aligning them to neighboring segments.
925     * Elsewhere, just adds the vetor returned by calculateBestMovement to {@link #initialN1en},  {@link #initialN2en}.
926     * @param mouseEn mouse coordinates
927     * @return best movement vector
928     */
929    private EastNorth calculateBestMovementAndNewNodes(EastNorth mouseEn) {
930        EastNorth bestMovement = calculateBestMovement(mouseEn);
931        EastNorth n1movedEn = initialN1en.add(bestMovement), n2movedEn;
932
933        // find out the movement distance, in metres
934        double distance = ProjectionRegistry.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(
935                ProjectionRegistry.getProjection().eastNorth2latlon(n1movedEn));
936        MainApplication.getMap().statusLine.setDist(distance);
937        updateStatusLine();
938
939        if (dualAlignActive) {
940            // new positions of selected segment's nodes, without applying dual alignment
941            n1movedEn = initialN1en.add(bestMovement);
942            n2movedEn = initialN2en.add(bestMovement);
943
944            // calculate intersections of parallel shifted segment and the adjacent lines
945            newN1en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment1.p1, dualAlignSegment1.p2);
946            newN2en = Geometry.getLineLineIntersection(n1movedEn, n2movedEn, dualAlignSegment2.p1, dualAlignSegment2.p2);
947            if (newN1en == null || newN2en == null) return bestMovement;
948            if (keepSegmentDirection && isOppositeDirection(newN1en, newN2en, initialN1en, initialN2en)) {
949                EastNorth collapsedSegmentPosition = Geometry.getLineLineIntersection(dualAlignSegment1.p1, dualAlignSegment1.p2,
950                        dualAlignSegment2.p1, dualAlignSegment2.p2);
951                newN1en = collapsedSegmentPosition;
952                newN2en = collapsedSegmentPosition;
953                dualAlignSegmentCollapsed = true;
954            } else {
955                dualAlignSegmentCollapsed = false;
956            }
957        } else {
958            newN1en = n1movedEn;
959            newN2en = initialN2en.add(bestMovement);
960        }
961        return bestMovement;
962    }
963
964    /**
965     * Gets a node index from selected way before given index.
966     * @param index  index of current node
967     * @return index of previous node or <code>-1</code> if there are no nodes there.
968     */
969    private int getPreviousNodeIndex(int index) {
970        if (index > 0)
971            return index - 1;
972        else if (selectedSegment.way.isClosed())
973            return selectedSegment.way.getNodesCount() - 2;
974        else
975            return -1;
976    }
977
978    /**
979     * Gets a node from selected way before given index.
980     * @param index  index of current node
981     * @return previous node or <code>null</code> if there are no nodes there.
982     */
983    private Node getPreviousNode(int index) {
984        int indexPrev = getPreviousNodeIndex(index);
985        if (indexPrev >= 0)
986            return selectedSegment.way.getNode(indexPrev);
987        else
988            return null;
989    }
990
991    /**
992     * Gets a node index from selected way after given index.
993     * @param index index of current node
994     * @return index of next node or <code>-1</code> if there are no nodes there.
995     */
996    private int getNextNodeIndex(int index) {
997        int count = selectedSegment.way.getNodesCount();
998        if (index < count - 1)
999            return index + 1;
1000        else if (selectedSegment.way.isClosed())
1001            return 1;
1002        else
1003            return -1;
1004    }
1005
1006    /**
1007     * Gets a node from selected way after given index.
1008     * @param index index of current node
1009     * @return next node or <code>null</code> if there are no nodes there.
1010     */
1011    private Node getNextNode(int index) {
1012        int indexNext = getNextNodeIndex(index);
1013        if (indexNext >= 0)
1014            return selectedSegment.way.getNode(indexNext);
1015        else
1016            return null;
1017    }
1018
1019    // -------------------------------------------------------------------------
1020    // paint methods
1021    // -------------------------------------------------------------------------
1022
1023    @Override
1024    public void paint(Graphics2D g, MapView mv, Bounds box) {
1025        Graphics2D g2 = g;
1026        if (mode == Mode.select) {
1027            // Nothing to do
1028        } else {
1029            if (newN1en != null) {
1030
1031                EastNorth p1 = initialN1en;
1032                EastNorth p2 = initialN2en;
1033                EastNorth p3 = newN1en;
1034                EastNorth p4 = newN2en;
1035
1036                Point2D normalUnitVector = activeMoveDirection != null ? getNormalUniVector() : null;
1037
1038                if (mode == Mode.extrude || mode == Mode.create_new) {
1039                    g2.setColor(mainColor);
1040                    g2.setStroke(mainStroke);
1041                    // Draw rectangle around new area.
1042                    MapViewPath b = new MapViewPath(mv);
1043                    b.moveTo(p1);
1044                    b.lineTo(p3);
1045                    b.lineTo(p4);
1046                    b.lineTo(p2);
1047                    b.lineTo(p1);
1048                    g2.draw(b);
1049
1050                    if (dualAlignActive) {
1051                        // Draw reference ways
1052                        drawReferenceSegment(g2, mv, dualAlignSegment1);
1053                        drawReferenceSegment(g2, mv, dualAlignSegment2);
1054                    } else if (activeMoveDirection != null && normalUnitVector != null) {
1055                        // Draw reference way
1056                        drawReferenceSegment(g2, mv, activeMoveDirection);
1057
1058                        // Draw right angle marker on first node position, only when moving at right angle
1059                        if (activeMoveDirection.perpendicular) {
1060                            // mirror RightAngle marker, so it is inside the extrude
1061                            double headingRefWS = activeMoveDirection.p1.heading(activeMoveDirection.p2);
1062                            double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX());
1063                            double headingDiff = headingRefWS - headingMoveDir;
1064                            if (headingDiff < 0)
1065                                headingDiff += 2 * Math.PI;
1066                            boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
1067                            Point pr1 = mv.getPoint(activeMoveDirection.p1);
1068                            drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA);
1069                        }
1070                    }
1071                } else if (mode == Mode.translate || mode == Mode.translate_node) {
1072                    g2.setColor(mainColor);
1073                    if (p1.distance(p2) < 3) {
1074                        g2.setStroke(mainStroke);
1075                        g2.draw(new MapViewPath(mv).shapeAround(p1, SymbolShape.CIRCLE, symbolSize));
1076                    } else {
1077                        g2.setStroke(oldLineStroke);
1078                        g2.draw(new MapViewPath(mv).moveTo(p1).lineTo(p2));
1079                    }
1080
1081                    if (dualAlignActive) {
1082                        // Draw reference ways
1083                        drawReferenceSegment(g2, mv, dualAlignSegment1);
1084                        drawReferenceSegment(g2, mv, dualAlignSegment2);
1085                    } else if (activeMoveDirection != null) {
1086
1087                        g2.setColor(helperColor);
1088                        g2.setStroke(helperStrokeDash);
1089                        // Draw a guideline along the normal.
1090                        Point2D centerpoint = mv.getPoint2D(p1.interpolate(p2, .5));
1091                        g2.draw(createSemiInfiniteLine(centerpoint, normalUnitVector, g2));
1092                        // Draw right angle marker on initial position, only when moving at right angle
1093                        if (activeMoveDirection.perpendicular) {
1094                            // EastNorth units per pixel
1095                            g2.setStroke(helperStrokeRA);
1096                            g2.setColor(mainColor);
1097                            drawAngleSymbol(g2, centerpoint, normalUnitVector, false);
1098                        }
1099                    }
1100                }
1101            }
1102            g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings
1103        }
1104    }
1105
1106    private Point2D getNormalUniVector() {
1107        double fac = 1.0 / activeMoveDirection.en.length();
1108        // mult by factor to get unit vector.
1109        Point2D normalUnitVector = new Point2D.Double(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac);
1110
1111        // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
1112        // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
1113        if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) {
1114            // If not, use a sign-flipped version of the normalUnitVector.
1115            normalUnitVector = new Point2D.Double(-normalUnitVector.getX(), -normalUnitVector.getY());
1116        }
1117
1118        //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
1119        //This is normally done by MapView.getPoint, but it does not work on vectors.
1120        normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
1121        return normalUnitVector;
1122    }
1123
1124    /**
1125     * Determines if from1-to1 and from2-to2 vectors directions are opposite
1126     * @param from1 vector1 start
1127     * @param to1 vector1 end
1128     * @param from2 vector2 start
1129     * @param to2 vector2 end
1130     * @return true if from1-to1 and from2-to2 vectors directions are opposite
1131     */
1132    private static boolean isOppositeDirection(EastNorth from1, EastNorth to1, EastNorth from2, EastNorth to2) {
1133        return (from1.getX()-to1.getX())*(from2.getX()-to2.getX())
1134              +(from1.getY()-to1.getY())*(from2.getY()-to2.getY()) < 0;
1135    }
1136
1137    /**
1138     * Draws right angle symbol at specified position.
1139     * @param g2 the Graphics2D object used to draw on
1140     * @param center center point of angle
1141     * @param normal vector of normal
1142     * @param mirror {@code true} if symbol should be mirrored by the normal
1143     */
1144    private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) {
1145        // EastNorth units per pixel
1146        double factor = 1.0/g2.getTransform().getScaleX();
1147        double raoffsetx = symbolSize*factor*normal.getX();
1148        double raoffsety = symbolSize*factor*normal.getY();
1149
1150        double cx = center.getX(), cy = center.getY();
1151        double k = mirror ? -1 : 1;
1152        Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety);
1153        Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k);
1154        Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k);
1155
1156        GeneralPath ra = new GeneralPath();
1157        ra.moveTo((float) ra1.getX(), (float) ra1.getY());
1158        ra.lineTo((float) ra2.getX(), (float) ra2.getY());
1159        ra.lineTo((float) ra3.getX(), (float) ra3.getY());
1160        g2.setStroke(helperStrokeRA);
1161        g2.draw(ra);
1162    }
1163
1164    /**
1165     * Draws given reference segment.
1166     * @param g2 the Graphics2D object used to draw on
1167     * @param mv map view
1168     * @param seg the reference segment
1169     */
1170    private void drawReferenceSegment(Graphics2D g2, MapView mv, ReferenceSegment seg) {
1171        g2.setColor(helperColor);
1172        g2.setStroke(helperStrokeDash);
1173        g2.draw(new MapViewPath(mv).moveTo(seg.p1).lineTo(seg.p2));
1174    }
1175
1176    /**
1177     * Creates a new Line that extends off the edge of the viewport in one direction
1178     * @param start The start point of the line
1179     * @param unitvector A unit vector denoting the direction of the line
1180     * @param g the Graphics2D object  it will be used on
1181     * @return created line
1182     */
1183    private static Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
1184        Rectangle bounds = g.getClipBounds();
1185        try {
1186            AffineTransform invtrans = g.getTransform().createInverse();
1187            Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width, 0), null);
1188            Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0, bounds.height), null);
1189
1190            // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
1191            // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
1192            // This can be used as a safe length of line to generate which will always go off-viewport.
1193            double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY())
1194                    + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
1195
1196            return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength), start.getY()
1197                    + (unitvector.getY() * linelength)));
1198        } catch (NoninvertibleTransformException e) {
1199            Logging.debug(e);
1200            return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10), start.getY()
1201                    + (unitvector.getY() * 10)));
1202        }
1203    }
1204}