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

Last change on this file since 12841 was 12841, checked in by bastiK, 3 months ago

see #15229 - fix deprecations caused by [12840]

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