Ticket #67: parallel-way-action.0.2.patch

File parallel-way-action.0.2.patch, 25.9 KB (added by olejorgenb, 10 years ago)

Select ways from mode, simple snap, copy tags, works on multiple (simple connected) ways

  • new file src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java

    diff --git a/images/mapmode/parallel.png b/images/mapmode/parallel.png
    new file mode 100644
    index 0000000..a02c6fc
    Binary files /dev/null and b/images/mapmode/parallel.png differ
    diff --git a/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java b/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java
    new file mode 100644
    index 0000000..6aa149e
    - +  
     1// License: GPL. Copyright 2007 by Immanuel Scholz and others
     2
     3package org.openstreetmap.josm.actions.mapmode;
     4
     5import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
     6import static org.openstreetmap.josm.tools.I18n.tr;
     7
     8import java.awt.AWTEvent;
     9import java.awt.Cursor;
     10import java.awt.Point;
     11import java.awt.Toolkit;
     12import java.awt.event.AWTEventListener;
     13import java.awt.event.ActionEvent;
     14import java.awt.event.InputEvent;
     15import java.awt.event.KeyEvent;
     16import java.awt.event.MouseEvent;
     17import java.util.ArrayList;
     18import java.util.Collections;
     19import java.util.HashMap;
     20import java.util.List;
     21
     22import org.openstreetmap.josm.Main;
     23import org.openstreetmap.josm.actions.CombineWayAction;
     24import org.openstreetmap.josm.command.AddCommand;
     25import org.openstreetmap.josm.command.Command;
     26import org.openstreetmap.josm.command.SequenceCommand;
     27import org.openstreetmap.josm.data.coor.EastNorth;
     28import org.openstreetmap.josm.data.osm.DataSet;
     29import org.openstreetmap.josm.data.osm.Node;
     30import org.openstreetmap.josm.data.osm.OsmPrimitive;
     31import org.openstreetmap.josm.data.osm.Way;
     32import org.openstreetmap.josm.data.osm.WaySegment;
     33import org.openstreetmap.josm.gui.MapFrame;
     34import org.openstreetmap.josm.gui.MapView;
     35import org.openstreetmap.josm.gui.layer.Layer;
     36import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     37import org.openstreetmap.josm.tools.Geometry;
     38import org.openstreetmap.josm.tools.Shortcut;
     39
     40//// TODO: (list below)
     41/*
     42 * 1. Use selected nodes as split points for the selected ways.
     43 *
     44 * The ways containing the selected nodes will be split and only the "inner"
     45 * parts will be copied
     46 *
     47 * 2. Enter exact offset
     48 *
     49 * 3. Improve snapping
     50 *
     51 * Need at least a setting for step length
     52 *
     53 * 4. Visual cues? Highlight source path, draw offset line, etc?
     54 *
     55 * 5. Cursors
     56 *
     57 * 6. (long term) Parallelize and adjust offsets of existing ways
     58 */
     59
     60/**
     61 * MapMode for making parallel ways.
     62 *
     63 * All calculations are done in projected coordinates.
     64 *
     65 * @author Ole Jørgen Brønner (olejorgenb)
     66 */
     67public class ParallelWayAction extends MapMode implements AWTEventListener {
     68    // omg..
     69    public void dumpMouseEvent(MouseEvent e) {
     70        //        System.out.println(e.paramString());
     71        //        System.out.print("e.getButton() = ");
     72        //        switch (e.getButton()) {
     73        //        case MouseEvent.BUTTON1:
     74        //            System.out.println("BUTTON1");
     75        //            break;
     76        //        case MouseEvent.BUTTON2:
     77        //            System.out.println("BUTTON2");
     78        //            break;
     79        //        case MouseEvent.BUTTON3:
     80        //            System.out.println("BUTTON3");
     81        //            break;
     82        //        case MouseEvent.NOBUTTON:
     83        //            System.out.println("NOBUTTON");
     84        //            break;
     85        //        default:
     86        //            System.out.println(e.getButton());
     87        //            break;
     88        //        }
     89    }
     90
     91    private enum Mode {
     92        dragging, normal
     93    }
     94
     95    private Mode mode;
     96
     97    private boolean snap;
     98    private double snapThreshold;
     99
     100    private int initialMoveDelay;
     101    //    private int initialMoveThreshold;
     102
     103    private final MapView mv;
     104
     105    ParallelWayAction.MouseTracker mouseTracker = new MouseTracker();
     106
     107    private WaySegment referenceSegment;
     108    private ParallelWays pWays;
     109
     110    private boolean ctrl;
     111    private boolean alt;
     112    private boolean shift;
     113
     114    public ParallelWayAction(MapFrame mapFrame) {
     115        super(tr("Parallel"), "parallel", tr("Makes a paralell copy of the selected way(s)"), Shortcut
     116                .registerShortcut("mapmode:parallel", tr("Mode: {0}", tr("Parallel")), KeyEvent.VK_P,
     117                        Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), mapFrame, Cursor
     118                        .getPredefinedCursor(Cursor.MOVE_CURSOR));
     119        putValue("help", ht("/Action/Parallel"));
     120        mv = mapFrame.mapView;
     121
     122        mouseTracker = new MouseTracker();
     123    }
     124
     125    @Override
     126    public String getModeHelpText() {
     127        // TODO: add more detailed feedback based on modifier state.
     128        switch (mode) {
     129        case normal:
     130            return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt for tagless)");
     131        case dragging:
     132            return tr("Hold Ctrl to snap to whole meters");
     133        }
     134        return ""; // impossible ..
     135    }
     136
     137    @Override
     138    public void enterMode() {
     139        setMode(Mode.normal);
     140        pWays = null;
     141        super.enterMode();
     142        mv.addMouseListener(this);
     143        mv.addMouseMotionListener(this);
     144        // FIXME: What do do with default values? Not that they probably change
     145        //        that often, but having them multiple places is still a bit ickky.
     146        initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", 200);
     147        //        initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5);
     148        snapThreshold = Main.pref.getDouble("edit.make-parallel-way-action.snap-threshold", 0.35);
     149
     150        //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
     151        try {
     152            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
     153        } catch (SecurityException ex) {
     154        }
     155
     156    }
     157
     158    @Override
     159    public void exitMode() {
     160        super.exitMode();
     161        mv.removeMouseListener(this);
     162        mv.removeMouseMotionListener(this);
     163        Main.map.statusLine.setDist(-1);
     164        Main.map.statusLine.repaint();
     165        try {
     166            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
     167        } catch (SecurityException ex) {
     168        }
     169    }
     170
     171    @Override
     172    public boolean layerIsSupported(Layer layer) {
     173        return layer instanceof OsmDataLayer;
     174    }
     175
     176    @Override
     177    public void eventDispatched(AWTEvent e) {
     178        if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
     179            return;
     180
     181        // Should only get InputEvents due to the mask in enterMode
     182        updateKeyModifiers((InputEvent) e);
     183    }
     184
     185    private void updateKeyModifiers(InputEvent e) {
     186        ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
     187        alt = (e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0;
     188        shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
     189    }
     190
     191    private void updateCursor(Mode mode) {
     192
     193    }
     194
     195    private void setMode(Mode mode) {
     196        this.mode = mode;
     197        updateCursor(this.mode);
     198        updateStatusLine();
     199    }
     200
     201    private void onModifiersChanged() {
     202        updateCursor(mode);
     203    }
     204
     205    @Override
     206    public void mousePressed(MouseEvent e) {
     207        mouseTracker.registrerPressed(e);
     208        dumpMouseEvent(e);
     209        //        System.out.println("MousePressed");
     210        if (e.getButton() != MouseEvent.BUTTON1)
     211            return;
     212
     213        if (!mv.isActiveLayerVisible())
     214            return;
     215        if (!mv.isActiveLayerDrawable())
     216            return;
     217        if (!(Boolean) this.getValue("active"))
     218            return;
     219    }
     220
     221    @Override
     222    public void mouseClicked(MouseEvent e) {
     223        //        System.out.println("MouseClicked");
     224    }
     225
     226    @Override
     227    public void mouseReleased(MouseEvent e) {
     228        mouseTracker.registrerReleased(e);
     229        dumpMouseEvent(e);
     230        //        System.out.println("MouseReleased");
     231        if (e.getButton() != MouseEvent.BUTTON1)
     232            return;
     233
     234        updateKeyModifiers(e);
     235
     236        pWays = null;
     237        setMode(Mode.normal);
     238
     239        if (!(mouseTracker.buttonState[MouseEvent.BUTTON1].hasBeenDragged)) {
     240            assert (pWays == null);
     241            // use point from press or click event? (or are these always the same)
     242            Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive.isSelectablePredicate);
     243            if (nearestWay == null) {
     244                if (!shift && !ctrl) {
     245                    getCurrentDataSet().clearSelection();
     246                }
     247                return;
     248            }
     249            boolean isSelected = nearestWay.isSelected();
     250            if (shift && !ctrl && !alt) {
     251                if (!isSelected) {
     252                    getCurrentDataSet().addSelected(nearestWay);
     253                }
     254            } else if (ctrl && !shift && !alt) {
     255                if (isSelected) {
     256                    getCurrentDataSet().clearSelection(nearestWay);
     257                } else {
     258                    getCurrentDataSet().addSelected(nearestWay);
     259                }
     260            } else if (!ctrl && !shift && !alt) {
     261                getCurrentDataSet().setSelected(nearestWay);
     262            } // else -> invalid modifier combination
     263        }
     264    }
     265
     266    @Override
     267    public void mouseDragged(MouseEvent e) {
     268        mouseTracker.registrerDragged(e);
     269        dumpMouseEvent(e);
     270        //        System.out.println("MouseDragged");
     271        if (!mouseTracker.buttonState[MouseEvent.BUTTON1].isPressed)
     272            return; // WTF.. the events passed here does not have button info?
     273
     274        updateKeyModifiers(e);
     275
     276        if ((System.currentTimeMillis() - mouseTracker.buttonState[MouseEvent.BUTTON1].pressedTime) < initialMoveDelay)
     277            return;
     278
     279        if (!shift && !ctrl) {
     280            snap = false;
     281        } else if (!shift && ctrl) {
     282            snap = true;
     283        }
     284
     285        Point p = e.getPoint();
     286        if (pWays == null) {
     287            boolean copyTags = true;
     288            // This is the first drag event
     289            if (!shift && alt) {
     290                copyTags = false;
     291            } else if (!shift && !alt) {
     292                copyTags = true;
     293            } else
     294                return; // invalid modifier combinations
     295            // Important to use mouse position from the press, since the drag
     296            // event can come quite late
     297            if (!initParallelWays(mouseTracker.buttonState[MouseEvent.BUTTON1].pressedPos, copyTags))
     298                return;
     299            setMode(Mode.dragging);
     300        }
     301
     302        //// Calculate distance to the reference line
     303        EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
     304        EastNorth enOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
     305                referenceSegment.getSecondNode().getEastNorth(), enp);
     306        double d = enp.distance(enOnRefLine);
     307        boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(),
     308                referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
     309
     310        if (snap) {
     311            // TODO: Very simple snapping
     312            // - Snap steps and/or threshold relative to the distance?
     313            long closestWholeUnit = Math.round(d);
     314            if (Math.abs(closestWholeUnit - d) < snapThreshold) {
     315                d = closestWholeUnit;
     316            } else {
     317                d = closestWholeUnit + Math.signum(closestWholeUnit - d) * -0.5;
     318            }
     319        }
     320        if (toTheRight) {
     321            d = -d;
     322        }
     323        pWays.changeOffset(d);
     324
     325        Main.map.statusLine.setDist(Math.abs(d));
     326        Main.map.statusLine.repaint();
     327        mv.repaint();
     328    }
     329
     330    // TODO: rename
     331    private boolean initParallelWays(Point p, boolean copyTags) {
     332        referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true);
     333        if (referenceSegment == null)
     334            return false;
     335
     336        // The collection returned is very inefficient so we collect it in an ArrayList
     337        // Not sure if the list is iterated multiple times any more...
     338        List<Way> selectedWays = new ArrayList<Way>(getCurrentDataSet().getSelectedWays());
     339        if (!selectedWays.contains(referenceSegment.way)) {
     340            getCurrentDataSet().setSelected(referenceSegment.way);
     341            selectedWays.clear();
     342            selectedWays.add(referenceSegment.way);
     343        }
     344
     345        try {
     346            pWays = new ParallelWays(selectedWays, copyTags, selectedWays.indexOf(referenceSegment.way));
     347            pWays.commit(null);
     348            getCurrentDataSet().setSelected(pWays.ways);
     349            return true;
     350        } catch (IllegalArgumentException e) {
     351            System.err.println(e);
     352            pWays = null;
     353            return false;
     354        }
     355    }
     356
     357    // TODO: 'ParallelPath' better name?
     358    static final class ParallelWays {
     359        private List<Way> ways;
     360        private List<Node> sortedNodes;
     361
     362        private int nodeCount;
     363
     364        private EastNorth[] pts;
     365        private EastNorth[] normals;
     366
     367        public ParallelWays(List<Way> sourceWays, boolean copyTags, int refWayIndex) {
     368            // Possible/sensible to use PrimetiveDeepCopy here?
     369
     370            //// Make a deep copy of the ways, keeping the copied ways connected
     371            HashMap<Node, Node> splitNodeMap = new HashMap<Node, Node>(sourceWays.size());
     372            for (Way w : sourceWays) {
     373                if (!splitNodeMap.containsKey(w.firstNode())) {
     374                    splitNodeMap.put(w.firstNode(), copyNode(w.firstNode(), copyTags));
     375                }
     376                if (!splitNodeMap.containsKey(w.lastNode())) {
     377                    splitNodeMap.put(w.lastNode(), copyNode(w.lastNode(), copyTags));
     378                }
     379            }
     380            ways = new ArrayList<Way>(sourceWays.size());
     381            for (Way w : sourceWays) {
     382                Way wCopy = new Way();
     383                wCopy.addNode(splitNodeMap.get(w.firstNode()));
     384                for (int i = 1; i < w.getNodesCount() - 1; i++) {
     385                    wCopy.addNode(copyNode(w.getNode(i), copyTags));
     386                }
     387                wCopy.addNode(splitNodeMap.get(w.lastNode()));
     388                if (copyTags) {
     389                    wCopy.setKeys(w.getKeys());
     390                }
     391                ways.add(wCopy);
     392            }
     393            sourceWays = null; // Ensure that we only use the copies from now
     394
     395            //// Find a linear ordering of the nodes. Fail if there isn't one.
     396            CombineWayAction.NodeGraph nodeGraph = CombineWayAction.NodeGraph.createUndirectedGraphFromNodeWays(ways);
     397            sortedNodes = nodeGraph.buildSpanningPath();
     398            if (sortedNodes == null)
     399                throw new IllegalArgumentException("Ways must have spanning path"); // Create a dedicated exception?
     400
     401            //// Ugly method of ensuring that the offset isn't inverted. I'm sure there is a better and more elegant way, but I'm starting to get sleepy, so I do this for now.
     402            {
     403                Way refWay = ways.get(refWayIndex);
     404                boolean refWayReversed = false;
     405                if (isClosedPath()) { // Nodes occur more than once in the list
     406                    if (refWay.firstNode() == sortedNodes.get(0) && refWay.lastNode() == sortedNodes.get(0)) {
     407                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1);
     408                    } else if (refWay.lastNode() == sortedNodes.get(0)) {
     409                        refWayReversed =
     410                            sortedNodes.get(sortedNodes.size() - 1) != refWay.getNode(refWay.getNodesCount() - 1);
     411                    } else if (refWay.firstNode() == sortedNodes.get(0)) {
     412                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1);
     413                    } else {
     414                        refWayReversed =
     415                            sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode());
     416                    }
     417
     418                } else {
     419                    refWayReversed = sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode());
     420                }
     421                if (refWayReversed) {
     422                    Collections.reverse(sortedNodes); // need to keep the orientation of the reference way.
     423                    System.err.println("reversed!");
     424                }
     425            }
     426
     427            //// Initialize the required parameters. (segment normals, etc.)
     428            nodeCount = sortedNodes.size();
     429            pts = new EastNorth[nodeCount];
     430            normals = new EastNorth[nodeCount - 1];
     431            int i = 0;
     432            for (Node n : sortedNodes) {
     433                EastNorth t = n.getEastNorth();
     434                pts[i] = t;
     435                i++;
     436            }
     437            for (i = 0; i < nodeCount - 1; i++) {
     438                double dx = pts[i + 1].getX() - pts[i].getX();
     439                double dy = pts[i + 1].getY() - pts[i].getY();
     440                double len = Math.sqrt(dx * dx + dy * dy);
     441                normals[i] = new EastNorth(-dy / len, dx / len);
     442            }
     443        }
     444
     445        public boolean isClosedPath() {
     446            return sortedNodes.get(0) == sortedNodes.get(sortedNodes.size() - 1);
     447        }
     448
     449        public void changeOffset(double d) {
     450            //// This is the core algorithm:
     451            /* 1. Calculate a parallel line, offset by 'd', to each segment in
     452             *    the path
     453             * 2. Find the intersection of lines belonging to neighboring
     454             *    segments. These become the new node positions
     455             * 3. Do some special casing for closed paths
     456             *
     457             * Simple and probably not even close to optimal performance-vise
     458             */
     459
     460            EastNorth[] ppts = new EastNorth[nodeCount];
     461
     462            EastNorth prevA = add(pts[0], mul(normals[0], d));
     463            EastNorth prevB = add(pts[1], mul(normals[0], d));
     464            for (int i = 1; i < nodeCount - 1; i++) {
     465                EastNorth A = add(pts[i], mul(normals[i], d));
     466                EastNorth B = add(pts[i + 1], mul(normals[i], d));
     467                if (Geometry.segmentsParallel(A, B, prevA, prevB)) {
     468                    ppts[i] = A;
     469                } else {
     470                    ppts[i] = Geometry.getLineLineIntersection(A, B, prevA, prevB);
     471                }
     472                prevA = A;
     473                prevB = B;
     474            }
     475            if (isClosedPath()) {
     476                EastNorth A = add(pts[0], mul(normals[0], d));
     477                EastNorth B = add(pts[1], mul(normals[0], d));
     478                if (Geometry.segmentsParallel(A, B, prevA, prevB)) {
     479                    ppts[0] = A;
     480                } else {
     481                    ppts[0] = Geometry.getLineLineIntersection(A, B, prevA, prevB);
     482                }
     483                ppts[nodeCount - 1] = ppts[0];
     484            } else {
     485                ppts[0] = add(pts[0], mul(normals[0], d));
     486                ppts[nodeCount - 1] = add(pts[nodeCount - 1], mul(normals[nodeCount - 2], d));
     487            }
     488
     489            for (int i = 0; i < nodeCount; i++) {
     490                sortedNodes.get(i).setEastNorth(ppts[i]);
     491            }
     492        }
     493
     494        // Draw helper lines instead like DrawAction ExtrudeAction?
     495        public void commit(DataSet ds) {
     496            SequenceCommand undoCommand = new SequenceCommand("Make parallel way(s)", makeAddWayAndNodesCommandList());
     497            Main.main.undoRedo.add(undoCommand);
     498        }
     499
     500        private List<Command> makeAddWayAndNodesCommandList() {
     501            ArrayList<Command> commands = new ArrayList<Command>(sortedNodes.size() + ways.size());
     502            for (int i = 0; i < sortedNodes.size() - 1; i++) {
     503                commands.add(new AddCommand(sortedNodes.get(i)));
     504            }
     505            if (!isClosedPath()) {
     506                commands.add(new AddCommand(sortedNodes.get(sortedNodes.size() - 1)));
     507            }
     508            for (Way w : ways) {
     509                commands.add(new AddCommand(w));
     510            }
     511            return commands;
     512        }
     513
     514        static private Node copyNode(Node source, boolean copyTags) {
     515            if (copyTags)
     516                return new Node(source, true);
     517            else {
     518                Node n = new Node();
     519                n.setCoor(source.getCoor());
     520                return n;
     521            }
     522        }
     523
     524        // We need either a dedicated vector type, or operations such as these
     525        // added to EastNorth...
     526        static private EastNorth mul(EastNorth en, double f) {
     527            return new EastNorth(en.getX() * f, en.getY() * f);
     528        }
     529
     530        static private EastNorth add(EastNorth a, EastNorth b) {
     531            return new EastNorth(a.east() + b.east(), a.north() + b.north());
     532        }
     533    }
     534
     535    // TODO: Finish me. (or maybe having a state-tracker object for this is over-complicating things?)
     536    // Dunno.. this swing stuff is a mess..
     537    static final class MouseTracker {
     538        public static final class ButtonState {
     539            public long pressedTime = -1;
     540            public Point pressedPos = null;
     541            public long releasedTime = -1;
     542            public Point releasedPos = null;
     543            public boolean hasBeenDragged = false;
     544            public boolean isPressed = false;
     545            public Point lastPos = null;
     546        }
     547
     548        public MouseTracker() {
     549            for (int i = 0; i < buttonCount; i++) {
     550                buttonState[i] = new ButtonState();
     551            }
     552        }
     553
     554        public ButtonState[] buttonState = new ButtonState[4];
     555        public final int buttonCount = 4;
     556
     557        public MouseEvent lastPressedEvent;
     558        public MouseEvent lastMoveEvent;
     559        public MouseEvent lastDragEvent;
     560        public MouseEvent lastReleaseEvent;
     561
     562        public ButtonState getButtonState(MouseEvent e) {
     563            return buttonState[e.getButton()];
     564        }
     565
     566        public void registrerPressed(MouseEvent e) {
     567            ButtonState b = getButtonState(e);
     568            b.pressedTime = System.currentTimeMillis();
     569            b.pressedPos = e.getPoint();
     570            b.hasBeenDragged = false;
     571            b.lastPos = e.getPoint();
     572            b.isPressed = true;
     573
     574            lastPressedEvent = e;
     575        }
     576
     577        public void registrerReleased(MouseEvent e) {
     578            ButtonState b = getButtonState(e);
     579            b.releasedTime = System.currentTimeMillis();
     580            b.releasedPos = e.getPoint();
     581            b.isPressed = false;
     582            b.lastPos = e.getPoint();
     583
     584            lastReleaseEvent = e;
     585        }
     586
     587        public void registrerMoved(MouseEvent e) {
     588            // TODO:
     589        }
     590
     591        public void registrerDragged(MouseEvent e) {
     592            // no button info
     593            for (ButtonState b : buttonState) {
     594                if (b.isPressed) {
     595                    b.hasBeenDragged = true;
     596                    b.lastPos = e.getPoint();
     597                }
     598            }
     599
     600            lastDragEvent = e;
     601        }
     602
     603        public void clear() {
     604        }
     605    }
     606}
  • src/org/openstreetmap/josm/gui/MapFrame.java

    diff --git a/src/org/openstreetmap/josm/gui/MapFrame.java b/src/org/openstreetmap/josm/gui/MapFrame.java
    index 96bddae..830c567 100644
    a b import org.openstreetmap.josm.actions.mapmode.DeleteAction; 
    4040import org.openstreetmap.josm.actions.mapmode.DrawAction;
    4141import org.openstreetmap.josm.actions.mapmode.ExtrudeAction;
    4242import org.openstreetmap.josm.actions.mapmode.MapMode;
     43import org.openstreetmap.josm.actions.mapmode.ParallelWayAction;
    4344import org.openstreetmap.josm.actions.mapmode.SelectAction;
    4445import org.openstreetmap.josm.actions.mapmode.ZoomAction;
    4546import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
    public class MapFrame extends JPanel implements Destroyable, LayerChangeListener 
    129130        addMapMode(new IconToggleButton(new SelectAction(this)));
    130131        addMapMode(new IconToggleButton(new DrawAction(this)));
    131132        addMapMode(new IconToggleButton(new ExtrudeAction(this)));
     133        addMapMode(new IconToggleButton(new ParallelWayAction(this)));
    132134        addMapMode(new IconToggleButton(new ZoomAction(this)));
    133135        addMapMode(new IconToggleButton(new DeleteAction(this)));
    134136
  • src/org/openstreetmap/josm/tools/Geometry.java

    diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
    index ade8359..9930fbb 100644
    a b public class Geometry { 
    332332        else
    333333            return new EastNorth(segmentP1.getX() + ldx * offset, segmentP1.getY() + ldy * offset);
    334334    }
     335    public static EastNorth closestPointToLine(EastNorth lineP1, EastNorth lineP2, EastNorth point) {
     336        double ldx = lineP2.getX() - lineP1.getX();
     337        double ldy = lineP2.getY() - lineP1.getY();
     338
     339        if (ldx == 0 && ldy == 0) //segment zero length
     340            return lineP1;
     341
     342        double pdx = point.getX() - lineP1.getX();
     343        double pdy = point.getY() - lineP1.getY();
     344
     345        double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy);
     346        return new EastNorth(lineP1.getX() + ldx * offset, lineP1.getY() + ldy * offset);
     347    }
    335348
    336349    /**
    337350     * This method tests if secondNode is clockwise to first node.
    public class Geometry { 
    457470
    458471        return inside;
    459472    }
    460    
     473
    461474    /**
    462475     * returns area of a closed way in square meters
    463476     * (approximate(?), but should be OK for small areas)
    public class Geometry { 
    477490        }
    478491        return Math.abs(area/2);
    479492    }
    480    
     493
    481494    protected static double calcX(Node p1){
    482495        double lat1, lon1, lat2, lon2;
    483496        double dlon, dlat;
    public class Geometry { 
    494507        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    495508        return 6367000 * c;
    496509    }
    497    
     510
    498511    protected static double calcY(Node p1){
    499512        double lat1, lon1, lat2, lon2;
    500513        double dlon, dlat;