source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java @ 12987

Last change on this file since 12987 was 12987, checked in by bastiK, 10 months ago

see #15410 - change preferences scheme for named colors - makes runtime color name registry obsolete

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