001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions.mapmode;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.marktr;
006    import static org.openstreetmap.josm.tools.I18n.tr;
007    
008    import java.awt.AWTEvent;
009    import java.awt.BasicStroke;
010    import java.awt.Color;
011    import java.awt.Cursor;
012    import java.awt.Graphics2D;
013    import java.awt.Point;
014    import java.awt.Rectangle;
015    import java.awt.Toolkit;
016    import java.awt.event.AWTEventListener;
017    import java.awt.event.ActionEvent;
018    import java.awt.event.InputEvent;
019    import java.awt.event.KeyEvent;
020    import java.awt.event.MouseEvent;
021    import java.awt.geom.AffineTransform;
022    import java.awt.geom.GeneralPath;
023    import java.awt.geom.Line2D;
024    import java.awt.geom.NoninvertibleTransformException;
025    import java.awt.geom.Point2D;
026    import java.util.ArrayList;
027    import java.util.Collection;
028    import java.util.LinkedList;
029    import java.util.List;
030    
031    import org.openstreetmap.josm.Main;
032    import org.openstreetmap.josm.command.AddCommand;
033    import org.openstreetmap.josm.command.ChangeCommand;
034    import org.openstreetmap.josm.command.Command;
035    import org.openstreetmap.josm.command.MoveCommand;
036    import org.openstreetmap.josm.command.SequenceCommand;
037    import org.openstreetmap.josm.data.Bounds;
038    import org.openstreetmap.josm.data.coor.EastNorth;
039    import org.openstreetmap.josm.data.osm.Node;
040    import org.openstreetmap.josm.data.osm.OsmPrimitive;
041    import org.openstreetmap.josm.data.osm.Way;
042    import org.openstreetmap.josm.data.osm.WaySegment;
043    import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
044    import org.openstreetmap.josm.gui.MapFrame;
045    import org.openstreetmap.josm.gui.MapView;
046    import org.openstreetmap.josm.gui.layer.Layer;
047    import org.openstreetmap.josm.gui.layer.MapViewPaintable;
048    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
049    import org.openstreetmap.josm.tools.Geometry;
050    import org.openstreetmap.josm.tools.ImageProvider;
051    import org.openstreetmap.josm.tools.Shortcut;
052    
053    /**
054     * Makes a rectangle from a line, or modifies a rectangle.
055     */
056    public class ExtrudeAction extends MapMode implements MapViewPaintable {
057    
058        enum Mode { extrude, translate, select, create_new }
059    
060        private Mode mode = Mode.select;
061    
062        /**
063         * If true, when extruding create new node even if segments parallel.
064         */
065        private boolean alwaysCreateNodes = false;
066    
067        private long mouseDownTime = 0;
068        private WaySegment selectedSegment = null;
069        private Color selectedColor;
070    
071        /**
072         * drawing settings for helper lines
073         */
074        private Color helperColor;
075        private BasicStroke helperStrokeDash;
076        private BasicStroke helperStrokeRA;
077    
078        /**
079         * Possible directions to move to.
080         */
081        private List<ReferenceSegment> possibleMoveDirections;
082    
083        /**
084         * The direction that is currently active.
085         */
086        private ReferenceSegment activeMoveDirection;
087    
088        /**
089         * The position of the mouse cursor when the drag action was initiated.
090         */
091        private Point initialMousePos;
092        /**
093         * The time which needs to pass between click and release before something
094         * counts as a move, in milliseconds
095         */
096        private int initialMoveDelay = 200;
097        /**
098         * The initial EastNorths of node1 and node2
099         */
100        private EastNorth initialN1en;
101        private EastNorth initialN2en;
102        /**
103         * The new EastNorths of node1 and node2
104         */
105        private EastNorth newN1en;
106        private EastNorth newN2en;
107    
108        /**
109         * the command that performed last move.
110         */
111        private MoveCommand moveCommand;
112    
113        /** The cursor for the 'create_new' mode. */
114        private final Cursor cursorCreateNew;
115    
116        /** The cursor for the 'translate' mode. */
117        private final Cursor cursorTranslate;
118    
119        /** The cursor for the 'alwaysCreateNodes' submode. */
120        private final Cursor cursorCreateNodes;
121    
122        private class ReferenceSegment {
123            public final EastNorth en;
124            public final WaySegment ws;
125            public final boolean perpendicular;
126    
127            public ReferenceSegment(EastNorth en, WaySegment ws, boolean perpendicular) {
128                this.en = en;
129                this.ws = ws;
130                this.perpendicular = perpendicular;
131            }
132        }
133    
134        /**
135         * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed.
136         */
137        private final AWTEventListener altKeyListener = new AWTEventListener() {
138            @Override
139            public void eventDispatched(AWTEvent e) {
140                if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable())
141                    return;
142                InputEvent ie = (InputEvent) e;
143                boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
144                boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
145                boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
146                if (mode == Mode.select) {
147                    Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
148                }
149            }
150        };
151    
152        /**
153         * Create a new SelectAction
154         * @param mapFrame The MapFrame this action belongs to.
155         */
156        public ExtrudeAction(MapFrame mapFrame) {
157            super(tr("Extrude"), "extrude/extrude", tr("Create areas"),
158                    Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT),
159                    mapFrame,
160                    ImageProvider.getCursor("normal", "rectangle"));
161            putValue("help", ht("/Action/Extrude"));
162            initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
163            selectedColor = PaintColors.SELECTED.get();
164            cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus");
165            cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move");
166            cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall");
167            helperColor = Main.pref.getColor(marktr("Extrude: helper line"), Color.ORANGE);
168            float dash1[] = { 4.0f };
169            helperStrokeDash = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
170                    BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
171            helperStrokeRA = new BasicStroke(1);
172        }
173    
174        @Override public String getModeHelpText() {
175            if (mode == Mode.translate)
176                return tr("Move a segment along its normal, then release the mouse button.");
177            else if (mode == Mode.extrude)
178                return tr("Draw a rectangle of the desired size, then release the mouse button.");
179            else if (mode == Mode.create_new)
180                return tr("Draw a rectangle of the desired size, then release the mouse button.");
181            else
182                return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " +
183                "Alt-drag to create a new rectangle, double click to add a new node.");
184        }
185    
186        @Override public boolean layerIsSupported(Layer l) {
187            return l instanceof OsmDataLayer;
188        }
189    
190        @Override public void enterMode() {
191            super.enterMode();
192            Main.map.mapView.addMouseListener(this);
193            Main.map.mapView.addMouseMotionListener(this);
194            try {
195                Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK);
196            } catch (SecurityException ex) {
197            }
198        }
199    
200        @Override public void exitMode() {
201            Main.map.mapView.removeMouseListener(this);
202            Main.map.mapView.removeMouseMotionListener(this);
203            Main.map.mapView.removeTemporaryLayer(this);
204            try {
205                Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener);
206            } catch (SecurityException ex) {
207            }
208            super.exitMode();
209        }
210    
211        /**
212         * If the left mouse button is pressed over a segment, switch
213         * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held.
214         */
215        @Override public void mousePressed(MouseEvent e) {
216            if(!Main.map.mapView.isActiveLayerVisible())
217                return;
218            if (!(Boolean)this.getValue("active"))
219                return;
220            if (e.getButton() != MouseEvent.BUTTON1)
221                return;
222    
223            updateKeyModifiers(e);
224    
225            selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
226    
227            if (selectedSegment == null) {
228                // If nothing gets caught, stay in select mode
229            } else {
230                // Otherwise switch to another mode
231    
232                if (ctrl) {
233                    mode = Mode.translate;
234                } else if (alt) {
235                    mode = Mode.create_new;
236                    // create a new segment and then select and extrude the new segment
237                    getCurrentDataSet().setSelected(selectedSegment.way);
238                    alwaysCreateNodes = true;
239                } else {
240                    mode = Mode.extrude;
241                    getCurrentDataSet().setSelected(selectedSegment.way);
242                    alwaysCreateNodes = shift;
243                }
244    
245                // remember initial positions for segment nodes.
246                initialN1en = selectedSegment.getFirstNode().getEastNorth();
247                initialN2en = selectedSegment.getSecondNode().getEastNorth();
248    
249                //gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments
250                possibleMoveDirections = new ArrayList<ReferenceSegment>();
251                possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
252                        initialN1en.getY() - initialN2en.getY(),
253                        initialN2en.getX() - initialN1en.getX()
254                        ), selectedSegment, true));
255    
256                //add directions parallel to neighbor segments
257    
258                Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
259                if (prevNode != null) {
260                    EastNorth en = prevNode.getEastNorth();
261                    possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
262                            initialN1en.getX() - en.getX(),
263                            initialN1en.getY() - en.getY()
264                            ), new WaySegment(selectedSegment.way, getPreviousNodeIndex(selectedSegment.lowerIndex)), false));
265                }
266    
267                Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
268                if (nextNode != null) {
269                    EastNorth en = nextNode.getEastNorth();
270                    possibleMoveDirections.add(new ReferenceSegment(new EastNorth(
271                            initialN2en.getX() - en.getX(),
272                            initialN2en.getY() - en.getY()
273                            ), new WaySegment(selectedSegment.way, getPreviousNodeIndex(getNextNodeIndex(getNextNodeIndex(selectedSegment.lowerIndex)))), false));
274                }
275    
276                // Signifies that nothing has happened yet
277                newN1en = null;
278                newN2en = null;
279                moveCommand = null;
280    
281                Main.map.mapView.addTemporaryLayer(this);
282    
283                updateStatusLine();
284                Main.map.mapView.repaint();
285    
286                // Make note of time pressed
287                mouseDownTime = System.currentTimeMillis();
288    
289                // Make note of mouse position
290                initialMousePos = e.getPoint();
291            }
292        }
293    
294        /**
295         * Perform action depending on what mode we're in.
296         */
297        @Override public void mouseDragged(MouseEvent e) {
298            if(!Main.map.mapView.isActiveLayerVisible())
299                return;
300    
301            // do not count anything as a drag if it lasts less than 100 milliseconds.
302            if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)
303                return;
304    
305            if (mode == Mode.select) {
306                // Just sit tight and wait for mouse to be released.
307            } else {
308                //move, create new and extrude mode - move the selected segment
309    
310                EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y);
311                EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
312                EastNorth mouseMovement = new EastNorth(mouseEn.getX() - initialMouseEn.getX(), mouseEn.getY() - initialMouseEn.getY());
313    
314                double bestDistance = Double.POSITIVE_INFINITY;
315                EastNorth bestMovement = null;
316                activeMoveDirection = null;
317    
318                //find the best movement direction and vector
319                for (ReferenceSegment direction : possibleMoveDirections) {
320                    EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn);
321                    if (movement == null) {
322                        //if direction parallel to segment.
323                        continue;
324                    }
325    
326                    double distanceFromMouseMovement = movement.distance(mouseMovement);
327                    if (bestDistance > distanceFromMouseMovement) {
328                        bestDistance = distanceFromMouseMovement;
329                        activeMoveDirection = direction;
330                        bestMovement = movement;
331                    }
332                }
333    
334                newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY());
335                newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY());
336    
337                // find out the movement distance, in metres
338                double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en));
339                Main.map.statusLine.setDist(distance);
340                updateStatusLine();
341    
342                Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this);
343    
344                if (mode == Mode.extrude || mode == Mode.create_new) {
345                    //nothing here
346                } else if (mode == Mode.translate) {
347                    //move nodes to new position
348                    if (moveCommand == null) {
349                        //make a new move command
350                        Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>();
351                        nodelist.add(selectedSegment.getFirstNode());
352                        nodelist.add(selectedSegment.getSecondNode());
353                        moveCommand = new MoveCommand(nodelist, bestMovement.getX(), bestMovement.getY());
354                        Main.main.undoRedo.add(moveCommand);
355                    } else {
356                        //reuse existing move command
357                        moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY());
358                    }
359                }
360    
361                Main.map.mapView.repaint();
362            }
363        }
364    
365        /**
366         * Do anything that needs to be done, then switch back to select mode
367         */
368        @Override public void mouseReleased(MouseEvent e) {
369    
370            if(!Main.map.mapView.isActiveLayerVisible())
371                return;
372    
373            if (mode == Mode.select) {
374                // Nothing to be done
375            } else {
376                if (mode == Mode.create_new) {
377                    if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
378                        // crete a new rectangle
379                        Collection<Command> cmds = new LinkedList<Command>();
380                        Node third = new Node(newN2en);
381                        Node fourth = new Node(newN1en);
382                        Way wnew = new Way();
383                        wnew.addNode(selectedSegment.getFirstNode());
384                        wnew.addNode(selectedSegment.getSecondNode());
385                        wnew.addNode(third);
386                        wnew.addNode(fourth);
387                        // ... and close the way
388                        wnew.addNode(selectedSegment.getFirstNode());
389                        // undo support
390                        cmds.add(new AddCommand(third));
391                        cmds.add(new AddCommand(fourth));
392                        cmds.add(new AddCommand(wnew));
393                        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
394                        Main.main.undoRedo.add(c);
395                        getCurrentDataSet().setSelected(wnew);
396                    }
397                } else if (mode == Mode.extrude) {
398                    if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) {
399                        // double click add a new node
400                        // Should maybe do the same as in DrawAction and fetch all nearby segments?
401                        WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
402                        if (ws != null) {
403                            Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY()));
404                            EastNorth A = ws.getFirstNode().getEastNorth();
405                            EastNorth B = ws.getSecondNode().getEastNorth();
406                            n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth()));
407                            Way wnew = new Way(ws.way);
408                            wnew.addNode(ws.lowerIndex+1, n);
409                            SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"),
410                                    new AddCommand(n), new ChangeCommand(ws.way, wnew));
411                            Main.main.undoRedo.add(cmds);
412                        }
413                    }
414                    else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) {
415                        // create extrusion
416    
417                        Collection<Command> cmds = new LinkedList<Command>();
418                        Way wnew = new Way(selectedSegment.way);
419                        int insertionPoint = selectedSegment.lowerIndex + 1;
420    
421                        //find if the new points overlap existing segments (in case of 90 degree angles)
422                        Node prevNode = getPreviousNode(selectedSegment.lowerIndex);
423                        boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en);
424                        boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way);
425    
426                        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
427                            //move existing node
428                            Node n1Old = selectedSegment.getFirstNode();
429                            cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en)));
430                        } else {
431                            //introduce new node
432                            Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en));
433                            wnew.addNode(insertionPoint, n1New);
434                            insertionPoint ++;
435                            cmds.add(new AddCommand(n1New));
436                        }
437    
438                        //find if the new points overlap existing segments (in case of 90 degree angles)
439                        Node nextNode = getNextNode(selectedSegment.lowerIndex + 1);
440                        nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en);
441                        hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way);
442    
443                        if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) {
444                            //move existing node
445                            Node n2Old = selectedSegment.getSecondNode();
446                            cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en)));
447                        } else {
448                            //introduce new node
449                            Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en));
450                            wnew.addNode(insertionPoint, n2New);
451                            insertionPoint ++;
452                            cmds.add(new AddCommand(n2New));
453                        }
454    
455                        //the way was a single segment, close the way
456                        if (wnew.getNodesCount() == 4) {
457                            wnew.addNode(selectedSegment.getFirstNode());
458                        }
459    
460                        cmds.add(new ChangeCommand(selectedSegment.way, wnew));
461                        Command c = new SequenceCommand(tr("Extrude Way"), cmds);
462                        Main.main.undoRedo.add(c);
463                    }
464                } else if (mode == Mode.translate) {
465                    //Commit translate
466                    //the move command is already committed in mouseDragged
467                }
468    
469                boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
470                boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0;
471                boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0;
472                // Switch back into select mode
473                Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this);
474                Main.map.mapView.removeTemporaryLayer(this);
475                selectedSegment = null;
476                moveCommand = null;
477                mode = Mode.select;
478    
479                updateStatusLine();
480                Main.map.mapView.repaint();
481            }
482        }
483    
484        /**
485         * This method tests if a node has other ways apart from the given one.
486         * @param node
487         * @param myWay
488         * @return true of node belongs only to myWay, false if there are more ways.
489         */
490        private boolean hasNodeOtherWays(Node node, Way myWay) {
491            for (OsmPrimitive p : node.getReferrers()) {
492                if (p instanceof Way && p.isUsable() && p != myWay)
493                    return true;
494            }
495            return false;
496        }
497    
498        /***
499         * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position.
500         * @param segmentP1
501         * @param segmentP2
502         * @param targetPos
503         * @return offset amount of P1 and P2.
504         */
505        private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection,
506                EastNorth targetPos) {
507            EastNorth intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos,
508                    new EastNorth(targetPos.getX() + moveDirection.getX(), targetPos.getY() + moveDirection.getY()));
509    
510            if (intersectionPoint == null)
511                return null;
512            else
513                //return distance form base to target position
514                return new EastNorth(targetPos.getX() - intersectionPoint.getX(),
515                        targetPos.getY() - intersectionPoint.getY());
516        }
517    
518        /**
519         * Gets a node from selected way before given index.
520         * @param index  index of current node
521         * @return index of previous node or -1 if there are no nodes there.
522         */
523        private int getPreviousNodeIndex(int index) {
524            if (index > 0)
525                return index - 1;
526            else if (selectedSegment.way.isClosed())
527                return selectedSegment.way.getNodesCount() - 2;
528            else
529                return -1;
530        }
531    
532        /**
533         * Gets a node from selected way before given index.
534         * @param index  index of current node
535         * @return previous node or null if there are no nodes there.
536         */
537        private Node getPreviousNode(int index) {
538            int indexPrev = getPreviousNodeIndex(index);
539            if (indexPrev >= 0)
540                return selectedSegment.way.getNode(indexPrev);
541            else
542                return null;
543        }
544    
545    
546        /**
547         * Gets a node from selected way after given index.
548         * @param index index of current node
549         * @return index of next node or -1 if there are no nodes there.
550         */
551        private int getNextNodeIndex(int index) {
552            int count = selectedSegment.way.getNodesCount();
553            if (index <  count - 1)
554                return index + 1;
555            else if (selectedSegment.way.isClosed())
556                return 1;
557            else
558                return -1;
559        }
560    
561        /**
562         * Gets a node from selected way after given index.
563         * @param index index of current node
564         * @return next node or null if there are no nodes there.
565         */
566        private Node getNextNode(int index) {
567            int indexNext = getNextNodeIndex(index);
568            if (indexNext >= 0)
569                return selectedSegment.way.getNode(indexNext);
570            else
571                return null;
572        }
573    
574        public void paint(Graphics2D g, MapView mv, Bounds box) {
575            if (mode == Mode.select) {
576                // Nothing to do
577            } else {
578                if (newN1en != null) {
579                    Graphics2D g2 = g;
580                    g2.setColor(selectedColor);
581                    g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
582    
583                    Point p1 = mv.getPoint(initialN1en);
584                    Point p2 = mv.getPoint(initialN2en);
585                    Point p3 = mv.getPoint(newN1en);
586                    Point p4 = mv.getPoint(newN2en);
587    
588                    double fac = 1.0 / activeMoveDirection.en.distance(0,0);
589                    // mult by factor to get unit vector.
590                    EastNorth normalUnitVector = new EastNorth(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac);
591    
592                    // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
593                    // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
594                    if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) {
595                        // If not, use a sign-flipped version of the normalUnitVector.
596                        normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
597                    }
598    
599                    //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up.
600                    //This is normally done by MapView.getPoint, but it does not work on vectors.
601                    normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY());
602    
603                    if (mode == Mode.extrude || mode == Mode.create_new) {
604                        // Draw rectangle around new area.
605                        GeneralPath b = new GeneralPath();
606                        b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
607                        b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
608                        b.lineTo(p1.x, p1.y);
609                        g2.draw(b);
610    
611                        if (activeMoveDirection != null) {
612                            // Draw reference way
613                            Point pr1 = mv.getPoint(activeMoveDirection.ws.getFirstNode().getEastNorth());
614                            Point pr2 = mv.getPoint(activeMoveDirection.ws.getSecondNode().getEastNorth());
615                            b = new GeneralPath();
616                            b.moveTo(pr1.x, pr1.y);
617                            b.lineTo(pr2.x, pr2.y);
618                            g2.setColor(helperColor);
619                            g2.setStroke(helperStrokeDash);
620                            g2.draw(b);
621    
622                            // Draw right angle marker on first node position, only when moving at right angle
623                            if (activeMoveDirection.perpendicular) {
624                                // mirror RightAngle marker, so it is inside the extrude
625                                double headingRefWS = activeMoveDirection.ws.getFirstNode().getEastNorth().heading(activeMoveDirection.ws.getSecondNode().getEastNorth());
626                                double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX());
627                                double headingDiff = headingRefWS - headingMoveDir;
628                                if (headingDiff < 0) headingDiff += 2 * Math.PI;
629                                boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5;
630    
631                                // EastNorth units per pixel
632                                double factor = 1.0/g2.getTransform().getScaleX();
633                                double raoffsetx = 8.0*factor*normalUnitVector.getX();
634                                double raoffsety = 8.0*factor*normalUnitVector.getY();
635    
636                                Point2D ra1 = new Point2D.Double(pr1.x + raoffsetx, pr1.y+raoffsety);
637                                Point2D ra3 = new Point2D.Double(pr1.x - raoffsety*(mirrorRA ? -1 : 1), pr1.y + raoffsetx*(mirrorRA ? -1 : 1));
638                                Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*(mirrorRA ? -1 : 1), ra1.getY() + raoffsetx*(mirrorRA ? -1 : 1));
639                                GeneralPath ra = new GeneralPath();
640                                ra.moveTo((float)ra1.getX(), (float)ra1.getY());
641                                ra.lineTo((float)ra2.getX(), (float)ra2.getY());
642                                ra.lineTo((float)ra3.getX(), (float)ra3.getY());
643                                g2.setStroke(helperStrokeRA);
644                                g2.draw(ra);
645                            }
646                        }
647                    } else if (mode == Mode.translate) {
648                        // Highlight the new and old segments.
649                        Line2D newline = new Line2D.Double(p3, p4);
650                        g2.draw(newline);
651                        g2.setStroke(new BasicStroke(1));
652                        Line2D oldline = new Line2D.Double(p1, p2);
653                        g2.draw(oldline);
654    
655                        if (activeMoveDirection != null) {
656    
657                            // Draw a guideline along the normal.
658                            Line2D normline;
659                            Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
660                            normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2);
661                            g2.draw(normline);
662    
663                            // Draw right angle marker on initial position, only when moving at right angle
664                            if (activeMoveDirection.perpendicular) {
665                                // EastNorth units per pixel
666                                double factor = 1.0/g2.getTransform().getScaleX();
667    
668                                double raoffsetx = 8.0*factor*normalUnitVector.getX();
669                                double raoffsety = 8.0*factor*normalUnitVector.getY();
670                                Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety);
671                                Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx);
672                                Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx);
673                                GeneralPath ra = new GeneralPath();
674                                ra.moveTo((float)ra1.getX(), (float)ra1.getY());
675                                ra.lineTo((float)ra2.getX(), (float)ra2.getY());
676                                ra.lineTo((float)ra3.getX(), (float)ra3.getY());
677                                g2.draw(ra);
678                            }
679                        }
680                    }
681                }
682            }
683        }
684    
685        /**
686         * Create a new Line that extends off the edge of the viewport in one direction
687         * @param start The start point of the line
688         * @param unitvector A unit vector denoting the direction of the line
689         * @param g the Graphics2D object  it will be used on
690         */
691        static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
692            Rectangle bounds = g.getDeviceConfiguration().getBounds();
693            try {
694                AffineTransform invtrans = g.getTransform().createInverse();
695                Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
696                Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
697    
698                // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
699                // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
700                // This can be used as a safe length of line to generate which will always go off-viewport.
701                double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
702    
703                return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
704            }
705            catch (NoninvertibleTransformException e) {
706                return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
707            }
708        }
709    }