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