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