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
--- /dev/null
+++ b/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java
@@ -0,0 +1,606 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+
+package org.openstreetmap.josm.actions.mapmode;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.AWTEvent;
+import java.awt.Cursor;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.CombineWayAction;
+import org.openstreetmap.josm.command.AddCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.Geometry;
+import org.openstreetmap.josm.tools.Shortcut;
+
+//// TODO: (list below)
+/*
+ * 1. Use selected nodes as split points for the selected ways.
+ * 
+ * The ways containing the selected nodes will be split and only the "inner"
+ * parts will be copied
+ * 
+ * 2. Enter exact offset
+ * 
+ * 3. Improve snapping
+ * 
+ * Need at least a setting for step length
+ * 
+ * 4. Visual cues? Highlight source path, draw offset line, etc?
+ * 
+ * 5. Cursors
+ * 
+ * 6. (long term) Parallelize and adjust offsets of existing ways
+ */
+
+/**
+ * MapMode for making parallel ways.
+ * 
+ * All calculations are done in projected coordinates.
+ * 
+ * @author Ole Jørgen Brønner (olejorgenb)
+ */
+public class ParallelWayAction extends MapMode implements AWTEventListener {
+    // omg..
+    public void dumpMouseEvent(MouseEvent e) {
+        //        System.out.println(e.paramString());
+        //        System.out.print("e.getButton() = ");
+        //        switch (e.getButton()) {
+        //        case MouseEvent.BUTTON1:
+        //            System.out.println("BUTTON1");
+        //            break;
+        //        case MouseEvent.BUTTON2:
+        //            System.out.println("BUTTON2");
+        //            break;
+        //        case MouseEvent.BUTTON3:
+        //            System.out.println("BUTTON3");
+        //            break;
+        //        case MouseEvent.NOBUTTON:
+        //            System.out.println("NOBUTTON");
+        //            break;
+        //        default:
+        //            System.out.println(e.getButton());
+        //            break;
+        //        }
+    }
+
+    private enum Mode {
+        dragging, normal
+    }
+
+    private Mode mode;
+
+    private boolean snap;
+    private double snapThreshold;
+
+    private int initialMoveDelay;
+    //    private int initialMoveThreshold;
+
+    private final MapView mv;
+
+    ParallelWayAction.MouseTracker mouseTracker = new MouseTracker();
+
+    private WaySegment referenceSegment;
+    private ParallelWays pWays;
+
+    private boolean ctrl;
+    private boolean alt;
+    private boolean shift;
+
+    public ParallelWayAction(MapFrame mapFrame) {
+        super(tr("Parallel"), "parallel", tr("Makes a paralell copy of the selected way(s)"), Shortcut
+                .registerShortcut("mapmode:parallel", tr("Mode: {0}", tr("Parallel")), KeyEvent.VK_P,
+                        Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), mapFrame, Cursor
+                        .getPredefinedCursor(Cursor.MOVE_CURSOR));
+        putValue("help", ht("/Action/Parallel"));
+        mv = mapFrame.mapView;
+
+        mouseTracker = new MouseTracker();
+    }
+
+    @Override
+    public String getModeHelpText() {
+        // TODO: add more detailed feedback based on modifier state.
+        switch (mode) {
+        case normal:
+            return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt for tagless)");
+        case dragging:
+            return tr("Hold Ctrl to snap to whole meters");
+        }
+        return ""; // impossible ..
+    }
+
+    @Override
+    public void enterMode() {
+        setMode(Mode.normal);
+        pWays = null;
+        super.enterMode();
+        mv.addMouseListener(this);
+        mv.addMouseMotionListener(this);
+        // FIXME: What do do with default values? Not that they probably change
+        //        that often, but having them multiple places is still a bit ickky.
+        initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", 200);
+        //        initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5);
+        snapThreshold = Main.pref.getDouble("edit.make-parallel-way-action.snap-threshold", 0.35);
+
+        //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
+        try {
+            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
+        } catch (SecurityException ex) {
+        }
+
+    }
+
+    @Override
+    public void exitMode() {
+        super.exitMode();
+        mv.removeMouseListener(this);
+        mv.removeMouseMotionListener(this);
+        Main.map.statusLine.setDist(-1);
+        Main.map.statusLine.repaint();
+        try {
+            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
+        } catch (SecurityException ex) {
+        }
+    }
+
+    @Override
+    public boolean layerIsSupported(Layer layer) {
+        return layer instanceof OsmDataLayer;
+    }
+
+    @Override
+    public void eventDispatched(AWTEvent e) {
+        if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
+            return;
+
+        // Should only get InputEvents due to the mask in enterMode
+        updateKeyModifiers((InputEvent) e);
+    }
+
+    private void updateKeyModifiers(InputEvent e) {
+        ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
+        alt = (e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0;
+        shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
+    }
+
+    private void updateCursor(Mode mode) {
+
+    }
+
+    private void setMode(Mode mode) {
+        this.mode = mode;
+        updateCursor(this.mode);
+        updateStatusLine();
+    }
+
+    private void onModifiersChanged() {
+        updateCursor(mode);
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+        mouseTracker.registrerPressed(e);
+        dumpMouseEvent(e);
+        //        System.out.println("MousePressed");
+        if (e.getButton() != MouseEvent.BUTTON1)
+            return;
+
+        if (!mv.isActiveLayerVisible())
+            return;
+        if (!mv.isActiveLayerDrawable())
+            return;
+        if (!(Boolean) this.getValue("active"))
+            return;
+    }
+
+    @Override
+    public void mouseClicked(MouseEvent e) {
+        //        System.out.println("MouseClicked");
+    }
+
+    @Override
+    public void mouseReleased(MouseEvent e) {
+        mouseTracker.registrerReleased(e);
+        dumpMouseEvent(e);
+        //        System.out.println("MouseReleased");
+        if (e.getButton() != MouseEvent.BUTTON1)
+            return;
+
+        updateKeyModifiers(e);
+
+        pWays = null;
+        setMode(Mode.normal);
+
+        if (!(mouseTracker.buttonState[MouseEvent.BUTTON1].hasBeenDragged)) {
+            assert (pWays == null);
+            // use point from press or click event? (or are these always the same)
+            Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive.isSelectablePredicate);
+            if (nearestWay == null) {
+                if (!shift && !ctrl) {
+                    getCurrentDataSet().clearSelection();
+                }
+                return;
+            }
+            boolean isSelected = nearestWay.isSelected();
+            if (shift && !ctrl && !alt) {
+                if (!isSelected) {
+                    getCurrentDataSet().addSelected(nearestWay);
+                }
+            } else if (ctrl && !shift && !alt) {
+                if (isSelected) {
+                    getCurrentDataSet().clearSelection(nearestWay);
+                } else {
+                    getCurrentDataSet().addSelected(nearestWay);
+                }
+            } else if (!ctrl && !shift && !alt) {
+                getCurrentDataSet().setSelected(nearestWay);
+            } // else -> invalid modifier combination
+        }
+    }
+
+    @Override
+    public void mouseDragged(MouseEvent e) {
+        mouseTracker.registrerDragged(e);
+        dumpMouseEvent(e);
+        //        System.out.println("MouseDragged");
+        if (!mouseTracker.buttonState[MouseEvent.BUTTON1].isPressed)
+            return; // WTF.. the events passed here does not have button info?
+
+        updateKeyModifiers(e);
+
+        if ((System.currentTimeMillis() - mouseTracker.buttonState[MouseEvent.BUTTON1].pressedTime) < initialMoveDelay)
+            return;
+
+        if (!shift && !ctrl) {
+            snap = false;
+        } else if (!shift && ctrl) {
+            snap = true;
+        }
+
+        Point p = e.getPoint();
+        if (pWays == null) {
+            boolean copyTags = true;
+            // This is the first drag event
+            if (!shift && alt) {
+                copyTags = false;
+            } else if (!shift && !alt) {
+                copyTags = true;
+            } else
+                return; // invalid modifier combinations
+            // Important to use mouse position from the press, since the drag
+            // event can come quite late
+            if (!initParallelWays(mouseTracker.buttonState[MouseEvent.BUTTON1].pressedPos, copyTags))
+                return;
+            setMode(Mode.dragging);
+        }
+
+        //// Calculate distance to the reference line
+        EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
+        EastNorth enOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
+                referenceSegment.getSecondNode().getEastNorth(), enp);
+        double d = enp.distance(enOnRefLine);
+        boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(),
+                referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
+
+        if (snap) {
+            // TODO: Very simple snapping
+            // - Snap steps and/or threshold relative to the distance?
+            long closestWholeUnit = Math.round(d);
+            if (Math.abs(closestWholeUnit - d) < snapThreshold) {
+                d = closestWholeUnit;
+            } else {
+                d = closestWholeUnit + Math.signum(closestWholeUnit - d) * -0.5;
+            }
+        }
+        if (toTheRight) {
+            d = -d;
+        }
+        pWays.changeOffset(d);
+
+        Main.map.statusLine.setDist(Math.abs(d));
+        Main.map.statusLine.repaint();
+        mv.repaint();
+    }
+
+    // TODO: rename
+    private boolean initParallelWays(Point p, boolean copyTags) {
+        referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true);
+        if (referenceSegment == null)
+            return false;
+
+        // The collection returned is very inefficient so we collect it in an ArrayList
+        // Not sure if the list is iterated multiple times any more...
+        List<Way> selectedWays = new ArrayList<Way>(getCurrentDataSet().getSelectedWays());
+        if (!selectedWays.contains(referenceSegment.way)) {
+            getCurrentDataSet().setSelected(referenceSegment.way);
+            selectedWays.clear();
+            selectedWays.add(referenceSegment.way);
+        }
+
+        try {
+            pWays = new ParallelWays(selectedWays, copyTags, selectedWays.indexOf(referenceSegment.way));
+            pWays.commit(null);
+            getCurrentDataSet().setSelected(pWays.ways);
+            return true;
+        } catch (IllegalArgumentException e) {
+            System.err.println(e);
+            pWays = null;
+            return false;
+        }
+    }
+
+    // TODO: 'ParallelPath' better name?
+    static final class ParallelWays {
+        private List<Way> ways;
+        private List<Node> sortedNodes;
+
+        private int nodeCount;
+
+        private EastNorth[] pts;
+        private EastNorth[] normals;
+
+        public ParallelWays(List<Way> sourceWays, boolean copyTags, int refWayIndex) {
+            // Possible/sensible to use PrimetiveDeepCopy here?
+
+            //// Make a deep copy of the ways, keeping the copied ways connected
+            HashMap<Node, Node> splitNodeMap = new HashMap<Node, Node>(sourceWays.size());
+            for (Way w : sourceWays) {
+                if (!splitNodeMap.containsKey(w.firstNode())) {
+                    splitNodeMap.put(w.firstNode(), copyNode(w.firstNode(), copyTags));
+                }
+                if (!splitNodeMap.containsKey(w.lastNode())) {
+                    splitNodeMap.put(w.lastNode(), copyNode(w.lastNode(), copyTags));
+                }
+            }
+            ways = new ArrayList<Way>(sourceWays.size());
+            for (Way w : sourceWays) {
+                Way wCopy = new Way();
+                wCopy.addNode(splitNodeMap.get(w.firstNode()));
+                for (int i = 1; i < w.getNodesCount() - 1; i++) {
+                    wCopy.addNode(copyNode(w.getNode(i), copyTags));
+                }
+                wCopy.addNode(splitNodeMap.get(w.lastNode()));
+                if (copyTags) {
+                    wCopy.setKeys(w.getKeys());
+                }
+                ways.add(wCopy);
+            }
+            sourceWays = null; // Ensure that we only use the copies from now
+
+            //// Find a linear ordering of the nodes. Fail if there isn't one.
+            CombineWayAction.NodeGraph nodeGraph = CombineWayAction.NodeGraph.createUndirectedGraphFromNodeWays(ways);
+            sortedNodes = nodeGraph.buildSpanningPath();
+            if (sortedNodes == null)
+                throw new IllegalArgumentException("Ways must have spanning path"); // Create a dedicated exception?
+
+            //// 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.
+            {
+                Way refWay = ways.get(refWayIndex);
+                boolean refWayReversed = false;
+                if (isClosedPath()) { // Nodes occur more than once in the list
+                    if (refWay.firstNode() == sortedNodes.get(0) && refWay.lastNode() == sortedNodes.get(0)) {
+                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1);
+                    } else if (refWay.lastNode() == sortedNodes.get(0)) {
+                        refWayReversed =
+                            sortedNodes.get(sortedNodes.size() - 1) != refWay.getNode(refWay.getNodesCount() - 1);
+                    } else if (refWay.firstNode() == sortedNodes.get(0)) {
+                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1);
+                    } else {
+                        refWayReversed =
+                            sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode());
+                    }
+
+                } else {
+                    refWayReversed = sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode());
+                }
+                if (refWayReversed) {
+                    Collections.reverse(sortedNodes); // need to keep the orientation of the reference way.
+                    System.err.println("reversed!");
+                }
+            }
+
+            //// Initialize the required parameters. (segment normals, etc.)
+            nodeCount = sortedNodes.size();
+            pts = new EastNorth[nodeCount];
+            normals = new EastNorth[nodeCount - 1];
+            int i = 0;
+            for (Node n : sortedNodes) {
+                EastNorth t = n.getEastNorth();
+                pts[i] = t;
+                i++;
+            }
+            for (i = 0; i < nodeCount - 1; i++) {
+                double dx = pts[i + 1].getX() - pts[i].getX();
+                double dy = pts[i + 1].getY() - pts[i].getY();
+                double len = Math.sqrt(dx * dx + dy * dy);
+                normals[i] = new EastNorth(-dy / len, dx / len);
+            }
+        }
+
+        public boolean isClosedPath() {
+            return sortedNodes.get(0) == sortedNodes.get(sortedNodes.size() - 1);
+        }
+
+        public void changeOffset(double d) {
+            //// This is the core algorithm:
+            /* 1. Calculate a parallel line, offset by 'd', to each segment in
+             *    the path
+             * 2. Find the intersection of lines belonging to neighboring
+             *    segments. These become the new node positions
+             * 3. Do some special casing for closed paths
+             * 
+             * Simple and probably not even close to optimal performance-vise
+             */
+
+            EastNorth[] ppts = new EastNorth[nodeCount];
+
+            EastNorth prevA = add(pts[0], mul(normals[0], d));
+            EastNorth prevB = add(pts[1], mul(normals[0], d));
+            for (int i = 1; i < nodeCount - 1; i++) {
+                EastNorth A = add(pts[i], mul(normals[i], d));
+                EastNorth B = add(pts[i + 1], mul(normals[i], d));
+                if (Geometry.segmentsParallel(A, B, prevA, prevB)) {
+                    ppts[i] = A;
+                } else {
+                    ppts[i] = Geometry.getLineLineIntersection(A, B, prevA, prevB);
+                }
+                prevA = A;
+                prevB = B;
+            }
+            if (isClosedPath()) {
+                EastNorth A = add(pts[0], mul(normals[0], d));
+                EastNorth B = add(pts[1], mul(normals[0], d));
+                if (Geometry.segmentsParallel(A, B, prevA, prevB)) {
+                    ppts[0] = A;
+                } else {
+                    ppts[0] = Geometry.getLineLineIntersection(A, B, prevA, prevB);
+                }
+                ppts[nodeCount - 1] = ppts[0];
+            } else {
+                ppts[0] = add(pts[0], mul(normals[0], d));
+                ppts[nodeCount - 1] = add(pts[nodeCount - 1], mul(normals[nodeCount - 2], d));
+            }
+
+            for (int i = 0; i < nodeCount; i++) {
+                sortedNodes.get(i).setEastNorth(ppts[i]);
+            }
+        }
+
+        // Draw helper lines instead like DrawAction ExtrudeAction?
+        public void commit(DataSet ds) {
+            SequenceCommand undoCommand = new SequenceCommand("Make parallel way(s)", makeAddWayAndNodesCommandList());
+            Main.main.undoRedo.add(undoCommand);
+        }
+
+        private List<Command> makeAddWayAndNodesCommandList() {
+            ArrayList<Command> commands = new ArrayList<Command>(sortedNodes.size() + ways.size());
+            for (int i = 0; i < sortedNodes.size() - 1; i++) {
+                commands.add(new AddCommand(sortedNodes.get(i)));
+            }
+            if (!isClosedPath()) {
+                commands.add(new AddCommand(sortedNodes.get(sortedNodes.size() - 1)));
+            }
+            for (Way w : ways) {
+                commands.add(new AddCommand(w));
+            }
+            return commands;
+        }
+
+        static private Node copyNode(Node source, boolean copyTags) {
+            if (copyTags)
+                return new Node(source, true);
+            else {
+                Node n = new Node();
+                n.setCoor(source.getCoor());
+                return n;
+            }
+        }
+
+        // We need either a dedicated vector type, or operations such as these
+        // added to EastNorth...
+        static private EastNorth mul(EastNorth en, double f) {
+            return new EastNorth(en.getX() * f, en.getY() * f);
+        }
+
+        static private EastNorth add(EastNorth a, EastNorth b) {
+            return new EastNorth(a.east() + b.east(), a.north() + b.north());
+        }
+    }
+
+    // TODO: Finish me. (or maybe having a state-tracker object for this is over-complicating things?)
+    // Dunno.. this swing stuff is a mess..
+    static final class MouseTracker {
+        public static final class ButtonState {
+            public long pressedTime = -1;
+            public Point pressedPos = null;
+            public long releasedTime = -1;
+            public Point releasedPos = null;
+            public boolean hasBeenDragged = false;
+            public boolean isPressed = false;
+            public Point lastPos = null;
+        }
+
+        public MouseTracker() {
+            for (int i = 0; i < buttonCount; i++) {
+                buttonState[i] = new ButtonState();
+            }
+        }
+
+        public ButtonState[] buttonState = new ButtonState[4];
+        public final int buttonCount = 4;
+
+        public MouseEvent lastPressedEvent;
+        public MouseEvent lastMoveEvent;
+        public MouseEvent lastDragEvent;
+        public MouseEvent lastReleaseEvent;
+
+        public ButtonState getButtonState(MouseEvent e) {
+            return buttonState[e.getButton()];
+        }
+
+        public void registrerPressed(MouseEvent e) {
+            ButtonState b = getButtonState(e);
+            b.pressedTime = System.currentTimeMillis();
+            b.pressedPos = e.getPoint();
+            b.hasBeenDragged = false;
+            b.lastPos = e.getPoint();
+            b.isPressed = true;
+
+            lastPressedEvent = e;
+        }
+
+        public void registrerReleased(MouseEvent e) {
+            ButtonState b = getButtonState(e);
+            b.releasedTime = System.currentTimeMillis();
+            b.releasedPos = e.getPoint();
+            b.isPressed = false;
+            b.lastPos = e.getPoint();
+
+            lastReleaseEvent = e;
+        }
+
+        public void registrerMoved(MouseEvent e) {
+            // TODO:
+        }
+
+        public void registrerDragged(MouseEvent e) {
+            // no button info
+            for (ButtonState b : buttonState) {
+                if (b.isPressed) {
+                    b.hasBeenDragged = true;
+                    b.lastPos = e.getPoint();
+                }
+            }
+
+            lastDragEvent = e;
+        }
+
+        public void clear() {
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/MapFrame.java b/src/org/openstreetmap/josm/gui/MapFrame.java
index 96bddae..830c567 100644
--- a/src/org/openstreetmap/josm/gui/MapFrame.java
+++ b/src/org/openstreetmap/josm/gui/MapFrame.java
@@ -40,6 +40,7 @@ import org.openstreetmap.josm.actions.mapmode.DeleteAction;
 import org.openstreetmap.josm.actions.mapmode.DrawAction;
 import org.openstreetmap.josm.actions.mapmode.ExtrudeAction;
 import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.actions.mapmode.ParallelWayAction;
 import org.openstreetmap.josm.actions.mapmode.SelectAction;
 import org.openstreetmap.josm.actions.mapmode.ZoomAction;
 import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
@@ -129,6 +130,7 @@ public class MapFrame extends JPanel implements Destroyable, LayerChangeListener
         addMapMode(new IconToggleButton(new SelectAction(this)));
         addMapMode(new IconToggleButton(new DrawAction(this)));
         addMapMode(new IconToggleButton(new ExtrudeAction(this)));
+        addMapMode(new IconToggleButton(new ParallelWayAction(this)));
         addMapMode(new IconToggleButton(new ZoomAction(this)));
         addMapMode(new IconToggleButton(new DeleteAction(this)));
 
diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
index ade8359..9930fbb 100644
--- a/src/org/openstreetmap/josm/tools/Geometry.java
+++ b/src/org/openstreetmap/josm/tools/Geometry.java
@@ -332,6 +332,19 @@ public class Geometry {
         else
             return new EastNorth(segmentP1.getX() + ldx * offset, segmentP1.getY() + ldy * offset);
     }
+    public static EastNorth closestPointToLine(EastNorth lineP1, EastNorth lineP2, EastNorth point) {
+        double ldx = lineP2.getX() - lineP1.getX();
+        double ldy = lineP2.getY() - lineP1.getY();
+
+        if (ldx == 0 && ldy == 0) //segment zero length
+            return lineP1;
+
+        double pdx = point.getX() - lineP1.getX();
+        double pdy = point.getY() - lineP1.getY();
+
+        double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy);
+        return new EastNorth(lineP1.getX() + ldx * offset, lineP1.getY() + ldy * offset);
+    }
 
     /**
      * This method tests if secondNode is clockwise to first node.
@@ -457,7 +470,7 @@ public class Geometry {
 
         return inside;
     }
-    
+
     /**
      * returns area of a closed way in square meters
      * (approximate(?), but should be OK for small areas)
@@ -477,7 +490,7 @@ public class Geometry {
         }
         return Math.abs(area/2);
     }
-    
+
     protected static double calcX(Node p1){
         double lat1, lon1, lat2, lon2;
         double dlon, dlat;
@@ -494,7 +507,7 @@ public class Geometry {
         double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
         return 6367000 * c;
     }
-    
+
     protected static double calcY(Node p1){
         double lat1, lon1, lat2, lon2;
         double dlon, dlat;
