Index: applications/editors/josm/plugins/utilsplugin/META-INF/MANIFEST.MF
===================================================================
--- applications/editors/josm/plugins/utilsplugin/META-INF/MANIFEST.MF	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/META-INF/MANIFEST.MF	(revision 5075)
@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+Created-By: 1.5.0_10 (Sun Microsystems Inc.)
+Plugin-Class: UtilsPlugin.UtilsPlugin
+Plugin-Description: Adds various utilities to JOSM (v.4) By Martijn va
+ n Oosterhout <kleptog@svana.org>
+Main-Class: UtilsPlugin.UtilsPlugin
+
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/DeduplicateWayAction.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/DeduplicateWayAction.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/DeduplicateWayAction.java	(revision 5075)
@@ -0,0 +1,58 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Collection;
+
+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.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.SequenceCommand;
+
+import javax.swing.AbstractAction;
+
+class DeduplicateWayAction extends AbstractAction {
+	public DeduplicateWayAction() {
+		super("Deduplicate Way");
+	}
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> sel = Main.ds.getSelected();
+		Collection<Way> ways = new ArrayList<Way>();
+		for (OsmPrimitive osm : sel)
+			if (osm instanceof Way)
+				ways.add((Way)osm);
+
+		Collection<Command> cmds = new LinkedList<Command>();
+		for ( Way w : ways )
+		{
+			List<Segment> segs = new ArrayList<Segment>();
+			
+			for ( Segment s : w.segments )
+			{
+				if( !segs.contains(s) )
+					segs.add(s);
+			}
+			if( segs.size() != w.segments.size() )
+			{
+				Way newway = new Way(w);
+				newway.segments.clear();
+				newway.segments.addAll(segs);
+				cmds.add(new ChangeCommand(w,newway));
+			}
+		}
+		if( cmds.size() != 0 )
+			Main.main.editLayer().add(new SequenceCommand(tr("Deduplicate Ways"), cmds));
+		Main.map.repaint();
+	}
+}
+
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/ConsistancyTest.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/ConsistancyTest.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/ConsistancyTest.java	(revision 5075)
@@ -0,0 +1,62 @@
+package UtilsPlugin.JosmLint;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class ConsistancyTest implements JosmLintTest {
+        private class NullSegmentResult extends JosmLintTestResult
+        {
+                public NullSegmentResult( Segment o, ConsistancyTest c )
+                {
+                        super( o, c );
+                }
+                public String toString()
+                {
+                        return "Null segment: "+ObjectDescr(obj);
+                }
+        }
+        private class DuplicateSegmentResult extends JosmLintTestResult
+        {
+                private Segment seg;
+                public DuplicateSegmentResult( Way o, ConsistancyTest c, Segment bad )
+                {
+                        super( o, c );
+                        seg = bad;
+                }
+                public String toString()
+                {
+                        return "Duplicate segment: "+ObjectDescr(obj);
+                }
+                public void cloneFrom(DuplicateSegmentResult res)
+                {
+                        this.obj = res.obj;
+                        this.test = res.test;
+                        this.seg = res.seg;
+                }
+        }
+        public ConsistancyTest() {}
+        
+        public JosmLintTestResult runTest( OsmPrimitive o )
+        {
+                if( o instanceof Node )
+                        return null;
+                if( o instanceof Segment )
+                {
+                        Segment s = (Segment)o;
+                        if( !s.incomplete && s.from == s.to )
+                                return new NullSegmentResult( s, this );
+                }
+                if( o instanceof Way )
+                {
+                        Way w = (Way)o;
+                        for ( Segment s : w.segments )
+                        {
+                                if( w.segments.indexOf(s) != w.segments.lastIndexOf(s) )
+                                        return new DuplicateSegmentResult( w, this, s );
+                        }
+                }
+                return null;
+        }
+}
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLint.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLint.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLint.java	(revision 5075)
@@ -0,0 +1,219 @@
+package UtilsPlugin.JosmLint;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.IconToggleButton;
+import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import java.awt.event.KeyEvent;
+import javax.swing.JToolBar;
+import java.awt.BorderLayout;
+import javax.swing.BoxLayout;
+import java.awt.GridLayout;
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListSelectionModel;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import org.openstreetmap.josm.data.SelectionChangedListener;
+import org.openstreetmap.josm.data.osm.DataSet;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.Collection;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+public class JosmLint extends ToggleDialog implements SelectionChangedListener {
+        /**
+         * The selection's list data.
+         */
+        private final DefaultListModel list = new DefaultListModel();
+        private final Map<OsmPrimitive,JosmLintTestResult> results = new TreeMap<OsmPrimitive,JosmLintTestResult>();
+        /**
+         * The display list.
+         */
+        private JList displaylist = new JList(list);
+        List<JosmLintTest> tests = new ArrayList<JosmLintTest>();
+
+        private class JosmLintWorker implements Runnable
+        {
+                public boolean stop;
+                private JosmLint parent;
+                
+                public JosmLintWorker(JosmLint p)
+                {
+                        parent = p;
+                        stop = false;
+                }
+                
+                public void run()
+                {
+			/* Background loop, checks 200 objects per second... */
+                        while( !stop )
+                        {
+                                Collection<OsmPrimitive> ds = Main.ds.allNonDeletedPrimitives();
+                                for( OsmPrimitive o : ds )
+                                {
+                                        if( stop )
+                                                break;
+                                        parent.checkObject(o);
+                                        simpleSleep(5);
+                                }
+                                simpleSleep(10000);
+                        }
+                }
+        }
+        
+        private static void simpleSleep( int millis )
+        {
+                try {
+                        Thread.sleep(millis);
+                } 
+                catch(InterruptedException e) {}
+        }
+        private void checkObject( OsmPrimitive o )
+        {
+                JosmLintTestResult res = results.get(o);
+                
+                if( res != null )
+                {
+                      if( !res.recheck() )
+                      {
+                              results.remove(o);
+                              list.removeElement(res);
+                              res = null;
+                      }
+                }
+                if( res != null )
+                        return;
+
+                for( JosmLintTest test : tests )
+                {
+                        res = test.runTest(o);
+                        if( res != null )
+                        {
+                                System.out.println( "Got test failure: "+res );
+                                
+                                results.put(o,res);
+                                list.addElement(res);
+                                break;
+                        }
+                }
+        }
+
+        private JosmLintWorker worker;
+        private static JosmLint lint;
+        
+        public JosmLint()
+        {
+                super( tr("JosmLint"), "josmlint", tr("Scans the current data for problems"), KeyEvent.VK_J, 150 );
+                add(new JScrollPane(displaylist), BorderLayout.CENTER);
+                displaylist.addMouseListener(new MouseAdapter(){
+                        @Override public void mouseClicked(MouseEvent e) {
+                                if (e.getClickCount() < 2)
+                                        return;
+                                selectObject();
+                        }
+                });
+                                                                                                                                                                                        		
+                tests.add( new ConsistancyTest() );
+                tests.add( new WayCheckTest() );
+        }
+
+        @Override public void setVisible(boolean b) {
+                if (b) {
+                        try { Main.ds.addSelectionChangedListener(this); }
+                        catch( NoSuchMethodError e )
+                        {
+                                try {
+                                java.lang.reflect.Field f = DataSet.class.getDeclaredField("listeners");
+                                ((Collection<SelectionChangedListener>)f.get(Main.ds)).add(this);
+//                                Main.ds.listeners.add(this);
+                                } catch (Exception x) { System.out.println( e ); }
+                        }
+                        selectionChanged(Main.ds.getSelected());
+                        worker = new JosmLintWorker(this);
+                        new Thread(worker).start();
+                } else {
+                        try { Main.ds.removeSelectionChangedListener(this); }
+                        catch( NoSuchMethodError e )
+                        {
+                                try {
+                                java.lang.reflect.Field f = DataSet.class.getDeclaredField("listeners");
+                                ((Collection<SelectionChangedListener>)f.get(Main.ds)).remove(this);
+//                                Main.ds.listeners.remove(this);
+                                } catch (Exception x) { System.out.println( e ); }
+                        }
+			if( worker != null )
+			{
+			        worker.stop = true;
+//				worker.join();
+		        	worker = null;
+                        }
+                }
+                super.setVisible(b);
+        }
+        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
+                if (list == null)
+                        return; // selection changed may be received in base class constructor before init
+		for( OsmPrimitive o : newSelection )
+			checkObject(o);
+	}
+        /* When user doubleclicks an item, select that object */
+	public void selectObject()
+	{
+	        JosmLintTestResult res = (JosmLintTestResult)displaylist.getSelectedValue();
+	        OsmPrimitive obj = res.getSelection();
+	        Main.ds.setSelected(obj);
+	}
+
+
+	public static void setupPlugin()
+	{
+		JPanel toggleDialogs = null;
+		JToolBar toolBarActions = Main.map.toolBarActions;
+
+		// Find the toggleDialogs
+		for( final java.awt.Component c : Main.map.getComponents() )
+		{
+			if( c.getClass() != JPanel.class )
+				continue;
+			JPanel c2 = (JPanel)c;
+			
+			if( c2.getLayout().getClass() != BoxLayout.class )
+				continue;
+//			System.out.println( "Found: "+ c2.getComponent(1).getClass() );
+			toggleDialogs = c2;
+			break;
+		}
+
+		if( toggleDialogs == null )
+		{
+		        System.out.println( "Failed to insert dialog" );
+		        return;
+                }
+                lint = new JosmLint();
+                lint.addIconToggle( toggleDialogs, toolBarActions );
+	}
+        private void addIconToggle(JPanel toggleDialogs, JToolBar toolBarActions) {
+                IconToggleButton button = new IconToggleButton(this.action);
+                this.action.button = button;
+                this.parent = toggleDialogs;
+                toolBarActions.add(button);
+                toggleDialogs.add(this);
+        }
+        public static void stopPlugin()
+        {
+                if( lint.worker != null )
+                        lint.worker.stop = true;
+        }
+}
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLintTest.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLintTest.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLintTest.java	(revision 5075)
@@ -0,0 +1,7 @@
+package UtilsPlugin.JosmLint;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+public abstract interface JosmLintTest {
+        public JosmLintTestResult runTest( OsmPrimitive o );
+}
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLintTestResult.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLintTestResult.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/JosmLintTestResult.java	(revision 5075)
@@ -0,0 +1,57 @@
+package UtilsPlugin.JosmLint;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class JosmLintTestResult
+{
+        protected OsmPrimitive obj;
+        protected JosmLintTest test;
+        
+        public JosmLintTestResult( OsmPrimitive o, JosmLintTest t )
+        {
+                this.obj = o;
+                this.test = t;
+        }
+        public boolean recheck()
+        {
+                JosmLintTestResult newres = test.runTest(obj);
+                if( newres == null )
+                        return false;
+                this.cloneFrom(newres);  /* Pick up changes if any */
+                return true;
+        }
+        public void cloneFrom(JosmLintTestResult res)
+        {
+                this.obj = res.obj;
+                this.test = res.test;
+        }
+        public static String ObjectDescr(OsmPrimitive o)
+        {
+                String str;
+                if( o instanceof Node )
+                {
+                        str = "Node "+o.id;
+                }
+                else if( o instanceof Segment )
+                {
+                        str = "Segment "+o.id;
+                }
+                else if( o instanceof Way )
+                {
+                        str = "Way "+o.id;
+                }
+                else
+                {
+                        str = "Unknown object";
+                }
+                return str;
+        }
+        // Return object to select when doubleclicked
+        public OsmPrimitive getSelection()
+        {
+                return obj;
+        }
+}
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/WayCheckTest.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/WayCheckTest.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/JosmLint/WayCheckTest.java	(revision 5075)
@@ -0,0 +1,107 @@
+package UtilsPlugin.JosmLint;
+
+import java.util.Collection;
+import java.util.ArrayList;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class WayCheckTest implements JosmLintTest {
+        private class DiscontinuousWayResult extends JosmLintTestResult
+        {
+                public DiscontinuousWayResult( Way o, WayCheckTest c )
+                {
+                        super( o, c );
+                }
+                public String toString()
+                {
+                        return "Discontinuous way: "+ObjectDescr(obj);
+                }
+        }
+        private class NullWayResult extends JosmLintTestResult
+        {
+                public NullWayResult( Way o, WayCheckTest c )
+                {
+                        super( o, c );
+                }
+                public String toString()
+                {
+                        return "Null way: "+ObjectDescr(obj);
+                }
+        }
+        private class UnorderedWayResult extends JosmLintTestResult
+        {
+                private Segment seg;
+                public UnorderedWayResult( Way o, WayCheckTest c )
+                {
+                        super( o, c );
+                }
+                public String toString()
+                {
+                        return "Unordered way: "+ObjectDescr(obj);
+                }
+        }
+        public WayCheckTest() {}
+        
+        public JosmLintTestResult runTest( OsmPrimitive o )
+        {
+                if( o instanceof Node )
+                        return null;
+                if( o instanceof Segment )
+                        return null;
+                if( o instanceof Way )
+                {
+                        Way w = (Way)o;
+                        if( w.isIncomplete() )
+                                return null;
+                        if( w.segments.size() == 0 )
+                                return new NullWayResult( w, this );
+                        boolean unordered = false;
+                        ArrayList<Segment> unused = new ArrayList<Segment>(w.segments);
+                        Segment s1 = unused.get(0);
+                        unused.remove(s1);
+                        Node start = s1.from;
+                        Node end = s1.to;
+                        boolean change = true;
+                        while(change)
+                        {
+                                change = false;
+                                ArrayList<Segment> temp = new ArrayList<Segment>(unused);
+                                for ( Segment s : temp )
+                                {
+                                        if( s.from == s.to )
+                                        {
+                                                unused.remove(s);
+                                                change = true;
+                                                continue;
+                                        }
+                                        if( s.from == end )
+                                        {
+                                                end = s.to;
+                                                change = true;
+                                                unused.remove(s);
+                                        }
+                                        else if( s.to == start )
+                                        {
+                                                start = s.from;
+                                                unused.remove(s);
+                                                change = true;
+                                                unordered = true;
+                                        }
+                                }
+                        }
+                        if( unused.size() != 0 )
+                        {
+                                return new DiscontinuousWayResult( w, this );
+                        }
+                        if( unordered )
+                        {
+                                return new UnorderedWayResult( w, this );
+                        }
+                        return null;
+                }
+                return null;
+        }
+}
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergePointLineAction.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergePointLineAction.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergePointLineAction.java	(revision 5075)
@@ -0,0 +1,145 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Collection;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+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 MergePointLineAction extends AbstractAction {
+	public MergePointLineAction() {
+	    super("Join Point and Segment");
+	}
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> sel = Main.ds.getSelected();
+	        Node node = null;
+	        Segment seg = null;
+	        Way way = null;
+
+		boolean error = false;	        
+		for (OsmPrimitive osm : sel)
+		{
+			if (osm instanceof Node)
+				if( node == null )
+					node = (Node)osm;
+				else
+					error = true;
+			if (osm instanceof Segment)
+				if( seg == null )
+					seg = (Segment)osm;
+				else
+					error = true;
+			if (osm instanceof Way)
+				if( way == null )
+					way = (Way)osm;
+				else
+					error = true;
+		}
+		if( node == null || !(seg == null ^ way == null))
+			error = true;
+		if( error )
+		{
+			javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Must select one node and one segment/way."));
+			return;
+		}
+		if( way != null )
+		{
+			if( way.isIncomplete() )
+			{
+				javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Selected way must be complete."));
+				return;
+			}
+			double mindist = 0;
+//			System.out.println( node.toString() );
+			// If the user has selected a way and a point, we need to determine the segment that is closest to the given point.
+			for (Segment s : way.segments )
+			{
+				if( s.incomplete )
+					continue;
+					
+//				System.out.println( s.toString() );
+				double dx1 = s.from.coor.lat() - node.coor.lat();
+				double dy1 = s.from.coor.lon() - node.coor.lon();
+				double dx2 = s.to.coor.lat() - node.coor.lat();
+				double dy2 = s.to.coor.lon() - node.coor.lon();
+				
+//				System.out.println( dx1+","+dx2+" && "+dy1+","+dy2 );
+				double len1 = Math.sqrt(dx1*dx1+dy1*dy1);
+				double len2 = Math.sqrt(dx2*dx2+dy2*dy2);
+				dx1 /= len1;
+				dy1 /= len1;
+				dx2 /= len2;
+				dy2 /= len2;
+//				System.out.println( dx1+","+dx2+" && "+dy1+","+dy2 );
+				
+				double dist = dx1*dx2 + dy1*dy2;
+//				System.out.println( "Dist: "+dist );
+				if( dist < mindist )
+				{
+					mindist = dist;
+					seg = s;
+				}
+			}
+			if( seg == null )
+			{
+				javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("No segment found in range"));
+				return;
+			}
+		}
+
+		if( seg.incomplete )
+		{
+			javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Both objects must be complete."));
+			return;
+		}
+		if( node == seg.from || node == seg.to )
+		{
+			javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Node can't be endpoint of segment"));
+			return;
+		}
+		// Now do the merging
+		Collection<Command> cmds = new LinkedList<Command>();
+		Segment newseg1 = new Segment(seg);
+		newseg1.to = node;
+		Segment newseg2 = new Segment(node, seg.to);
+		if (seg.keys != null)
+			newseg2.keys = new java.util.HashMap<String, String>(seg.keys);
+		newseg2.selected = newseg1.selected;
+		                                
+		cmds.add(new ChangeCommand(seg,newseg1));
+		cmds.add(new AddCommand(newseg2));
+		
+		// find ways affected and fix them up...
+		for (final Way w : Main.ds.ways)
+		{
+			if( w.deleted )
+				continue;
+			int pos = w.segments.indexOf(seg);
+			if( pos == -1 )
+				continue;
+			Way newway = new Way(w);
+			newway.segments.add(pos+1, newseg2);
+			cmds.add(new ChangeCommand(w,newway));
+		}
+		Main.main.editLayer().add(new SequenceCommand(tr("Join Node and Line"), cmds));
+		Main.map.repaint();
+	}
+    }
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergePointsAction.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergePointsAction.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergePointsAction.java	(revision 5075)
@@ -0,0 +1,123 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Collection;
+
+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.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+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;
+
+class MergePointsAction extends AbstractAction {
+	public MergePointsAction() {
+		super("Merge Points");
+	}
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> sel = Main.ds.getSelected();
+		Collection<OsmPrimitive> nodes = new ArrayList<OsmPrimitive>();
+		Node target = null;
+		for (OsmPrimitive osm : sel)
+			if (osm instanceof Node)
+				nodes.add((Node)osm);
+		if (nodes.size() < 2) {
+			javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Must select at least two nodes."));
+			return;
+		}
+		for ( OsmPrimitive o : nodes )
+		{
+			Node n = (Node)o;
+			if( target == null || target.id == 0 )
+			{
+				target = n;
+				continue;
+			}
+			if( n.id == 0 )
+				continue;
+			if( n.id < target.id )
+				target = n;
+		}
+//		System.out.println( "Selected: "+target.toString() );
+		nodes.remove(target);
+		
+		// target is what we're merging into
+		// nodes is the list of nodes to be removed
+		// Since some segment may disappear, we need to track those too
+		Collection<OsmPrimitive> seglist = new ArrayList<OsmPrimitive>();
+				
+		// Now do the merging
+		Collection<Command> cmds = new LinkedList<Command>();
+		for (final Segment s : Main.ds.segments) 
+		{
+			if( s.deleted || s.incomplete )
+				continue;
+			if( !nodes.contains( s.from ) && !nodes.contains( s.to ) )
+				continue;
+				
+			Segment newseg = new Segment(s);
+			if( nodes.contains( s.from ) )
+				newseg.from = target;
+			if( nodes.contains( s.to ) )
+				newseg.to = target;
+
+			// Is this node now a NULL node?
+                        if( newseg.from == newseg.to )
+                        	seglist.add(s);
+                        else
+				cmds.add(new ChangeCommand(s,newseg));
+		}
+		if( seglist.size() > 0 )  // Some segments to be deleted?
+		{
+			// We really want to delete this, but we must check if it is part of a way first
+			for (final Way w : Main.ds.ways)
+			{
+				Way newway = null;
+				if( w.deleted )
+					continue;
+				for (final OsmPrimitive o : seglist )
+				{
+					Segment s = (Segment)o;
+					if( w.segments.contains(s) )
+					{
+						if( newway == null )
+							newway = new Way(w);
+						newway.segments.remove(s);
+					}
+				}
+				if( newway != null )   // Made changes?
+				{
+					// If no segments left, delete the way
+					if( newway.segments.size() == 0 )
+						cmds.add(makeDeleteCommand(w));
+					else
+						cmds.add(new ChangeCommand(w,newway));
+				}
+			}
+			cmds.add(new DeleteCommand(seglist));
+		}
+
+		cmds.add(new DeleteCommand(nodes));
+		Main.main.editLayer().add(new SequenceCommand(tr("Merge Nodes"), cmds));
+		Main.map.repaint();
+	}
+	private DeleteCommand makeDeleteCommand(OsmPrimitive obj)
+	{
+	  return new DeleteCommand(Arrays.asList(new OsmPrimitive[]{obj}));
+	}
+}
+
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergeWaysAction.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergeWaysAction.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/MergeWaysAction.java	(revision 5075)
@@ -0,0 +1,94 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Collection;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.plugins.Plugin;
+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 MergeWaysAction extends AbstractAction {
+	public MergeWaysAction() {
+		super("Merge Ways");
+	}
+	public void actionPerformed(ActionEvent e) {
+		Collection<OsmPrimitive> sel = Main.ds.getSelected();
+		Collection<OsmPrimitive> ways = new ArrayList<OsmPrimitive>();
+		Way target = null;
+		for (OsmPrimitive osm : sel)
+			if (osm instanceof Way)
+				ways.add(osm);
+		if (ways.size() < 2) {
+			javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Must select at least two ways."));
+			return;
+		}
+		for ( OsmPrimitive o : ways )
+		{
+			Way w = (Way)o;
+			if( target == null || target.id == 0 )
+			{
+				target = w;
+				continue;
+			}
+			if( w.id == 0 )
+				continue;
+			if( w.id < target.id )
+				target = w;
+		}
+//		System.out.println( "Selected: "+target.toString() );
+		ways.remove(target);
+		
+		// target is what we're merging into
+		// ways is the list of ways to be removed
+				
+		// Now do the merging
+		Collection<Command> cmds = new LinkedList<Command>();
+		Way newway = new Way(target);
+		for (final OsmPrimitive o : ways) 
+		{
+			Way w = (Way)o;
+			if( w.deleted || w.isIncomplete() )
+			{
+				javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Ways must exist and be complete."));
+				return;
+			}
+			for ( String key : w.keySet() )
+			{
+				if( newway.keys.containsKey(key) && !newway.get(key).equals(w.get(key)) )
+				{
+					javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Ways have conflicting key: "+key+"["+newway.get(key)+","+w.get(key)+"]"));
+					return;
+				}
+				newway.put( key, w.get(key) ); 
+			}
+			for (final Segment s : w.segments)
+			{
+				if( !newway.segments.contains( s ) )
+					newway.segments.add( s );
+			}
+		}
+
+		cmds.add(new ChangeCommand(target, newway));
+		cmds.add(new DeleteCommand(ways));
+		Main.main.editLayer().add(new SequenceCommand(tr("Merge Ways"), cmds));
+		Main.map.repaint();
+	}
+    }
+
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/SimplifyWayAction.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/SimplifyWayAction.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/SimplifyWayAction.java	(revision 5075)
@@ -0,0 +1,447 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+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.SelectionChangedListener;
+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.Segment;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.Visitor;
+
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.actions.JosmAction;
+
+/**
+ * Forgets the selected data, unless it is referenced by something.
+ * 
+ * "Forgetting", as opposed to "deleting", means that the data is simply removed from JOSM, and
+ * not tagged as "to be deleted on server".
+ *
+ * - selected WAYS can always be forgotten.
+ * - selected SEGMENTS can be forgotten unless they are referenced by not-forgotten ways.
+ * - selected NODES can be forgotten unless they are referenced by not-forgotten segments.
+ */
+
+public class SimplifyWayAction extends JosmAction implements SelectionChangedListener {
+
+	
+	private Way selectedWay = null;
+
+	/**
+	 * Create a new SimplifyWayAction.
+	 */
+	public SimplifyWayAction() {
+		super(tr("Simplify Way"), "simplify", tr("Delete low-information nodes from a way."), 0, 0, true);
+		try { Main.ds.addSelectionChangedListener(this); }
+		catch( NoSuchMethodError e )
+		{
+			try {
+			java.lang.reflect.Field f = DataSet.class.getDeclaredField("listeners");
+			((Collection<SelectionChangedListener>)f.get(Main.ds)).add(this);
+//			Main.ds.listeners.add(this);
+			} catch (Exception x) { System.out.println( e ); }
+		}
+	}
+
+	/**
+	 * Called when the action is executed.
+	 */
+	public void actionPerformed(ActionEvent e) {
+
+		Collection<OsmPrimitive> selection = Main.ds.getSelected();
+
+
+		Visitor selectVisitor = new Visitor(){
+			public void visit(Node n) {
+            }
+			public void visit(Segment s) {
+            }
+			public void visit(Way w) {
+				selectedWay = w;
+            }
+		};
+		
+		for (OsmPrimitive p : selection)
+			p.visit(selectVisitor);
+		
+		simplifyWay(selectedWay);
+	}
+
+	private class NodeRecord {
+		public boolean keep = false; // whether this node must be kept
+		public Node node; // the node
+		public NodeRecord previous; // the segment leading to this node
+		public double xte; // the cross-track error
+		public NodeRecord next;
+	}
+	
+	/**
+	 * Simplifies the given way by potentially removing nodes and segments.
+	 * 
+	 * @param way
+	 * @return true if simplification was successful (even if way was not changed)
+	 *         false if simplification was not possible (branching/unordered ways)
+	 */
+	public boolean simplifyWay(Way way) {
+		
+		// first build some structures that help us working with this way, assuming
+		// it might be very long, so we want to be efficient.
+		
+		// a map holding one NodeRecord object for every node in the way, except 
+		// the first node (which is never "simplified" anyway) 
+		HashMap<Node,NodeRecord> nodeIndex = new HashMap<Node,NodeRecord>();
+		
+		// a hash set containing all segments in this way, for fast is-in-way checks
+		HashSet<Segment> segmentIndex = new HashSet<Segment>();
+		
+		// in addition to all this, we also have each NodeRecord pointing
+		// to the next one along the way, making a linked list.
+		NodeRecord firstNr = null;
+		
+		// fill structures
+		NodeRecord prevNr = null;
+		for (Segment s : way.segments) {
+			if ((prevNr != null) && (!s.from.equals(prevNr.node))) {
+				// error
+				System.out.println("XXX err");
+				return false;
+			}
+			segmentIndex.add(s);
+			NodeRecord nr = new NodeRecord();
+			nr.node = s.to;
+			if (prevNr == null) {
+				nr.previous = new NodeRecord();
+				nr.previous.node = s.from;
+				// set "keep" on first node
+				nr.previous.keep = true;
+				firstNr = nr.previous;
+				firstNr.next = nr;
+				nodeIndex.put(s.from, nr.previous);
+			} else {
+				nr.previous = prevNr;
+				prevNr.next = nr;
+			}
+			nr.xte = 0;
+			nr.next = null;
+			prevNr = nr;
+			nodeIndex.put(s.to, nr);
+		}
+		
+		// set "keep" on last node
+		prevNr.keep = true;
+		
+		// check the current data set, and mark all nodes that are used by a segment
+		// not exclusively owned by the current way as "untouchable".
+		for (Segment s: Main.ds.segments) {
+			if (s.deleted) continue;
+			if (segmentIndex.contains(s)) continue; // these don't count
+			NodeRecord tmp;
+			tmp = nodeIndex.get(s.from); if (tmp != null) tmp.keep = true;
+			tmp = nodeIndex.get(s.to); if (tmp != null) tmp.keep = true;
+		}
+		
+		for (Way w: Main.ds.ways) {
+			if (w.deleted) continue; 
+			if (w.equals(way)) continue; // these don't count
+			for (Segment s: w.segments)
+			{
+				NodeRecord tmp;
+				tmp = nodeIndex.get(s.from); if (tmp != null) tmp.keep = true;
+				tmp = nodeIndex.get(s.to); if (tmp != null) tmp.keep = true;
+			}
+		}
+		
+		// keep all nodes which have tags other than source and created_by
+		for (NodeRecord nr : nodeIndex.values()) {
+			Collection<String> keyset = nr.node.keySet();
+			keyset.remove("source");
+			keyset.remove("created_by");
+			if (!keyset.isEmpty()) nr.keep = true;
+		}
+		
+		// compute cross-track error for all elements. cross-track error is the
+		// distance between a node and the nearest point on a line from the 
+		// previous to the next node - that's the error you would introduce
+		// by removing the node.
+		for (NodeRecord r = firstNr; r.next != null; r = r.next) {
+			computeXte(r);
+		}
+		
+		boolean stayInLoop = true;
+		double treshold = Double.parseDouble(Main.pref.get("simplify-way.max-error", "0.06"));
+		while(stayInLoop) {
+			NodeRecord[] sorted = new NodeRecord[nodeIndex.size()];
+			nodeIndex.values().toArray(sorted);
+			Arrays.sort(sorted, new Comparator<NodeRecord>() {
+				public int compare(NodeRecord a, NodeRecord b) {
+					return (a.xte < b.xte) ? -1 : (a.xte > b.xte) ? 1 : 0;
+				}
+			});
+
+			stayInLoop = false;
+			for (NodeRecord nr : sorted) {
+				if (nr.keep) continue;
+				if (nr.xte < treshold) {
+					// delete this node
+					nodeIndex.remove(nr.node);
+					if (nr == firstNr) {
+						firstNr = nr.next;
+					} else {
+						nr.previous.next = nr.next;
+					}
+					if (nr.next != null) {
+						nr.next.previous = nr.previous;
+					}
+					computeXte(nr.next);
+					computeXte(nr.previous);
+					stayInLoop = true;
+				}
+				break;
+			}
+		}
+		
+		Segment currentOriginalSegment = null;
+		Segment currentModifiedSegment = null;
+		Way wayCopy = null;
+		int delCount = 0;
+		Collection<Command> cmds = new LinkedList<Command>();
+		
+		for (Segment s : way.segments) {
+			if (currentOriginalSegment == null) {
+				currentOriginalSegment = s;
+				currentModifiedSegment = s;
+				continue;
+			}
+			
+			if (nodeIndex.containsKey(s.from)) {
+				// the current remaining segment's "to" node is not
+				// deleted, so it may stay.
+				if (currentModifiedSegment != currentOriginalSegment) {
+					cmds.add(new ChangeCommand(currentOriginalSegment, currentModifiedSegment));
+				}
+				currentOriginalSegment = s;
+				currentModifiedSegment = s;
+			} else {
+				// the "to" node is to be deleted; delete segment and 
+				// node
+				cmds.add(new DeleteCommand(Arrays.asList(new OsmPrimitive[]{s, s.from})));
+				delCount ++;
+				if (wayCopy == null) {
+					wayCopy = new Way(way);
+				}
+				wayCopy.segments.remove(s);
+				if (currentModifiedSegment == currentOriginalSegment) {
+					currentModifiedSegment = new Segment(currentOriginalSegment);
+				}
+				currentModifiedSegment.to = s.to;
+			}
+		}
+		if (currentModifiedSegment != currentOriginalSegment) {
+			cmds.add(new ChangeCommand(currentOriginalSegment, currentModifiedSegment));
+		}
+		
+		if (wayCopy != null) {
+			cmds.add(new ChangeCommand(way, wayCopy));
+			Main.main.editLayer().add(new SequenceCommand(tr("Simplify Way (remove {0} nodes)", delCount), cmds));
+		}
+		
+		return true;
+		
+	}
+	public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
+		setEnabled(!newSelection.isEmpty());
+	}
+
+	private static void computeXte(NodeRecord r) {
+		if ((r.previous == null) || (r.next == null)) {
+			r.xte = 0;
+			return;
+		}
+		Node prevNode = r.previous.node;
+		Node nextNode = r.next.node;
+		r.xte = radtomiles(linedist(prevNode.coor.lat(), prevNode.coor.lon(),
+			r.node.coor.lat(), r.node.coor.lon(),
+			nextNode.coor.lat(), nextNode.coor.lon()));
+	}
+	
+	/* ---------------------------------------------------------------------- 
+	 * 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/UtilsPlugin/SuperSelectionAction.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/SuperSelectionAction.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/SuperSelectionAction.java	(revision 5075)
@@ -0,0 +1,293 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.mapmode.*;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.GroupAction;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Segment;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.NavigatableComponent;
+import org.openstreetmap.josm.gui.SelectionManager;
+import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * This MapMode enables the user to easy make a selection of different objects.
+ *
+ * The selected objects are drawn in a different style.
+ *
+ * Holding and dragging the left mouse button draws an selection rectangle.
+ * When releasing the left mouse button, all objects within the rectangle get
+ * selected.
+ *
+ * When releasing the left mouse button while the right mouse button pressed,
+ * nothing happens (the selection rectangle will be cleared, however).
+ *
+ * When releasing the mouse button and one of the following keys was hold:
+ *
+ * If Alt key was hold, select all objects that are touched by the
+ * selection rectangle. If the Alt key was not hold, select only those objects
+ * completly within (e.g. for ways mean: only if all nodes of the way are
+ * within).
+ *
+ * If Shift key was hold, the objects are added to the current selection. If
+ * Shift key wasn't hold, the current selection get replaced.
+ *
+ * If Ctrl key was hold, remove all objects under the current rectangle from
+ * the active selection (if there were any). Nothing is added to the current
+ * selection.
+ *
+ * Alt can be combined with Ctrl or Shift. Ctrl and Shift cannot be combined.
+ * If both are pressed, nothing happens when releasing the mouse button.
+ *
+ * The user can also only click on the map. All total movements of 2 or less
+ * pixel are considered "only click". If that happens, the nearest Node will
+ * be selected if there is any within 10 pixel range. If there is no Node within
+ * 10 pixel, the nearest Segment (or Street, if user hold down the Alt-Key)
+ * within 10 pixel range is selected. If there is no Segment within 10 pixel
+ * and the user clicked in or 10 pixel away from an area, this area is selected.
+ * If there is even no area, nothing is selected. Shift and Ctrl key applies to
+ * this as usual. For more, @see MapView#getNearest(Point, boolean)
+ *
+ * @author imi
+ */
+public class SuperSelectionAction extends MapMode implements SelectionEnded {
+
+	enum Mode {select, straight}
+	private final Mode mode;
+
+	public static class Group extends GroupAction {
+		public Group(MapFrame mf) {
+			super(KeyEvent.VK_S,0);
+			putValue("help", "Action/Selection");
+			actions.add(new SuperSelectionAction(mf, tr("Selection"), Mode.select, tr("Select objects by dragging or clicking.")));
+			actions.add(new SuperSelectionAction(mf, tr("Straight line"), Mode.straight, tr("Select objects in a straight line.")));
+			setCurrent(0);
+//			System.out.println( "In SuperSelectionAction!!!" );
+		}
+	}
+
+
+	/**
+	 * The SelectionManager that manages the selection rectangle.
+	 */
+	private SelectionManager selectionManager;
+
+	private Node straightStart = null;
+	private Node lastEnd = null;
+	private Collection<OsmPrimitive> oldSelection = null;
+
+	//TODO: Implement reverse references into data objects and remove this
+	private final Map<Node, Collection<Segment>> reverseSegmentMap = new HashMap<Node, Collection<Segment>>();
+
+	/**
+	 * Create a new SelectionAction in the given frame.
+	 * @param mapFrame The frame this action belongs to
+	 */
+	public SuperSelectionAction(MapFrame mapFrame, String name, Mode mode, String desc) {
+		super(name, "selection/"+mode, desc, mapFrame, ImageProvider.getCursor("normal", "selection"));
+		this.mode = mode;
+		putValue("help", "Action/Selection/"+Character.toUpperCase(mode.toString().charAt(0))+mode.toString().substring(1));
+		this.selectionManager = new SelectionManager(this, false, mapFrame.mapView);
+	}
+
+	@Override public void enterMode() {
+		super.enterMode();
+//		System.out.println( "enterMode()" );
+		if (mode == Mode.select)
+		{
+			selectionManager.register(Main.map.mapView);
+			Main.map.mapView.addMouseListener(this);
+		}
+		else {
+			Main.map.mapView.addMouseMotionListener(this);
+			Main.map.mapView.addMouseListener(this);
+			for (Segment s : Main.ds.segments) {
+				addBackReference(s.from, s);
+				addBackReference(s.to, s);
+			}
+		}
+	}
+
+	private void addBackReference(Node n, Segment s) {
+		Collection<Segment> c = reverseSegmentMap.get(n);
+		if (c == null) {
+			c = new HashSet<Segment>();
+			reverseSegmentMap.put(n, c);
+		}
+		c.add(s);
+	}
+
+	@Override public void exitMode() {
+		super.exitMode();
+//		System.out.println( "exitMode()" );
+		if (mode == Mode.select)
+		{
+			selectionManager.unregister(Main.map.mapView);
+			Main.map.mapView.removeMouseListener(this);
+		}
+		else {
+			Main.map.mapView.removeMouseMotionListener(this);
+			Main.map.mapView.removeMouseListener(this);
+			reverseSegmentMap.clear();
+		}
+	}
+
+
+	/**
+	 * Check the state of the keys and buttons and set the selection accordingly.
+	 */
+	public void selectionEnded(Rectangle r, boolean alt, boolean shift, boolean ctrl) {
+//		System.out.println( "selectionEnded() r="+r );
+		selectEverythingInRectangle(selectionManager, r, alt, shift, ctrl);
+	}
+
+	public static void selectEverythingInRectangle(SelectionManager selectionManager, Rectangle r, boolean alt, boolean shift, boolean ctrl) {
+	    if (shift && ctrl)
+			return; // not allowed together
+
+		Collection<OsmPrimitive> curSel;
+		if (!ctrl && !shift)
+			curSel = new LinkedList<OsmPrimitive>(); // new selection will replace the old.
+		else
+			curSel = Main.ds.getSelected();
+
+		Collection<OsmPrimitive> selectionList = selectionManager.getObjectsInRectangle(r,alt);
+		for (OsmPrimitive osm : selectionList)
+			if (ctrl)
+				curSel.remove(osm);
+			else
+				curSel.add(osm);
+		Main.ds.setSelected(curSel);
+		Main.map.mapView.repaint();
+    }
+
+	@Override public void mouseDragged(MouseEvent e) {
+		if( mode == Mode.select )
+			return;
+//		System.out.println( "mouseDragged()" );
+		Node old = lastEnd;
+		lastEnd = Main.map.mapView.getNearestNode(e.getPoint());
+		if (straightStart == null)
+			straightStart = lastEnd;
+		if (straightStart != null && lastEnd != null && straightStart != lastEnd && old != lastEnd) {
+			Collection<OsmPrimitive> path = new HashSet<OsmPrimitive>();
+			Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>();
+			path.add(straightStart);
+			calculateShortestPath(path, straightStart, lastEnd);
+			if ((e.getModifiers() & MouseEvent.CTRL_MASK) != 0) {
+				sel.addAll(oldSelection);
+				sel.removeAll(path);
+			} else if ((e.getModifiers() & MouseEvent.SHIFT_MASK) != 0) {
+				sel = path;
+				sel.addAll(oldSelection);
+			} else
+				sel = path;
+			Main.ds.setSelected(sel);
+		}
+	}
+
+	@Override public void mousePressed(MouseEvent e) {
+		if( mode == Mode.select )
+			return;
+		straightStart = Main.map.mapView.getNearestNode(e.getPoint());
+		lastEnd = null;
+		oldSelection = Main.ds.getSelected();
+//		System.out.println( "mousePressed: count="+e.getClickCount() );
+	}
+
+	@Override public void mouseReleased(MouseEvent e) {
+		if( mode == Mode.select )
+		{
+			if( e.getClickCount() < 2 )
+				return;
+				
+//			System.out.println( "Got doubleclick!!" );
+			// We want all of the matching type under the mouse
+			// right now. The algorithm is fairly simple: we use
+			// getNearest to get the nearest object that the
+			// user would normally expect to be selected.  The
+			// we do a getAllNearest which does almost what we
+			// want. We just toss out everything that's not the
+			// same type as what was selected.
+			
+			NavigatableComponent nc = Main.map.mapView;
+
+			Point center = e.getPoint();
+			OsmPrimitive osm = nc.getNearest(center, (e.getModifiersEx() & MouseEvent.ALT_DOWN_MASK) != 0 );
+			Collection<OsmPrimitive> set = nc.getAllNearest(center);
+			Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>();
+			Collection<OsmPrimitive> oldsel = Main.ds.getSelected();
+			
+			for (OsmPrimitive o : set )
+			{
+				if( osm.getClass() == o.getClass() )
+					sel.add( o );
+			}
+			if( (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0 )
+				oldsel.addAll( sel );
+			else if( (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0 )
+				oldsel.removeAll( sel );
+			else
+				oldsel = sel;
+			Main.ds.setSelected( oldsel );
+			return;
+		}
+		straightStart = null;
+		lastEnd = null;
+		oldSelection = null;
+	}
+
+	/**
+	 * Get the shortest path by stepping through the node with a common segment with start
+	 * and nearest to the end (greedy algorithm).
+	 */
+	private void calculateShortestPath(Collection<OsmPrimitive> path, Node start, Node end) {
+		for (Node pivot = start; pivot != null;)
+			pivot = addNearest(path, pivot, end);
+	}
+
+	private Node addNearest(Collection<OsmPrimitive> path, Node start, Node end) {
+		Collection<Segment> c = reverseSegmentMap.get(start);
+		if (c == null)
+			return null; // start may be a waypoint without segments
+		double min = Double.MAX_VALUE;
+		Node next = null;
+		Segment seg = null;
+		for (Segment s : c) {
+			Node other = s.from == start ? s.to : s.from;
+			if (other == end) {
+				next = other;
+				seg = s;
+				min = 0;
+				break;
+			}
+			double distance = other.eastNorth.distance(end.eastNorth);
+			if (distance < min) {
+				min = distance;
+				next = other;
+				seg = s;
+			}
+		}
+		if (min < start.eastNorth.distance(end.eastNorth) && next != null) {
+			path.add(next);
+			path.add(seg);
+			return next;
+		}
+		return null;
+	}
+}
Index: applications/editors/josm/plugins/utilsplugin/UtilsPlugin/UtilsPlugin.java
===================================================================
--- applications/editors/josm/plugins/utilsplugin/UtilsPlugin/UtilsPlugin.java	(revision 5075)
+++ applications/editors/josm/plugins/utilsplugin/UtilsPlugin/UtilsPlugin.java	(revision 5075)
@@ -0,0 +1,89 @@
+package UtilsPlugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import UtilsPlugin.*;
+//import UtilsPlugin.JosmLint.JosmLint;
+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 {
+
+    private JMenu toolsMenu;
+    private JMenuItem mergePointsMenu = new JMenuItem(new MergePointsAction());
+    private JMenuItem mergePointLineMenu = new JMenuItem(new MergePointLineAction());
+    private JMenuItem mergeWaysMenu = new JMenuItem(new MergeWaysAction());
+    private JMenuItem deduplicateWayMenu =  new JMenuItem(new DeduplicateWayAction());
+    private JMenuItem simplifyWayMenu =  new JMenuItem(new SimplifyWayAction());
+        
+    public UtilsPlugin() {
+	JMenuBar menu = Main.main.menu;
+	toolsMenu = menu.getMenu(4);
+/*
+	This code doesn't work, because getName returns null always, so we get two menus
+	
+	for (int i = 0; i < menu.getMenuCount(); ++i) {
+	    javax.swing.JOptionPane.showMessageDialog(Main.parent, tr("Menu ["+menu.getMenu(i).getName()+","+tr("Edit")+"]"));
+	    if (menu.getMenu(i) != null && tr("Edit").equals(menu.getMenu(i).getName())) {
+		editMenu = menu.getMenu(i);
+		break;
+	    }
+	}
+*/
+	if (toolsMenu == null) {
+	    toolsMenu = new JMenu(tr("Tools"));
+	    menu.add(toolsMenu, 5);
+	    toolsMenu.setVisible(false);
+	}
+	toolsMenu.add(mergePointsMenu);
+	toolsMenu.add(mergePointLineMenu);
+	toolsMenu.add(mergeWaysMenu);
+	toolsMenu.add(deduplicateWayMenu);
+	toolsMenu.add(simplifyWayMenu);
+	mergePointsMenu.setVisible(false);
+	mergePointLineMenu.setVisible(false);
+	mergeWaysMenu.setVisible(false);
+	deduplicateWayMenu.setVisible(false);
+	simplifyWayMenu.setVisible(false);
+    }
+	@Override
+	public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+		if (oldFrame != null && newFrame == null) {
+			// disable
+			mergePointsMenu.setVisible(false);
+			mergePointLineMenu.setVisible(false);
+			mergeWaysMenu.setVisible(false);
+			deduplicateWayMenu.setVisible(false);
+			simplifyWayMenu.setVisible(false);
+//			JosmLint.stopPlugin();
+			if (toolsMenu.getMenuComponentCount() == 4)
+				toolsMenu.setVisible(false);
+		} else if (oldFrame == null && newFrame != null) {
+			// enable
+			mergePointsMenu.setVisible(true);
+			mergePointLineMenu.setVisible(true);
+			mergeWaysMenu.setVisible(true);
+			deduplicateWayMenu.setVisible(true);
+			simplifyWayMenu.setVisible(true);
+
+//			JosmLint.setupPlugin();
+			
+			if (toolsMenu.getMenuComponentCount() == 4)
+				toolsMenu.setVisible(true);
+		}
+	}
+
+}
