Index: /applications/editors/josm/plugins/utilsplugin/build.xml
===================================================================
--- /applications/editors/josm/plugins/utilsplugin/build.xml	(revision 5076)
+++ /applications/editors/josm/plugins/utilsplugin/build.xml	(revision 5076)
@@ -0,0 +1,34 @@
+<project name="utilsplugin" default="build" basedir=".">
+  <property name="josm" location="../../core/dist/josm-custom.jar" />
+	
+  <target name="init">
+    <mkdir dir="build"/>
+  </target>
+
+  <target name="compile" depends="init">
+    <javac srcdir="src" classpath="${josm}" destdir="build" debug="true"/>
+  </target>
+
+  <target name="build" depends="clean, compile">
+    <copy todir="build/images">
+      <fileset dir="images"/>
+    </copy>
+    <jar destfile="utilsplugin.jar" basedir="build">
+      <manifest>
+	<attribute name="Plugin-Class" value="UtilsPlugin.UtilsPlugin"/>
+	<attribute name="Plugin-Description" value="Useful JOSM utilities"/>
+	<attribute name="Plugin-Version" value="0.5"/>
+	<attribute name="Author"
+	  value="(originally) Martijn van Oosterhout &gt;kleptog@svana.org&gt;"/>
+      </manifest>
+    </jar>
+  </target>
+
+  <target name="clean">
+    <delete dir="build" />
+  </target>
+
+  <target name="install" depends="build">
+    <copy file="utilsplugin.jar" todir="${user.home}/.josm/plugins"/>
+  </target>
+</project>
Index: /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/MergeNodeWayAction.java
===================================================================
--- /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/MergeNodeWayAction.java	(revision 5076)
+++ /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/MergeNodeWayAction.java	(revision 5076)
@@ -0,0 +1,89 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.AddCommand;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+import javax.swing.AbstractAction;
+
+class MergeNodeWayAction extends JosmAction {
+	public MergeNodeWayAction() {
+	    super(tr("Join node to way"), "mergenodeway",
+			tr("Join a node into the nearest way segments"), 0, 0, true);
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> sel = Main.ds.getSelected();
+		if (sel.size() != 1 || !(sel.iterator().next() instanceof Node)) return;
+		Node node = (Node) sel.iterator().next();
+
+		List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(
+			Main.map.mapView.getPoint(node.eastNorth));
+		HashMap<Way, List<Integer>> insertPoints = new HashMap<Way, List<Integer>>();
+		for (WaySegment ws : wss) {
+			List<Integer> is;
+			if (insertPoints.containsKey(ws.way)) {
+				is = insertPoints.get(ws.way);
+			} else {
+				is = new ArrayList<Integer>();
+				insertPoints.put(ws.way, is);
+			}
+
+			if (ws.way.nodes.get(ws.lowerIndex) != node
+					&& ws.way.nodes.get(ws.lowerIndex+1) != node) {
+				is.add(ws.lowerIndex);
+			}
+		}
+
+		Collection<Command> cmds = new LinkedList<Command>();
+		for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
+			Way w = insertPoint.getKey();
+			Way wnew = new Way(w);
+			List<Integer> is = insertPoint.getValue();
+			pruneSuccsAndReverse(is);
+			for (int i : is) wnew.nodes.add(i+1, node);
+			cmds.add(new ChangeCommand(w, wnew));
+		}
+
+		Main.main.undoRedo.add(new SequenceCommand(tr("Join Node and Line"), cmds));
+		Main.map.repaint();
+	}
+
+	private static void pruneSuccsAndReverse(List<Integer> is) {
+		//if (is.size() < 2) return;
+
+		HashSet<Integer> is2 = new HashSet<Integer>();
+		for (int i : is) {
+			if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
+				is2.add(i);
+			}
+		}
+		is.clear();
+		is.addAll(is2);
+		Collections.sort(is);
+		Collections.reverse(is);
+	}
+}
Index: /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/MergeNodesAction.java
===================================================================
--- /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/MergeNodesAction.java	(revision 5076)
+++ /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/MergeNodesAction.java	(revision 5076)
@@ -0,0 +1,126 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.Collection;
+import java.util.Collections;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.CollectBackReferencesVisitor;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.AddCommand;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.SequenceCommand;
+
+import javax.swing.AbstractAction;
+import javax.swing.JOptionPane;
+
+class MergeNodesAction extends JosmAction {
+	public MergeNodesAction() {
+		super(tr("Merge nodes"), "mergenodes",
+			tr("Merge nodes"), 0, 0, true);
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> sel = Main.ds.getSelected();
+		Collection<Node> nodes = new ArrayList<Node>();
+
+		for (OsmPrimitive osm : sel)
+			if (osm instanceof Node)
+				nodes.add((Node)osm);
+		if (nodes.size() < 2) {
+			JOptionPane.showMessageDialog(Main.parent,
+				tr("Must select at least two nodes."));
+			return;
+		}
+
+		// Find the node with the lowest ID.
+		// We're gonna keep our 3-digit node ids.
+		Node target = null;
+		for (Node n : nodes) {
+			if (target == null || target.id == 0 || n.id < target.id) {
+				target = n;
+			}
+		}
+
+		Collection<Command> cmds = new LinkedList<Command>();
+
+		Node newTarget = new Node(target);
+		cmds.add(new ChangeCommand(target, newTarget));
+
+		// Don't place the merged node on one of the former nodes.
+		// Place it right there in the middle.
+		double x = 0, y = 0;
+		for (Node n : nodes) {
+			x += n.eastNorth.east();
+			y += n.eastNorth.north();
+		}
+		newTarget.eastNorth = new EastNorth(
+			x / nodes.size(), y / nodes.size());
+
+		nodes.remove(target);
+
+		cmds.add(new DeleteCommand(nodes));
+
+		for (Way w : Main.ds.ways) {
+			if (w.deleted || w.incomplete) continue;
+
+			boolean affected = false;
+			for (Node n : nodes) {
+				if (w.nodes.contains(n)) {
+					affected = true;
+					break;
+				}
+			}
+			if (!affected) continue;
+
+			// Replace the old nodes with the merged ones
+			Way wnew = new Way(w);
+			for (int i = 0; i < wnew.nodes.size(); i++) {
+				if (nodes.contains(wnew.nodes.get(i))) {
+					wnew.nodes.set(i, newTarget);
+				}
+			}
+
+			// Remove duplicates
+			Node lastN = null;
+			for (int i = wnew.nodes.size() - 1; i >= 0; i--) {
+				if (lastN == wnew.nodes.get(i)) {
+					wnew.nodes.remove(i);
+					if (i < wnew.nodes.size()) i++;
+				}
+			}
+
+			if (wnew.nodes.size() < 2) {
+				CollectBackReferencesVisitor backRefV =
+					new CollectBackReferencesVisitor(Main.ds, false);
+				backRefV.visit(w);
+				if (!backRefV.data.isEmpty()) {
+					JOptionPane.showMessageDialog(Main.parent,
+						tr("Cannot merge nodes: " +
+							"Would have to delete way that is still used."));
+					return;
+				}
+
+				cmds.add(new DeleteCommand(Collections.singleton(w)));
+			} else {
+				cmds.add(new ChangeCommand(w, wnew));
+			}
+		}
+
+		Main.main.undoRedo.add(new SequenceCommand(tr("Merge Nodes"), cmds));
+		Main.map.repaint();
+	}
+}
Index: /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/SimplifyWayAction.java
===================================================================
--- /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/SimplifyWayAction.java	(revision 5076)
+++ /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/SimplifyWayAction.java	(revision 5076)
@@ -0,0 +1,279 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.coor.LatLon;
+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.visitor.CollectBackReferencesVisitor;
+
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.actions.JosmAction;
+
+public class SimplifyWayAction extends JosmAction {
+	public SimplifyWayAction() {
+		super(tr("Simplify Way"), "simplify",
+			tr("Delete unnecessary nodes from a way."), 0, 0, true);
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> selection = Main.ds.getSelected();
+
+		if (selection.size() == 1 && selection.iterator().next() instanceof Way) {
+			simplifyWay((Way) selection.iterator().next());
+		}
+	}
+
+	public void simplifyWay(Way w) {
+		double threshold = Double.parseDouble(
+			Main.pref.get("simplify-way.max-error", "50"));
+
+		Way wnew = new Way(w);
+
+		int toI = wnew.nodes.size() - 1;
+		for (int i = wnew.nodes.size() - 1; i >= 0; i--) {
+			CollectBackReferencesVisitor backRefsV =
+				new CollectBackReferencesVisitor(Main.ds, false);
+			backRefsV.visit(wnew.nodes.get(i));
+			boolean used = false;
+			if (backRefsV.data.size() == 1) {
+				used = Collections.frequency(
+					w.nodes, wnew.nodes.get(i)) > 1;
+			} else {
+				backRefsV.data.remove(w);
+				used = !backRefsV.data.isEmpty();
+			}
+
+			if (used) {
+				if (toI - i >= 2) {
+					ArrayList<Node> ns = new ArrayList<Node>();
+					simplifyWayRange(wnew, i, toI, ns, threshold);
+					for (int j = toI-1; j > i; j--) wnew.nodes.remove(j);
+					wnew.nodes.addAll(i+1, ns);
+				}
+				toI = i;
+			}
+		}
+
+		HashSet<Node> delNodes = new HashSet<Node>();
+		delNodes.addAll(w.nodes);
+		delNodes.removeAll(wnew.nodes);
+
+		if (wnew.nodes.size() != w.nodes.size()) {
+			Collection<Command> cmds = new LinkedList<Command>();
+			cmds.add(new ChangeCommand(w, wnew));
+			cmds.add(new DeleteCommand(delNodes));
+			Main.main.undoRedo.add(
+				new SequenceCommand(tr("Simplify Way (remove {0} nodes)",
+						delNodes.size()),
+					cmds));
+			Main.map.repaint();
+		}
+	}
+
+	/*
+	 * Takes an interval [from,to] and adds nodes from the set (from,to) to
+	 * ns.
+	 */
+	public void simplifyWayRange(Way wnew, int from, int to, ArrayList<Node> ns, double thr) {
+		Node fromN = wnew.nodes.get(from), toN = wnew.nodes.get(to);
+
+		int imax = -1;
+		double xtemax = 0;
+		for (int i = from+1; i < to; i++) {
+			Node n = wnew.nodes.get(i);
+			double xte = radtometers(linedist(
+				fromN.coor.lat(), fromN.coor.lon(),
+				n.coor.lat(), n.coor.lon(),
+				toN.coor.lat(), toN.coor.lon()));
+			if (xte > xtemax) {
+				xtemax = xte;
+				imax = i;
+			}
+		}
+
+		if (imax != -1 && xtemax >= thr) {
+			simplifyWayRange(wnew, from, imax, ns, thr);
+			ns.add(wnew.nodes.get(imax));
+			simplifyWayRange(wnew, imax, to, ns, thr);
+		}
+	}
+
+	/* ---------------------------------------------------------------------- 
+	 * Everything below this comment was converted from C to Java by Frederik
+	 * Ramm. The original sources are the files grtcirc.c and smplrout.c from 
+	 * the gpsbabel source code (www.gpsbabel.org), which is under GPL. The
+	 * relevant code portions have been written by Robert Lipe.
+	 * 
+	 * Method names have been left unchanged where possible.
+	 */
+	
+	public static double EARTH_RAD = 6378137.0;
+	public static double radmiles = EARTH_RAD*100.0/2.54/12.0/5280.0;
+
+	public static double[] crossproduct(double[] v1, double[] v2) {
+		double[] rv = new double[3];
+		rv[0] = v1[1]*v2[2]-v2[1]*v1[2];
+		rv[1] = v1[2]*v2[0]-v2[2]*v1[0];
+		rv[2] = v1[0]*v2[1]-v1[1]*v2[0];
+		return rv;
+	}
+
+	public static double dotproduct(double[] v1, double[] v2) {
+		return v1[0]*v2[0]+v1[1]*v2[1]+v1[2]*v2[2];
+	}
+
+	public static double radtomiles(double rads) {
+		return (rads*radmiles);
+	}
+
+	public static double radtometers(double rads) {
+		return (rads * EARTH_RAD);
+	}
+	
+	public static double veclen(double[] vec) {
+		return Math.sqrt(vec[0]*vec[0]+vec[1]*vec[1]+vec[2]*vec[2]);
+	}
+
+	public static double gcdist(double lat1, double lon1, double lat2, double lon2) 
+	{
+		double res;
+		double sdlat, sdlon;
+
+		sdlat = Math.sin((lat1 - lat2) / 2.0);
+		sdlon = Math.sin((lon1 - lon2) / 2.0);
+
+		res = Math.sqrt(sdlat * sdlat + Math.cos(lat1) * Math.cos(lat2) * sdlon * sdlon);
+
+		if (res > 1.0) {
+			res = 1.0;
+		} else if (res < -1.0) {
+			res = -1.0;
+		}
+
+		res = Math.asin(res);
+		return 2.0 * res;
+	}
+
+	static double linedist(double lat1, double lon1, double lat2, double lon2, double lat3, double lon3) {
+
+		double dot;
+
+		/* degrees to radians */
+		lat1 = Math.toRadians(lat1);  lon1 = Math.toRadians(lon1);
+		lat2 = Math.toRadians(lat2);  lon2 = Math.toRadians(lon2);
+		lat3 = Math.toRadians(lat3);  lon3 = Math.toRadians(lon3);
+
+		/* polar to ECEF rectangular */
+		double[] v1 = new double[3];
+		double[] v2 = new double[3];
+		double[] v3 = new double[3];
+		v1[0] = Math.cos(lon1)*Math.cos(lat1); v1[1] = Math.sin(lat1); v1[2] = Math.sin(lon1)*Math.cos(lat1);
+		v2[0] = Math.cos(lon2)*Math.cos(lat2); v2[1] = Math.sin(lat2); v2[2] = Math.sin(lon2)*Math.cos(lat2);
+		v3[0] = Math.cos(lon3)*Math.cos(lat3); v3[1] = Math.sin(lat3); v3[2] = Math.sin(lon3)*Math.cos(lat3);
+
+		/* 'va' is the axis; the line that passes through the center of the earth
+		 * and is perpendicular to the great circle through point 1 and point 2 
+		 * It is computed by taking the cross product of the '1' and '2' vectors.*/
+		double[] va = crossproduct(v1, v2);
+		double la = veclen(va);
+
+		if (la != 0) {
+			va[0] /= la;
+			va[1] /= la;
+			va[2] /= la;
+
+			/* dot is the component of the length of '3' that is along the axis.
+			 * What's left is a non-normalized vector that lies in the plane of 
+			 * 1 and 2. */
+
+			dot = dotproduct(v3, va);
+
+			double[] vp = new double[3];
+			vp[0]=v3[0]-dot*va[0];
+			vp[1]=v3[1]-dot*va[1];
+			vp[2]=v3[2]-dot*va[2];
+
+			double lp = veclen(vp);
+
+			if (lp != 0) {
+
+				/* After this, 'p' is normalized */
+				vp[0] /= lp;
+				vp[1] /= lp;
+				vp[2] /= lp;
+
+				double[] cp1 = crossproduct(v1, vp);
+				double dp1 = dotproduct(cp1, va);
+
+				double[] cp2 = crossproduct(v2, vp);
+				double dp2 = dotproduct(cp2, va);
+
+				if ( dp1 >= 0 && dp2 >= 0 ) {
+					/* rather than call gcdist and all its sines and cosines and
+					 * worse, we can get the angle directly.  It's the arctangent
+					 * of the length of the component of vector 3 along the axis 
+					 * divided by the length of the component of vector 3 in the 
+					 * plane.  We already have both of those numbers. 
+					 * 
+					 * atan2 would be overkill because lp and Math.abs are both
+					 * known to be positive. */
+					return Math.atan(Math.abs(dot)/lp); 
+				}
+
+				/* otherwise, get the distance from the closest endpoint */
+				double c1 = dotproduct(v1, vp);
+				double c2 = dotproduct(v2, vp);
+				dp1 = Math.abs(dp1);
+				dp2 = Math.abs(dp2);
+
+				/* This is a hack.  d$n$ is proportional to the sine of the angle
+				 * between point $n$ and point p.  That preserves orderedness up
+				 * to an angle of 90 degrees.  c$n$ is proportional to the cosine
+				 * of the same angle; if the angle is over 90 degrees, c$n$ is
+				 * negative.  In that case, we flop the sine across the y=1 axis
+				 * so that the resulting value increases as the angle increases. 
+				 * 
+				 * This only works because all of the points are on a unit sphere. */
+
+				if (c1 < 0) {
+					dp1 = 2 - dp1;
+				}
+				if (c2 < 0) {
+					dp2 = 2 - dp2;
+				}
+
+				if (Math.abs(dp1) < Math.abs(dp2)) {
+					return gcdist(lat1,lon1,lat3,lon3);  
+				} else {
+					return gcdist(lat2,lon2,lat3,lon3);
+				}
+			} else {
+				/* lp is 0 when 3 is 90 degrees from the great circle */
+				return Math.PI/2;
+			}    
+		} else {
+			/* la is 0 when 1 and 2 are either the same point or 180 degrees apart */
+			dot = dotproduct(v1, v2);
+			if (dot >= 0) { 
+				return gcdist(lat1,lon1,lat3,lon3);
+			} else {
+				return 0;
+			}
+		}
+	}
+}
Index: /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/UtilsPlugin.java
===================================================================
--- /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/UtilsPlugin.java	(revision 5076)
+++ /applications/editors/josm/plugins/utilsplugin/src/UtilsPlugin/UtilsPlugin.java	(revision 5076)
@@ -0,0 +1,31 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.gui.IconToggleButton;
+
+import java.awt.event.ActionEvent;
+import javax.swing.AbstractAction;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+
+import javax.swing.JPanel;
+import javax.swing.BoxLayout;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.gui.MapFrame;
+
+public class UtilsPlugin extends Plugin {
+	@Override
+	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+		if (oldFrame == null && newFrame != null) {
+			Main.map.toolBarActions.addSeparator();
+			Main.map.toolBarActions.add(new MergeNodesAction());
+			Main.map.toolBarActions.add(new MergeNodeWayAction());
+			Main.map.toolBarActions.add(new SimplifyWayAction());
+		}
+	}
+}
