Index: applications/editors/josm/plugins/CommandLine/build.xml
===================================================================
--- applications/editors/josm/plugins/CommandLine/build.xml	(revision 34573)
+++ applications/editors/josm/plugins/CommandLine/build.xml	(revision 34574)
@@ -10,5 +10,5 @@
     -->
     <property name="plugin.author" value="Hind"/>
-    <property name="plugin.class" value="CommandLine.CommandLine"/>
+    <property name="plugin.class" value="org.openstreetmap.josm.plugins.commandline.CommandLine"/>
     <property name="plugin.description" value="Implements a command line and enables to create your commands. See link for standard commands (arc, circle etc.)"/>
     <property name="plugin.icon" value="images/commandline.png"/>
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/AbstractOsmAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/AbstractOsmAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/AbstractOsmAction.java	(revision 34574)
@@ -0,0 +1,167 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.awt.AWTEvent;
+import java.awt.Cursor;
+import java.awt.EventQueue;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Logging;
+
+public abstract class AbstractOsmAction<T extends OsmPrimitive> extends MapMode implements AWTEventListener {
+    private final CommandLine parentPlugin;
+    private final Cursor cursorNormal;
+    private final Cursor cursorActive;
+    private Cursor currentCursor;
+    private Point mousePos;
+    private T nearestPrimitive;
+    private boolean isCtrlDown;
+
+    protected AbstractOsmAction(CommandLine parentPlugin, String activeCursorIcon) {
+        super(null, "addsegment.png", null, ImageProvider.getCursor("normal", "selection"));
+        this.parentPlugin = parentPlugin;
+        cursorNormal = ImageProvider.getCursor("normal", "selection");
+        cursorActive = ImageProvider.getCursor("normal", activeCursorIcon);
+        currentCursor = cursorNormal;
+        nearestPrimitive = null;
+    }
+
+    @Override public void enterMode() {
+        super.enterMode();
+        currentCursor = cursorNormal;
+        MainApplication.getMap().mapView.addMouseListener(this);
+        MainApplication.getMap().mapView.addMouseMotionListener(this);
+        try {
+            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
+        } catch (SecurityException ex) {
+            Logging.warn(ex);
+        }
+    }
+
+    @Override public void exitMode() {
+        super.exitMode();
+        MainApplication.getMap().mapView.removeMouseListener(this);
+        MainApplication.getMap().mapView.removeMouseMotionListener(this);
+        try {
+            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
+        } catch (SecurityException ex) {
+            Logging.warn(ex);
+        }
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+        if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
+            return;
+        processMouseEvent(e);
+        updCursor();
+        MainApplication.getMap().mapView.repaint();
+        super.mouseMoved(e);
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+        MapFrame map = MainApplication.getMap();
+        if (!map.mapView.isActiveLayerDrawable())
+            return;
+        processMouseEvent(e);
+        if (nearestPrimitive != null) {
+            DataSet ds = MainApplication.getLayerManager().getEditDataSet();
+            if (isCtrlDown) {
+                ds.clearSelection(nearestPrimitive);
+                map.mapView.repaint();
+            } else {
+                int maxInstances = parentPlugin.currentCommand.parameters.get(parentPlugin.currentCommand.currentParameterNum).maxInstances;
+                switch (maxInstances) {
+                case 0:
+                    ds.addSelected(nearestPrimitive);
+                    map.mapView.repaint();
+                    break;
+                case 1:
+                    ds.addSelected(nearestPrimitive);
+                    map.mapView.repaint();
+                    parentPlugin.loadParameter(nearestPrimitive, true);
+                    break;
+                default:
+                    if (ds.getSelected().size() < maxInstances) {
+                        ds.addSelected(nearestPrimitive);
+                        map.mapView.repaint();
+                    } else
+                        parentPlugin.printHistory("Maximum instances is " + maxInstances);
+                }
+            }
+        }
+        super.mousePressed(e);
+    }
+
+    @Override
+    public void eventDispatched(AWTEvent arg0) {
+        if (!(arg0 instanceof KeyEvent))
+            return;
+        KeyEvent ev = (KeyEvent) arg0;
+        isCtrlDown = (ev.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0;
+        if (ev.getKeyCode() == KeyEvent.VK_ESCAPE && ev.getID() == KeyEvent.KEY_PRESSED) {
+            ev.consume();
+            cancelDrawing();
+        }
+    }
+
+    private void updCursor() {
+        if (mousePos != null) {
+            if (!MainApplication.isDisplayingMapView())
+                return;
+            nearestPrimitive = getNearest(mousePos);
+            if (nearestPrimitive != null) {
+                setCursor(cursorActive);
+            } else {
+                setCursor(cursorNormal);
+            }
+        }
+    }
+
+    protected abstract T getNearest(Point mousePos);
+
+    private void processMouseEvent(MouseEvent e) {
+        if (e != null) {
+            mousePos = e.getPoint();
+        }
+    }
+
+    private void setCursor(final Cursor c) {
+        if (currentCursor.equals(c))
+            return;
+        try {
+            // We invoke this to prevent strange things from happening
+            EventQueue.invokeLater(() -> {
+                // Don't change cursor when mode has changed already
+                if (!AbstractOsmAction.this.getClass().isAssignableFrom(MainApplication.getMap().mapMode.getClass()))
+                    return;
+                MainApplication.getMap().mapView.setCursor(c);
+            });
+            currentCursor = c;
+        } catch (Exception e) {
+            Logging.warn(e);
+        }
+    }
+
+    public void cancelDrawing() {
+        MapFrame map = MainApplication.getMap();
+        if (map == null || map.mapView == null)
+            return;
+        map.statusLine.setHeading(-1);
+        map.statusLine.setAngle(-1);
+        map.mapView.repaint();
+        updateStatusLine();
+        parentPlugin.abortInput();
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/AnyAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/AnyAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/AnyAction.java	(revision 34574)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.awt.Point;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.MainApplication;
+
+public class AnyAction extends AbstractOsmAction<OsmPrimitive> {
+
+    public AnyAction(CommandLine parentPlugin) {
+        super(parentPlugin, "joinnode");
+    }
+
+    @Override
+    protected OsmPrimitive getNearest(Point mousePos) {
+        return MainApplication.getMap().mapView.getNearestNodeOrWay(mousePos, OsmPrimitive::isUsable, false);
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Command.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Command.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Command.java	(revision 34574)
@@ -0,0 +1,195 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+
+public class Command {
+    public String name;                         // Command name
+    public String run;                          // Executable file with arguments ("nya.exe {arg1} {arg2} ... {argn}")
+    public String icon;                         // Icon file name
+    public ArrayList<Parameter> parameters;    // Required parameters list
+    public ArrayList<Parameter> optParameters;    // Optional parameters list
+    public int currentParameterNum;
+    public boolean tracks;
+    public boolean asynchronous;
+
+    public Command() {
+        parameters = new ArrayList<>();
+        optParameters = new ArrayList<>();
+        currentParameterNum = 0;
+        tracks = false;
+        asynchronous = false;
+        icon = "";
+    }
+
+    @SuppressWarnings("unchecked")
+    public boolean loadObject(Object obj) {
+        Parameter currentParameter = parameters.get(currentParameterNum);
+        if (currentParameter.maxInstances == 1) {
+            if (isPair(obj, currentParameter)) {
+                currentParameter.setValue(obj);
+                return true;
+            }
+        } else {
+            ArrayList<OsmPrimitive> multiValue = currentParameter.getValueList();
+            if (obj instanceof Collection) {
+                if (((Collection<?>) obj).size() > currentParameter.maxInstances && currentParameter.maxInstances != 0)
+                    return false;
+                multiValue.clear();
+                multiValue.addAll((Collection<OsmPrimitive>) obj);
+                return true;
+            } else if (obj instanceof OsmPrimitive) {
+                if (multiValue.size() < currentParameter.maxInstances || currentParameter.maxInstances == 0) {
+                    if (isPair(obj, currentParameter)) {
+                        multiValue.add((OsmPrimitive) obj);
+                        return true;
+                    } else if (nextParameter() && multiValue.size() > 0) {
+                        return loadObject(obj);
+                    }
+                } else if (nextParameter()) {
+                    return loadObject(obj);
+                }
+            } else if (obj instanceof String) {
+                if (isPair(obj, currentParameter)) {
+                    currentParameter.setValue(obj);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public boolean nextParameter() {
+        currentParameterNum++;
+        return (currentParameterNum < parameters.size()) ? true : false;
+    }
+
+    public boolean hasNextParameter() {
+        return ((currentParameterNum + 1) < parameters.size()) ? true : false;
+    }
+
+    public void resetLoading() {
+        currentParameterNum = 0;
+        for (Parameter parameter : parameters) {
+            if (parameter.maxInstances != 1)
+                parameter.getValueList().clear();
+        }
+    }
+
+    private static boolean isPair(Object obj, Parameter parameter) {
+        switch (parameter.type) {
+        case POINT:
+            if (obj instanceof String) {
+                Pattern p = Pattern.compile("(-?\\d*\\.?\\d*,-?\\d*\\.?\\d*;?)*");
+                Matcher m = p.matcher((String) obj);
+                return m.matches();
+            }
+            break;
+        case NODE:
+            if (obj instanceof Node) return true;
+            break;
+        case WAY:
+            if (obj instanceof Way) return true;
+            break;
+        case RELATION:
+            if (obj instanceof Relation) return true;
+            break;
+        case ANY:
+            if (obj instanceof Node || obj instanceof Way || obj instanceof Relation) return true;
+            break;
+        case LENGTH:
+            if (obj instanceof String) {
+                Pattern p = Pattern.compile("\\d*\\.?\\d*");
+                Matcher m = p.matcher((String) obj);
+                if (m.matches()) {
+                    Float value = Float.parseFloat((String) obj);
+                    if (parameter.minVal != 0 && value < parameter.minVal)
+                        break;
+                    if (parameter.maxVal != 0 && value > parameter.maxVal)
+                        break;
+                    return true;
+                }
+            }
+            break;
+        case NATURAL:
+            if (obj instanceof String) {
+                Pattern p = Pattern.compile("\\d*");
+                Matcher m = p.matcher((String) obj);
+                if (m.matches()) {
+                    Integer value = Integer.parseInt((String) obj);
+                    if (parameter.minVal != 0 && value < parameter.minVal)
+                        break;
+                    if (parameter.maxVal != 0 && value > parameter.maxVal)
+                        break;
+                    return true;
+                }
+            }
+            break;
+        case STRING:
+            if (obj instanceof String) return true;
+            break;
+        case RELAY:
+            if (obj instanceof String) {
+                if (parameter.getRawValue() instanceof Relay) {
+                    if (((Relay) (parameter.getRawValue())).isCorrectValue((String) obj))
+                        return true;
+                }
+            }
+            break;
+        case USERNAME:
+            if (obj instanceof String) return true;
+            break;
+        case IMAGERYURL:
+            if (obj instanceof String) return true;
+            break;
+        case IMAGERYOFFSET:
+            if (obj instanceof String) return true;
+            break;
+        default:
+            break;
+        }
+        return false;
+    }
+
+    public Collection<OsmPrimitive> getDepsObjects() {
+        ArrayList<OsmPrimitive> depsObjects = new ArrayList<>();
+        for (Parameter parameter : parameters) {
+            if (!parameter.isOsm())
+                continue;
+            if (parameter.maxInstances == 1) {
+                depsObjects.addAll(getDepsObjects(depsObjects, (OsmPrimitive) parameter.getRawValue()));
+            } else {
+                for (OsmPrimitive primitive : parameter.getValueList()) {
+                    depsObjects.addAll(getDepsObjects(depsObjects, primitive));
+                }
+            }
+        }
+        return depsObjects;
+    }
+
+    public Collection<OsmPrimitive> getDepsObjects(Collection<OsmPrimitive> currentObjects, OsmPrimitive primitive) {
+        ArrayList<OsmPrimitive> depsObjects = new ArrayList<>();
+        if (!currentObjects.contains(primitive)) {
+            if (primitive instanceof Way) {
+                depsObjects.addAll(((Way) primitive).getNodes());
+            } else if (primitive instanceof Relation) {
+                Collection<OsmPrimitive> relationMembers = ((Relation) primitive).getMemberPrimitives();
+                for (OsmPrimitive member : relationMembers) {
+                    if (!currentObjects.contains(member)) {
+                        depsObjects.add(member);
+                        depsObjects.addAll(getDepsObjects(currentObjects, member));
+                    }
+                }
+            }
+        }
+        return depsObjects;
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandAction.java	(revision 34574)
@@ -0,0 +1,40 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+
+import javax.swing.Action;
+
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public class CommandAction extends JosmAction {
+    private final CommandLine parentPlugin;
+    private final Command parentCommand;
+    public CommandAction(Command parentCommand, CommandLine parentPlugin) {
+        super(tr(parentCommand.name), "blankmenu", tr(parentCommand.name), null, true, parentCommand.name, true);
+        if (!parentCommand.icon.equals("")) {
+            try {
+                putIcons(CommandLine.pluginDir, parentCommand.icon);
+            } catch (RuntimeException e) {
+                putIcons(null, "blankmenu");
+            }
+        }
+
+        this.parentCommand = parentCommand;
+        this.parentPlugin = parentPlugin;
+    }
+
+    private void putIcons(String subdir, String name) {
+        putValue(Action.SMALL_ICON, new ImageProvider(subdir, name).setSize(ImageProvider.ImageSizes.SMALLICON).get());
+        putValue(Action.LARGE_ICON_KEY, new ImageProvider(subdir, name).setSize(ImageProvider.ImageSizes.LARGEICON).get());
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        parentPlugin.startCommand(parentCommand);
+        parentPlugin.history.addItem(parentCommand.name);
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandLine.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandLine.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandLine.java	(revision 34574)
@@ -0,0 +1,671 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.GraphicsEnvironment;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import javax.swing.JMenu;
+import javax.swing.JOptionPane;
+import javax.swing.JTextField;
+import javax.swing.JToolBar;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.UndoRedoHandler;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.osm.BBox;
+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.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
+import org.openstreetmap.josm.gui.layer.GpxLayer;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
+import org.openstreetmap.josm.io.GpxWriter;
+import org.openstreetmap.josm.io.OsmWriter;
+import org.openstreetmap.josm.io.OsmWriterFactory;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.SubclassFilteredCollection;
+import org.openstreetmap.josm.tools.Utils;
+
+public class CommandLine extends Plugin {
+    protected JTextField textField;
+    protected JTextField historyField;
+    private String prefix;
+    private Mode mode;
+    private ArrayList<Command> commands;
+    private JMenu commandMenu;
+    protected Command currentCommand;
+    protected String commandSymbol;
+    protected History history;
+    protected MapFrame currentMapFrame;
+    protected MapMode previousMode;
+
+    static final String pluginDir = Preferences.main().getPluginsDirectory().getAbsolutePath() + "/CommandLine/";
+
+    public CommandLine(PluginInformation info) {
+        super(info);
+        commandSymbol = ": ";
+        history = new History(100);
+        historyField = new DisableShortcutsOnFocusGainedTextField();
+        textField = new CommandTextField();
+
+        MainMenu mainMenu = MainApplication.getMenu();
+        if (mainMenu != null) {
+            commandMenu = mainMenu.addMenu("Commands", tr("Commands"), KeyEvent.VK_O,
+                    mainMenu.getDefaultMenuPos(), ht("/Plugin/CommandLine"));
+            MainMenu.add(commandMenu, new CommandLineAction(this));
+        }
+        loadCommands();
+        setMode(Mode.IDLE);
+    }
+
+    public void startCommand(String commandName) {
+        Command command = findCommand(commandName, true);
+        if (command != null) {
+            startCommand(command);
+        }
+    }
+
+    protected void startCommand(Command command) {
+        MapFrame map = MainApplication.getMap();
+        if (map == null)
+            return;
+        DataSet ds = MainApplication.getLayerManager().getEditDataSet();
+        if (ds == null)
+            return;
+        currentCommand = command;
+        currentCommand.resetLoading();
+        parseSelection(ds.getSelected());
+        if (!(map.mapMode instanceof AnyAction
+           || map.mapMode instanceof DummyAction
+           || map.mapMode instanceof LengthAction
+           || map.mapMode instanceof NodeAction
+           || map.mapMode instanceof PointAction
+           || map.mapMode instanceof RelationAction
+           || map.mapMode instanceof WayAction)) {
+            previousMode = map.mapMode;
+        }
+        if (currentCommand.currentParameterNum < currentCommand.parameters.size())
+            setMode(Mode.SELECTION);
+        else
+            runTool();
+    }
+
+    @Override
+    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+        currentMapFrame = newFrame;
+        if (oldFrame == null && newFrame != null) {
+            JToolBar tb = new JToolBar();
+            tb.setLayout(new BorderLayout());
+            tb.setFloatable(false);
+            tb.setOrientation(JToolBar.HORIZONTAL);
+            tb.add(historyField, BorderLayout.NORTH);
+            tb.add(textField, BorderLayout.SOUTH);
+            currentMapFrame.add(tb, BorderLayout.NORTH);
+            printHistory("Loaded CommandLine, version " + getPluginInformation().version);
+        }
+    }
+
+    protected void printHistory(final String text) {
+        SwingUtilities.invokeLater(() -> historyField.setText(text));
+    }
+
+    private void loadCommands() {
+        commands = (new Loader(getPluginDirs().getUserDataDirectory(false))).load();
+        if (commands.isEmpty()) {
+            if (!GraphicsEnvironment.isHeadless() && JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(MainApplication.getMainFrame(),
+                    tr("No command has been found. Would you like to download and install default commands now?"),
+                    tr("No command found"), JOptionPane.YES_NO_CANCEL_OPTION)) {
+                try {
+                    downloadAndInstallDefaultCommands();
+                    commands = (new Loader(getPluginDirs().getUserDataDirectory(false))).load();
+                    JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("Default commands have been successfully installed"),
+                            tr("Success"), JOptionPane.INFORMATION_MESSAGE);
+                } catch (IOException e) {
+                    Logging.warn(e);
+                    JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
+                            tr("Failed to download and install default commands.\n\nError: {0}", e.getMessage()),
+                            tr("Warning"), JOptionPane.WARNING_MESSAGE);
+                }
+            }
+        }
+        for (Command command : commands) {
+            commandMenu.add(new CommandAction(command, this));
+        }
+    }
+
+    private void downloadAndInstallDefaultCommands() throws IOException {
+        String url = Config.getPref().get("commandline.default.commands.url",
+                "https://github.com/Foxhind/JOSM-CommandLine-commands/archive/master.zip");
+        try (ZipInputStream zis = new ZipInputStream(HttpClient.create(new URL(url)).connect().getContent(), StandardCharsets.UTF_8)) {
+            File dir = getPluginDirs().getUserDataDirectory(false);
+            if (!dir.exists()) {
+                dir.mkdirs();
+            }
+            ZipEntry entry = null;
+            while ((entry = zis.getNextEntry()) != null) {
+                if (!entry.isDirectory()) {
+                    String name = entry.getName();
+                    if (name.contains("/")) {
+                        name = name.substring(name.lastIndexOf("/"));
+                    }
+                    File file = new File(dir + File.separator + name);
+                    Logging.info("Installing command file: "+file);
+                    if (!file.createNewFile()) {
+                        throw new IOException("Could not create file: " + file.getAbsolutePath());
+                    }
+                    // Write file
+                    Files.copy(zis, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+                    // Set last modification date
+                    long time = entry.getTime();
+                    if (time > -1) {
+                        file.setLastModified(time);
+                    }
+                }
+            }
+        }
+    }
+
+    private Command findCommand(String text, boolean strict) {
+        for (int i = 0; i < commands.size(); i++) {
+            if (strict) {
+                if (commands.get(i).name.equalsIgnoreCase(text)) {
+                    return commands.get(i);
+                }
+            } else if (commands.get(i).name.toLowerCase().startsWith(text.toLowerCase()) && text.length() > 1) {
+                return commands.get(i);
+            }
+        }
+        return null;
+    }
+
+    protected void setMode(Mode targetMode) {
+        DataSet currentDataSet = MainApplication.getLayerManager().getEditDataSet();
+        if (currentDataSet != null) {
+            currentDataSet.clearSelection();
+            MainApplication.getMap().mapView.repaint();
+        }
+        if (targetMode == Mode.IDLE) {
+            mode = Mode.IDLE;
+            currentCommand = null;
+            prefix = tr("Command") + commandSymbol;
+            textField.setText(prefix);
+        } else if (targetMode == Mode.SELECTION) {
+            mode = Mode.SELECTION;
+            Parameter currentParameter = currentCommand.parameters.get(currentCommand.currentParameterNum);
+            prefix = tr(currentParameter.description == null ? currentParameter.name : currentParameter.description);
+            if (currentParameter.getRawValue() instanceof Relay)
+                prefix = prefix + " (" + ((Relay) (currentParameter.getRawValue())).getOptionsString() + ")";
+            prefix += commandSymbol;
+            String value = currentParameter.getValue();
+            textField.setText(prefix + value);
+            Type currentType = currentParameter.type;
+            MapMode action = null;
+            switch (currentType) {
+            case POINT:
+                action = new PointAction(this);
+                break;
+            case WAY:
+                action = new WayAction(this);
+                break;
+            case NODE:
+                action = new NodeAction(this);
+                break;
+            case RELATION:
+                action = new RelationAction(this);
+                break;
+            case ANY:
+                action = new AnyAction(this);
+                break;
+            case LENGTH:
+                action = new LengthAction(this);
+                break;
+            case USERNAME:
+                loadParameter(Config.getPref().get("osm-server.username", null), true);
+                action = new DummyAction(this);
+                break;
+            case IMAGERYURL:
+                Layer layer = MainApplication.getLayerManager().getActiveLayer();
+                if (layer != null) {
+                    if (!(layer instanceof ImageryLayer)) {
+                        List<ImageryLayer> imageryLayers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class);
+                        if (imageryLayers.size() == 1) {
+                            layer = imageryLayers.get(0);
+                        } else {
+                            endInput();
+                            return;
+                        }
+                    }
+                }
+                if (layer != null) {
+                    ImageryInfo info = ((ImageryLayer) layer).getInfo();
+                    String url = info.getUrl();
+                    loadParameter(url.isEmpty() ? info.getImageryType().getTypeString() : url, true);
+                }
+                action = new DummyAction(this);
+                break;
+            case IMAGERYOFFSET:
+                Layer olayer = MainApplication.getLayerManager().getActiveLayer();
+                if (olayer != null) {
+                    if (!(olayer instanceof AbstractTileSourceLayer)) {
+                        @SuppressWarnings("rawtypes")
+                        List<AbstractTileSourceLayer> imageryLayers = MainApplication.getLayerManager().getLayersOfType(
+                                AbstractTileSourceLayer.class);
+                        if (imageryLayers.size() == 1) {
+                            olayer = imageryLayers.get(0);
+                        } else {
+                            endInput();
+                            return;
+                        }
+                    }
+                }
+                loadParameter((String.valueOf(((AbstractTileSourceLayer<?>) olayer).getDisplaySettings().getDx()) + "," +
+                               String.valueOf(((AbstractTileSourceLayer<?>) olayer).getDisplaySettings().getDy())), true);
+                action = new DummyAction(this);
+                break;
+            default:
+                action = new DummyAction(this);
+                break;
+            }
+            currentMapFrame.selectMapMode(action);
+            activate();
+            textField.select(prefix.length(), textField.getText().length());
+        } else if (targetMode == Mode.PROCESSING) {
+            mode = Mode.PROCESSING;
+            prefix = tr("Processing...");
+            textField.setText(prefix);
+            MainApplication.getMap().mapView.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+        }
+    }
+
+    public void activate() {
+        textField.requestFocus();
+        textField.setCaretPosition(textField.getText().length());
+    }
+
+    public void deactivate() {
+        MainApplication.getMap().mapView.requestFocus();
+    }
+
+    public void abortInput() {
+        printHistory(tr("Aborted") + ".");
+        endInput();
+    }
+
+    public void endInput() {
+        setMode(Mode.IDLE);
+        MainApplication.getMap().selectMapMode(previousMode);
+        MainApplication.getMap().mapView.repaint();
+    }
+
+    public void loadParameter(Object obj, boolean next) {
+        if (currentCommand.loadObject(obj)) {
+            if (currentCommand.hasNextParameter()) {
+                if (next) {
+                    Parameter currentParameter = currentCommand.parameters.get(currentCommand.currentParameterNum);
+                    String prefix = tr(currentParameter.description == null ? currentParameter.name : currentParameter.description);
+                    prefix += commandSymbol;
+                    String value = currentParameter.getValue();
+                    printHistory(prefix + value);
+                    currentCommand.nextParameter();
+                    setMode(Mode.SELECTION);
+                }
+            } else {
+                runTool();
+            }
+        } else {
+            Logging.info("Invalid argument");
+            endInput();
+        }
+    }
+
+    private void parseSelection(Collection<OsmPrimitive> selection) {
+        boolean ok = false;
+        for (OsmPrimitive obj : selection) {
+            ok = currentCommand.loadObject(obj);
+            if (!ok)
+                break;
+        }
+        if (ok) {
+            currentCommand.nextParameter();
+        } else {
+            currentCommand.resetLoading();
+        }
+    }
+
+    private final class CommandTextField extends DisableShortcutsOnFocusGainedTextField {
+        @Override
+        protected void processKeyEvent(KeyEvent e) {
+            if (e.getID() == KeyEvent.KEY_PRESSED) {
+                int code = e.getKeyCode();
+                if (code == KeyEvent.VK_ENTER) {
+                    String commandText = textField.getText().substring(prefix.length());
+                    switch (mode) {
+                    case IDLE:
+                        if (commandText.isEmpty()) {
+                            commandText = history.getLastItem();
+                        } else {
+                            history.addItem(commandText);
+                        }
+                        Command command = findCommand(commandText, true);
+                        if (command != null) {
+                            startCommand(command);
+                        } else {
+                            setMode(Mode.IDLE);
+                        }
+                        break;
+                    case SELECTION:
+                        if (currentMapFrame.mapMode instanceof WayAction
+                         || currentMapFrame.mapMode instanceof NodeAction
+                         || currentMapFrame.mapMode instanceof RelationAction
+                         || currentMapFrame.mapMode instanceof AnyAction) {
+                            Collection<OsmPrimitive> selected = MainApplication.getLayerManager().getEditDataSet().getSelected();
+                            if (!selected.isEmpty())
+                                loadParameter(selected, true);
+                        } else {
+                            loadParameter(commandText, currentCommand.parameters.get(currentCommand.currentParameterNum).maxInstances == 1);
+                        }
+                        break;
+                    case ADJUSTMENT:
+                    default:
+                        break;
+                    }
+                    e.consume();
+                } else if (code == KeyEvent.VK_UP) {
+                    textField.setText(prefix + history.getPrevItem());
+                    e.consume();
+                } else if (code == KeyEvent.VK_DOWN) {
+                    textField.setText(prefix + history.getNextItem());
+                    e.consume();
+                } else if (code == KeyEvent.VK_BACK_SPACE || code == KeyEvent.VK_LEFT) {
+                    if (textField.getCaretPosition() <= prefix.length())
+                        e.consume();
+                } else if (code == KeyEvent.VK_HOME) {
+                    setCaretPosition(prefix.length());
+                    e.consume();
+                } else if (code == KeyEvent.VK_ESCAPE) {
+                    if (textField.getText().length() == prefix.length() && mode == Mode.IDLE)
+                        deactivate();
+                    else
+                        endInput();
+                    e.consume();
+                } else if (code == KeyEvent.VK_DELETE || code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_END) {
+                } else {
+                    e.consume();
+                }
+                if (textField.getCaretPosition() < prefix.length() ||
+                        (textField.getSelectionStart() < prefix.length() && textField.getSelectionStart() > 0))
+                    e.consume();
+            }
+            if (e.getID() == KeyEvent.KEY_TYPED)
+                if (textField.getCaretPosition() < prefix.length() ||
+                        (textField.getSelectionStart() < prefix.length() && textField.getSelectionStart() > 0))
+                    e.consume();
+            super.processKeyEvent(e);
+            if (textField.getText().length() < prefix.length()) { // Safe
+                setMode(mode);
+            }
+            if (e.getID() == KeyEvent.KEY_TYPED) {
+                if (e.getKeyChar() > 'A' && e.getKeyChar() < 'z') {
+                    Command command = findCommand(textField.getText().substring(prefix.length()), false);
+                    if (command != null) {
+                        int currentPos = textField.getSelectionStart() == 0 ? textField.getCaretPosition() : textField.getSelectionStart();
+                        textField.setText(prefix + command.name);
+                        textField.setCaretPosition(currentPos);
+                        textField.select(currentPos, prefix.length() + command.name.length());
+                    }
+                }
+            }
+        }
+
+        @Override
+        protected void processMouseEvent(MouseEvent e) {
+            super.processMouseEvent(e);
+            if (e.getButton() == MouseEvent.BUTTON1 && e.getID() == MouseEvent.MOUSE_RELEASED) {
+                if (textField.getSelectionStart() > 0 && textField.getSelectionStart() < prefix.length())
+                    textField.setSelectionStart(prefix.length());
+                else if (textField.getCaretPosition() < prefix.length())
+                    textField.setCaretPosition(prefix.length());
+            }
+        }
+    }
+
+    private static class ToolProcess {
+        public Process process;
+        public volatile boolean running;
+    }
+
+    // Thanks to Upliner
+    public void runTool() {
+        setMode(Mode.PROCESSING);
+        String commandToRun = currentCommand.run;
+        final boolean tracks = currentCommand.tracks;
+        final ArrayList<Parameter> parameters = currentCommand.parameters;
+
+        for (Parameter parameter : currentCommand.parameters) {
+            commandToRun = commandToRun.replace("{" + parameter.name + "}", parameter.getValue());
+        }
+        for (Parameter parameter : currentCommand.optParameters) {
+            commandToRun = commandToRun.replace("{" + parameter.name + "}", parameter.getValue());
+        }
+        String[] listToRun = commandToRun.split(" ");
+
+        // create the process
+        final Object syncObj = new Object();
+
+        ProcessBuilder builder;
+        builder = new ProcessBuilder(listToRun);
+        builder.directory(getPluginDirs().getUserDataDirectory(false));
+
+        final StringBuilder debugstr = new StringBuilder();
+
+        // debug: print resulting cmdline
+        for (String s : builder.command()) {
+            debugstr.append(s + " ");
+        }
+        debugstr.append("\n");
+        Logging.info(debugstr.toString());
+
+        final ToolProcess tp = new ToolProcess();
+        try {
+            tp.process = builder.start();
+        } catch (final IOException e) {
+            synchronized (debugstr) {
+                Logging.error(
+                        tr("Error executing the script:") + ' ' +
+                        debugstr.toString() + e.getMessage() + '\n' + Arrays.toString(e.getStackTrace()));
+            }
+            return;
+        }
+        tp.running = true;
+
+        // redirect child process's stderr to JOSM stderr
+        new Thread(() -> {
+            try {
+                byte[] buffer = new byte[1024];
+                InputStream errStream = tp.process.getErrorStream();
+                int len;
+                while ((len = errStream.read(buffer)) > 0) {
+                    synchronized (debugstr) {
+                        debugstr.append(new String(buffer, 0, len, StandardCharsets.UTF_8));
+                    }
+                    System.err.write(buffer, 0, len);
+                }
+            } catch (IOException e) {
+                Logging.warn(e);
+            }
+        }).start();
+
+        // Write stdin stream
+        Thread osmWriteThread = new Thread(() -> {
+            BBox bbox = null;
+            final OutputStream outputStream = tp.process.getOutputStream();
+            PrintWriter printWriter = null;
+            try {
+                printWriter = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
+            } catch (Exception e1) {
+                Logging.error(e1);
+            }
+            final OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(printWriter, true, null);
+            Collection<OsmPrimitive> refObjects = currentCommand.getDepsObjects();
+            Collection<OsmPrimitive> pObjects;
+            osmWriter.header();
+            Collection<OsmPrimitive> contents = new ArrayList<>();
+            for (OsmPrimitive primitive1 : refObjects) {
+                contents.add(primitive1);
+                if (bbox == null)
+                    bbox = new BBox(primitive1.getBBox());
+                else
+                    bbox.addPrimitive(primitive1, 0.0);
+            }
+            osmWriter.writeNodes(new SubclassFilteredCollection<OsmPrimitive, Node>(contents, Node.class::isInstance));
+            osmWriter.writeWays(new SubclassFilteredCollection<OsmPrimitive, Way>(contents, Way.class::isInstance));
+            osmWriter.writeRelations(new SubclassFilteredCollection<OsmPrimitive, Relation>(contents, Relation.class::isInstance));
+            osmWriter.footer();
+            osmWriter.flush();
+
+            for (Parameter parameter : parameters) {
+                if (!parameter.isOsm())
+                    continue;
+                contents = new ArrayList<>();
+                osmWriter.header();
+                pObjects = parameter.getParameterObjects();
+                for (OsmPrimitive primitive2 : pObjects) {
+                    contents.add(primitive2);
+                    if (bbox == null)
+                        bbox = new BBox(primitive2.getBBox());
+                    else
+                        bbox.addPrimitive(primitive2, 0.0);
+                }
+                osmWriter.writeNodes(new SubclassFilteredCollection<OsmPrimitive, Node>(contents, Node.class::isInstance));
+                osmWriter.writeWays(new SubclassFilteredCollection<OsmPrimitive, Way>(contents, Way.class::isInstance));
+                osmWriter.writeRelations(new SubclassFilteredCollection<OsmPrimitive, Relation>(contents, Relation.class::isInstance));
+                osmWriter.footer();
+                osmWriter.flush();
+            }
+
+            if (tracks) {
+                try (GpxWriter gpxWriter = new GpxWriter(printWriter)) {
+                    GpxFilter gpxFilter = new GpxFilter();
+                    gpxFilter.initBboxFilter(bbox);
+                    List<GpxLayer> gpxLayers = MainApplication.getLayerManager().getLayersOfType(GpxLayer.class);
+                    for (GpxLayer gpxLayer : gpxLayers) {
+                        gpxFilter.addGpxData(gpxLayer.data);
+                    }
+                    gpxWriter.write(gpxFilter.getGpxData());
+                } catch (IOException e2) {
+                    Logging.warn(e2);
+                }
+            }
+            Utils.close(osmWriter);
+            synchronized (syncObj) {
+                if (currentCommand.asynchronous) {
+                    tp.running = false;
+                    syncObj.notifyAll();
+                }
+            }
+        });
+
+        // Read stdout stream
+        final DataSet currentDataSet = MainApplication.getLayerManager().getEditDataSet();
+        final CommandLine that = this;
+        Thread osmParseThread = new Thread(() -> {
+            try {
+                final OsmToCmd osmToCmd = new OsmToCmd(that, currentDataSet);
+                String commandName = currentCommand.name;
+                final InputStream inputStream = tp.process.getInputStream();
+                osmToCmd.parseStream(inputStream);
+                final List<org.openstreetmap.josm.command.Command> cmdlist = osmToCmd.getCommandList();
+                if (!cmdlist.isEmpty()) {
+                    final SequenceCommand cmd = new SequenceCommand(commandName, cmdlist);
+                    SwingUtilities.invokeLater(() -> UndoRedoHandler.getInstance().add(cmd));
+                }
+            } catch (Exception e) {
+                Logging.warn(e);
+            } finally {
+                synchronized (syncObj) {
+                    tp.running = false;
+                    syncObj.notifyAll();
+                }
+            }
+        });
+
+        osmParseThread.start();
+        osmWriteThread.start();
+
+        synchronized (syncObj) {
+            try {
+                syncObj.wait(Config.getPref().getInt("commandline.timeout", 20000));
+            } catch (InterruptedException e) {
+                Logging.warn(e);
+            }
+        }
+        if (tp.running) {
+            new Thread(new PleaseWaitRunnable(currentCommand.name) {
+                @Override
+                protected void realRun() {
+                    try {
+                        progressMonitor.indeterminateSubTask(null);
+                        synchronized (syncObj) {
+                            if (tp.running)
+                                syncObj.wait();
+                        }
+                    } catch (InterruptedException e) {
+                        Logging.warn(e);
+                    }
+                }
+
+                @Override
+                protected void cancel() {
+                    synchronized (syncObj) {
+                        tp.running = false;
+                        tp.process.destroy();
+                        syncObj.notifyAll();
+                        endInput();
+                    }
+                }
+
+                @Override
+                protected void finish() {
+                }
+            }).start();
+        }
+        endInput();
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandLineAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandLineAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/CommandLineAction.java	(revision 34574)
@@ -0,0 +1,25 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.tools.Shortcut;
+
+public class CommandLineAction extends JosmAction {
+    private final CommandLine parentPlugin;
+
+    public CommandLineAction(CommandLine parentPlugin) {
+        super(tr("Command line"), "commandline", tr("Set input focus to the command line."),
+                Shortcut.registerShortcut("tool:commandline", tr("Tool: {0}", tr("Command line")), KeyEvent.VK_ENTER, Shortcut.DIRECT), true, "commandline", true);
+        this.parentPlugin = parentPlugin;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        parentPlugin.activate();
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/DummyAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/DummyAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/DummyAction.java	(revision 34574)
@@ -0,0 +1,42 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.awt.AWTEvent;
+import java.awt.event.AWTEventListener;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public class DummyAction extends MapMode implements AWTEventListener {
+    private final CommandLine parentPlugin;
+
+    public DummyAction(CommandLine parentPlugin) {
+            super(null, "addsegment.png", null, ImageProvider.getCursor("normal", null));
+            this.parentPlugin = parentPlugin;
+    }
+
+    @Override
+    public void eventDispatched(AWTEvent arg0) {
+        if (!(arg0 instanceof KeyEvent))
+                return;
+        KeyEvent ev = (KeyEvent) arg0;
+        if (ev.getKeyCode() == KeyEvent.VK_ESCAPE && ev.getID() == KeyEvent.KEY_PRESSED) {
+            ev.consume();
+            cancelDrawing();
+        }
+    }
+
+    public void cancelDrawing() {
+        if (!MainApplication.isDisplayingMapView())
+            return;
+        MapFrame map = MainApplication.getMap();
+        map.statusLine.setHeading(-1);
+        map.statusLine.setAngle(-1);
+        map.mapView.repaint();
+        updateStatusLine();
+        parentPlugin.abortInput();
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/GpxFilter.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/GpxFilter.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/GpxFilter.java	(revision 34574)
@@ -0,0 +1,57 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.openstreetmap.josm.data.gpx.GpxData;
+import org.openstreetmap.josm.data.gpx.GpxTrack;
+import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
+import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
+import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.osm.BBox;
+
+public class GpxFilter {
+    private BBox bbox;
+    private final GpxData data;
+
+    public GpxFilter() {
+        bbox = new BBox(0.0, 0.0, 0.0, 0.0);
+        data = new GpxData();
+    }
+
+    public void initBboxFilter(BBox bbox) {
+        this.bbox = bbox;
+    }
+
+    public void addGpxData(GpxData data) {
+        Collection<Collection<WayPoint>> currentTrack;
+        Collection<WayPoint> currentSegment;
+        for (GpxTrack track : data.tracks) {
+            currentTrack = new ArrayList<>();
+            for (GpxTrackSegment segment : track.getSegments()) {
+                currentSegment = new ArrayList<>();
+                for (WayPoint wp : segment.getWayPoints()) {
+                    if (bbox.bounds(wp.getCoor())) {
+                        currentSegment.add(wp);
+                    } else {
+                        if (currentSegment.size() > 1) {
+                            currentTrack.add(currentSegment);
+                            currentSegment = new ArrayList<>();
+                        }
+                    }
+                }
+                if (currentSegment.size() > 1) {
+                    currentTrack.add(currentSegment);
+                    currentSegment = new ArrayList<>();
+                }
+            }
+            this.data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
+        }
+    }
+
+    public GpxData getGpxData() {
+        return data;
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/History.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/History.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/History.java	(revision 34574)
@@ -0,0 +1,60 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.util.LinkedList;
+
+public class History {
+    private final LinkedList<String> historyList;
+    private final int maxLen;
+    private int num;
+
+    public History(int len) {
+        num = 0;
+        maxLen = len;
+        historyList = new LinkedList<>();
+    }
+
+    public void addItem(String item) {
+        if (!item.equals("")) {
+            String prevItem = historyList.peekFirst();
+            if (prevItem == null) {
+                historyList.addFirst(item);
+            } else {
+                if (!prevItem.equalsIgnoreCase(item))
+                    historyList.addFirst(item);
+            }
+            if (historyList.size() > maxLen) {
+                historyList.removeLast();
+            }
+        }
+        num = -1;
+    }
+
+    public String getPrevItem() {
+        num += 1;
+        if (num >= historyList.size()) {
+            num = historyList.size() - 1;
+        }
+        if (num < 0) {
+            num = -1;
+            return "";
+        }
+        return historyList.get(num);
+    }
+
+    public String getLastItem() {
+        if (historyList.size() > 0)
+            return historyList.get(0);
+        return "";
+    }
+
+    public String getNextItem() {
+        num -= 1;
+        if (num < 0) {
+            num = -1;
+            return "";
+        }
+        return historyList.get(num);
+    }
+}
+
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/LengthAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/LengthAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/LengthAction.java	(revision 34574)
@@ -0,0 +1,250 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.AWTEvent;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.EventQueue;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.awt.geom.GeneralPath;
+
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.data.Bounds;
+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.preferences.NamedColorProperty;
+import org.openstreetmap.josm.gui.MainApplication;
+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.MapViewPaintable;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Logging;
+
+public class LengthAction extends MapMode implements MapViewPaintable, AWTEventListener {
+    private final CommandLine parentPlugin;
+    private final Cursor cursorCrosshair;
+    private final Cursor cursorJoinNode;
+    private Cursor currentCursor;
+    private final Color selectedColor;
+    private Point drawStartPos;
+    private Point drawEndPos;
+    private LatLon startCoor;
+    private LatLon endCoor;
+    private Point mousePos;
+    private Node nearestNode;
+    private boolean drawing;
+
+    public LengthAction(CommandLine parentPlugin) {
+        super(null, "addsegment.png", null, ImageProvider.getCursor("crosshair", null));
+        this.parentPlugin = parentPlugin;
+        selectedColor = new NamedColorProperty(marktr("selected"), Color.red).get();
+        cursorCrosshair = ImageProvider.getCursor("crosshair", null);
+        cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
+        currentCursor = cursorCrosshair;
+        nearestNode = null;
+    }
+
+    @Override
+    public void enterMode() {
+        super.enterMode();
+        MapView mapView = MainApplication.getMap().mapView;
+        mapView.addMouseListener(this);
+        mapView.addMouseMotionListener(this);
+        mapView.addTemporaryLayer(this);
+        try {
+            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
+        } catch (SecurityException ex) {
+            Logging.warn(ex);
+        }
+    }
+
+    @Override
+    public void exitMode() {
+        super.exitMode();
+        MapView mapView = MainApplication.getMap().mapView;
+        mapView.removeMouseListener(this);
+        mapView.removeMouseMotionListener(this);
+        mapView.removeTemporaryLayer(this);
+        try {
+            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
+        } catch (SecurityException ex) {
+            Logging.warn(ex);
+        }
+        if (drawing)
+            mapView.repaint();
+    }
+
+    public void cancelDrawing() {
+        MapFrame map = MainApplication.getMap();
+        if (map == null || map.mapView == null)
+            return;
+        map.statusLine.setHeading(-1);
+        map.statusLine.setAngle(-1);
+        updateStatusLine();
+        parentPlugin.abortInput();
+    }
+
+    @Override
+    public void eventDispatched(AWTEvent event) {
+        if (!(event instanceof KeyEvent))
+            return;
+        KeyEvent ev = (KeyEvent) event;
+        if (ev.getKeyCode() == KeyEvent.VK_ESCAPE && ev.getID() == KeyEvent.KEY_PRESSED) {
+            if (drawing)
+                ev.consume();
+            cancelDrawing();
+        }
+    }
+
+    private void processMouseEvent(MouseEvent e) {
+        if (e != null) {
+            mousePos = e.getPoint();
+        }
+    }
+
+    @Override
+    public void paint(Graphics2D g, MapView mv, Bounds bbox) {
+        if (!drawing)
+            return;
+
+        g.setColor(selectedColor);
+        g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+
+        GeneralPath b = new GeneralPath();
+        Point pp1 = drawStartPos;
+        Point pp2 = drawEndPos;
+
+        b.moveTo(pp1.x, pp1.y);
+        b.lineTo(pp2.x, pp2.y);
+        g.draw(b);
+
+        g.setStroke(new BasicStroke(1));
+    }
+
+    private void drawingStart(MouseEvent e) {
+        mousePos = e.getPoint();
+        if (nearestNode != null) {
+            drawStartPos = MainApplication.getMap().mapView.getPoint(nearestNode.getCoor());
+        } else {
+            drawStartPos = mousePos;
+        }
+        drawEndPos = drawStartPos;
+        startCoor = MainApplication.getMap().mapView.getLatLon(drawStartPos.x, drawStartPos.y);
+        endCoor = startCoor;
+        drawing = true;
+        updateStatusLine();
+    }
+
+    private void drawingFinish() {
+        parentPlugin.loadParameter(String.valueOf(startCoor.greatCircleDistance(endCoor)), true);
+        drawStartPos = null;
+        drawing = false;
+        exitMode();
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+        if (e.getButton() == MouseEvent.BUTTON1) {
+            if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
+                return;
+            requestFocusInMapView();
+            drawingStart(e);
+        } else
+            drawing = false;
+    }
+
+    @Override
+    public void mouseReleased(MouseEvent e) {
+        if (e.getButton() != MouseEvent.BUTTON1)
+            return;
+        if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
+            return;
+        boolean dragged = true;
+        if (drawStartPos != null)
+            dragged = drawEndPos.distance(drawStartPos) > 10;
+            if (drawing && dragged)
+                drawingFinish();
+            drawing = false;
+    }
+
+    @Override
+    public void mouseDragged(MouseEvent e) {
+        processMouseEvent(e);
+        updCursor();
+        MapFrame map = MainApplication.getMap();
+        if (nearestNode != null)
+            drawEndPos = map.mapView.getPoint(nearestNode.getCoor());
+        else
+            drawEndPos = mousePos;
+        endCoor = map.mapView.getLatLon(drawEndPos.x, drawEndPos.y);
+        if (drawing) {
+            map.statusLine.setDist(startCoor.greatCircleDistance(endCoor));
+            map.mapView.repaint();
+        }
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+        if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
+            return;
+        processMouseEvent(e);
+        updCursor();
+        if (drawing)
+            MainApplication.getMap().mapView.repaint();
+    }
+
+    @Override
+    public String getModeHelpText() {
+        if (drawing)
+            return tr("Point on the start");
+        else
+            return tr("Point on the end");
+    }
+
+    @Override
+    public boolean layerIsSupported(Layer l) {
+        return l instanceof OsmDataLayer;
+    }
+
+    private void updCursor() {
+        if (mousePos != null) {
+            if (!MainApplication.isDisplayingMapView())
+                return;
+            nearestNode = MainApplication.getMap().mapView.getNearestNode(mousePos, OsmPrimitive::isUsable);
+            if (nearestNode != null) {
+                setCursor(cursorJoinNode);
+            } else {
+                setCursor(cursorCrosshair);
+            }
+        }
+    }
+
+    private void setCursor(final Cursor c) {
+        if (currentCursor.equals(c))
+            return;
+        try {
+            // We invoke this to prevent strange things from happening
+            EventQueue.invokeLater(() -> {
+                // Don't change cursor when mode has changed already
+                if (!(MainApplication.getMap().mapMode instanceof LengthAction))
+                    return;
+                MainApplication.getMap().mapView.setCursor(c);
+            });
+            currentCursor = c;
+        } catch (Exception e) {
+            Logging.warn(e);
+        }
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Loader.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Loader.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Loader.java	(revision 34574)
@@ -0,0 +1,168 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.openstreetmap.josm.tools.Logging;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class Loader extends DefaultHandler {
+    private final File dirToScan;
+    private String currentFile; // For debug XML-files
+    private String currentTag;
+    private Command currentCommand;
+    private Parameter currentParameter;
+    private final ArrayList<Command> loadingCommands;
+
+    public Loader(File dir) {
+        dirToScan = dir;
+        currentTag = "";
+        loadingCommands = new ArrayList<>();
+    }
+
+    public ArrayList<Command> load() {
+        try {
+            // Creating parser
+            SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
+
+            // Files loading
+            String[] list = dirToScan.list();
+            if (list != null) {
+                for (int i = 0; i < list.length; i++) {
+                    if (list[i].endsWith(".xml")) {
+                        currentFile = dirToScan.getPath() + "/" + list[i];
+                        loadFile(sp, currentFile);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Logging.error(e);
+        }
+        return loadingCommands;
+    }
+
+    private void loadFile(SAXParser parser, String fileName) {
+        try {
+            String a = new File(fileName).toURI().toString().replace("file:/", "file:///");
+            Logging.info(a);
+            parser.parse(a, this);
+        } catch (Exception e) {
+            Logging.error(e);
+        }
+        // TODO: Create links for each argument
+    }
+
+    @Override
+    public void startElement(String namespaceURI, String localName, String rawName, Attributes attrs) {
+        int len = attrs.getLength();
+        String Name, Value;
+        currentTag = rawName;
+
+        if (rawName.equals("command")) {
+            currentCommand = new Command();
+            for (int i = 0; i < len; i++) {
+                Name = attrs.getQName(i);
+                Value = attrs.getValue(i);
+                if (Name.equals("name"))
+                    currentCommand.name = Value;
+                else if (Name.equals("run"))
+                    currentCommand.run = Value;
+                else if (Name.equals("tracks")) {
+                    if (Value.equals("bbox"))
+                        currentCommand.tracks = true;
+                } else if (Name.equals("icon")) {
+                    currentCommand.icon = Value;
+                } else if (Name.equals("asynchronous")) {
+                    currentCommand.asynchronous = Value.equals("true") ? true : false;
+                }
+            }
+        } else if (rawName.equals("parameter")) {
+            currentParameter = new Parameter();
+            for (int i = 0; i < len; i++) {
+                Name = attrs.getQName(i);
+                Value = attrs.getValue(i);
+                if (Name.equals("required")) {
+                    currentParameter.required = Value.equals("true") ? true : false;
+                } else if (Name.equals("type")) {
+                    if (Value.equals("node")) currentParameter.type = Type.NODE;
+                    else if (Value.equals("way")) currentParameter.type = Type.WAY;
+                    else if (Value.equals("relation")) currentParameter.type = Type.RELATION;
+                    else if (Value.equals("point")) currentParameter.type = Type.POINT;
+                    else if (Value.equals("length")) currentParameter.type = Type.LENGTH;
+                    else if (Value.equals("natural")) currentParameter.type = Type.NATURAL;
+                    else if (Value.equals("any")) currentParameter.type = Type.ANY;
+                    else if (Value.equals("string")) currentParameter.type = Type.STRING;
+                    else if (Value.equals("relay")) currentParameter.type = Type.RELAY;
+                    else if (Value.equals("username")) currentParameter.type = Type.USERNAME;
+                    else if (Value.equals("imageryurl")) currentParameter.type = Type.IMAGERYURL;
+                    else if (Value.equals("imageryoffset")) currentParameter.type = Type.IMAGERYOFFSET;
+                } else if (Name.equals("maxinstances")) {
+                    currentParameter.maxInstances = Integer.parseInt(Value);
+                } else if (Name.equals("maxvalue")) {
+                    currentParameter.maxVal = Float.parseFloat(Value);
+                } else if (Name.equals("minvalue")) {
+                    currentParameter.minVal = Float.parseFloat(Value);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) {
+        String text = (new String(ch, start, length)).trim();
+        if (currentParameter != null) {
+            if (currentTag.equals("name")) {
+                currentParameter.name = text;
+            } else if (currentTag.equals("description")) {
+                currentParameter.description = text;
+            } else if (currentTag.equals("value")) {
+                if (currentParameter.type == Type.RELAY) {
+                    if (!(currentParameter.getRawValue() instanceof Relay))
+                        currentParameter.setValue(new Relay());
+                    ((Relay) currentParameter.getRawValue()).addValue(text);
+                } else {
+                    currentParameter.setValue(text);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String namespaceURI, String localName, String rawName) {
+        if (rawName.equals("command")) {
+            loadingCommands.add(currentCommand);
+            currentCommand = null;
+        } else if (rawName.equals("parameter")) {
+            if (currentParameter.required)
+                currentCommand.parameters.add(currentParameter);
+            else
+                currentCommand.optParameters.add(currentParameter);
+            currentParameter = null;
+        } else {
+            currentTag = "";
+        }
+    }
+
+    @Override
+    public void warning(SAXParseException ex) {
+        Logging.warn("Warning in command xml file " + currentFile + ": " + ex.getMessage());
+    }
+
+    @Override
+    public void error(SAXParseException ex) {
+        Logging.error("Error in command xml file " + currentFile + ": " + ex.getMessage());
+    }
+
+    @Override
+    public void fatalError(SAXParseException ex) throws SAXException {
+        Logging.error("Error in command xml file " + currentFile + ": " + ex.getMessage());
+        throw ex;
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Mode.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Mode.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Mode.java	(revision 34574)
@@ -0,0 +1,4 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+public enum Mode { IDLE, SELECTION, ADJUSTMENT, PROCESSING }
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/NodeAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/NodeAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/NodeAction.java	(revision 34574)
@@ -0,0 +1,20 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.awt.Point;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.MainApplication;
+
+public class NodeAction extends AbstractOsmAction<Node> {
+
+    public NodeAction(CommandLine parentPlugin) {
+        super(parentPlugin, "joinnode");
+    }
+
+    @Override
+    protected Node getNearest(Point mousePos) {
+        return MainApplication.getMap().mapView.getNearestNode(mousePos, OsmPrimitive::isUsable);
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/OsmToCmd.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/OsmToCmd.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/OsmToCmd.java	(revision 34574)
@@ -0,0 +1,393 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.openstreetmap.josm.command.AddCommand;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.NodeData;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.PrimitiveData;
+import org.openstreetmap.josm.data.osm.PrimitiveId;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationData;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
+import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WayData;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.io.IllegalDataException;
+import org.openstreetmap.josm.io.UTFInputStreamReader;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.XmlParsingException;
+import org.openstreetmap.josm.tools.date.DateUtils;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.ext.LexicalHandler;
+import org.xml.sax.helpers.DefaultHandler;
+
+final class OsmToCmd {
+    private final CommandLine parentPlugin;
+    private final DataSet targetDataSet;
+    private final LinkedList<Command> cmds = new LinkedList<>();
+    private final HashMap<PrimitiveId, OsmPrimitive> externalIdMap; // Maps external ids to internal primitives
+
+    OsmToCmd(CommandLine parentPlugin, DataSet targetDataSet) {
+        this.parentPlugin = parentPlugin;
+        this.targetDataSet = targetDataSet;
+        externalIdMap = new HashMap<>();
+    }
+
+    public void parseStream(InputStream stream) throws IllegalDataException {
+        try {
+            InputSource inputSource = new InputSource(UTFInputStreamReader.create(stream, "UTF-8"));
+            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
+            Parser handler = new Parser();
+            parser.setProperty("http://xml.org/sax/properties/lexical-handler", handler);
+            parser.parse(inputSource, handler);
+        } catch (ParserConfigurationException e) {
+            throw new IllegalDataException(e.getMessage(), e);
+        } catch (SAXParseException e) {
+            throw new IllegalDataException(tr("Line {0} column {1}: ", e.getLineNumber(), e.getColumnNumber()) + e.getMessage(), e);
+        } catch (SAXException e) {
+            throw new IllegalDataException(e.getMessage(), e);
+        } catch (Exception e) {
+            throw new IllegalDataException(e);
+        }
+    }
+
+    public LinkedList<Command> getCommandList() {
+        return cmds;
+    }
+
+    private class Parser extends DefaultHandler implements LexicalHandler {
+        private Locator locator;
+
+        @Override
+        public void setDocumentLocator(Locator locator) {
+            this.locator = locator;
+        }
+
+        protected void throwException(String msg) throws XmlParsingException {
+            throw new XmlParsingException(msg).rememberLocation(locator);
+        }
+
+        private OsmPrimitive currentPrimitive;
+        //private long currentExternalId;
+        private final List<Node> currentWayNodes = new ArrayList<>();
+        private final List<RelationMember> currentRelationMembers = new ArrayList<>();
+
+        @Override
+        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
+            try {
+                if (qName.equals("osm")) {
+                    if (atts == null) {
+                        throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm"));
+                        return;
+                    }
+                    String v = atts.getValue("version");
+                    if (v == null) {
+                        throwException(tr("Missing mandatory attribute ''{0}''.", "version"));
+                        return;
+                    }
+                    if (!(v.equals("0.6"))) {
+                        throwException(tr("Unsupported version: {0}", v));
+                        return;
+                    }
+
+                    // ---- PARSING NODES AND WAYS ----
+
+                } else if (qName.equals("node")) {
+                    Node n = new Node();
+                    NodeData source = new NodeData();
+                    source.setCoor(new LatLon(getDouble(atts, "lat"), getDouble(atts, "lon")));
+                    readCommon(atts, source);
+                    Node target = (Node) targetDataSet.getPrimitiveById(source.getUniqueId(), source.getType());
+
+                    if (target == null || !(source.isModified() || source.isDeleted()))
+                        n.load(source);
+                    else {
+                        n.cloneFrom(target);
+                        n.load(source);
+                    }
+
+                    currentPrimitive = n;
+                    externalIdMap.put(source.getPrimitiveId(), n);
+                } else if (qName.equals("way")) {
+                    Way w = new Way();
+                    WayData source = new WayData();
+                    readCommon(atts, source);
+                    Way target = (Way) targetDataSet.getPrimitiveById(source.getUniqueId(), source.getType());
+
+                    if (target == null || !(source.isModified() || source.isDeleted()))
+                        w.load(source);
+                    else {
+                        w.cloneFrom(target);
+                        w.load(source);
+                    }
+
+                    currentPrimitive = w;
+                    currentWayNodes.clear();
+                    externalIdMap.put(source.getPrimitiveId(), w);
+                } else if (qName.equals("nd")) {
+                    if (atts.getValue("ref") == null)
+                        throwException(tr("Missing mandatory attribute ''{0}'' on <nd> of way {1}.", "ref", currentPrimitive.getUniqueId()));
+                    long id = getLong(atts, "ref");
+                    if (id == 0)
+                        throwException(tr("Illegal value of attribute ''ref'' of element <nd>. Got {0}.", id));
+                    Node node = (Node) externalIdMap.get(new SimplePrimitiveId(id, OsmPrimitiveType.NODE));
+                    if (node == null || node.isModified()) {
+                        node = (Node) targetDataSet.getPrimitiveById(new SimplePrimitiveId(id, OsmPrimitiveType.NODE));
+                        if (node == null)
+                            throwException(tr("Missing definition of new object with id {0}.", id));
+                    }
+                    currentWayNodes.add(node);
+                } else if (qName.equals("relation")) { // ---- PARSING RELATIONS ----
+                    Relation r = new Relation();
+                    RelationData source = new RelationData();
+                    readCommon(atts, source);
+                    Relation target = (Relation) targetDataSet.getPrimitiveById(source.getUniqueId(), source.getType());
+
+                    if (target == null || !(source.isModified() || source.isDeleted()))
+                        r.load(source);
+                    else {
+                        r.cloneFrom(target);
+                        r.load(source);
+                    }
+
+                    currentPrimitive = r;
+                    currentRelationMembers.clear();
+                    externalIdMap.put(source.getPrimitiveId(), r);
+                } else if (qName.equals("member")) {
+                    if (atts.getValue("ref") == null)
+                        throwException(tr("Missing mandatory attribute ''{0}'' on <member> of relation {1}.",
+                                "ref", currentPrimitive.getUniqueId()));
+                    long id = getLong(atts, "ref");
+                    if (id == 0)
+                        throwException(tr("Illegal value of attribute ''ref'' of element <nd>. Got {0}.", id));
+
+                    OsmPrimitiveType type = OsmPrimitiveType.NODE;
+                    String value = atts.getValue("type");
+                    if (value == null) {
+                        throwException(tr("Missing attribute ''type'' on member {0} in relation {1}.", Long.toString(id),
+                                Long.toString(currentPrimitive.getUniqueId())));
+                    }
+                    try {
+                        type = OsmPrimitiveType.fromApiTypeName(value);
+                    } catch (IllegalArgumentException e) {
+                        throwException(tr("Illegal value for attribute ''type'' on member {0} in relation {1}. Got {2}.",
+                                Long.toString(id), Long.toString(currentPrimitive.getUniqueId()), value));
+                    }
+
+                    String role = atts.getValue("role");
+
+                    OsmPrimitive member = externalIdMap.get(new SimplePrimitiveId(id, type));
+                    if (member == null) {
+                        member = targetDataSet.getPrimitiveById(new SimplePrimitiveId(id, type));
+                        if (member == null)
+                            throwException(tr("Missing definition of new object with id {0}.", id));
+                    }
+                    RelationMember relationMember = new RelationMember(role, member);
+                    currentRelationMembers.add(relationMember);
+                } else if (qName.equals("tag")) { // ---- PARSING TAGS (applicable to all objects) ----
+                    String key = atts.getValue("k");
+                    String value = atts.getValue("v");
+                    if (key == null || value == null) {
+                        throwException(tr("Missing key or value attribute in tag."));
+                        return;
+                    }
+                    currentPrimitive.put(key.intern(), value.intern());
+                } else {
+                    Logging.warn(tr("Undefined element ''{0}'' found in input stream. Skipping.", qName));
+                }
+            } catch (Exception e) {
+                throw new SAXParseException(e.getMessage(), locator, e);
+            }
+        }
+
+        @Override
+        public void endElement(String namespaceURI, String localName, String qName) {
+            DataSet ds = MainApplication.getLayerManager().getEditDataSet();
+            if (qName.equals("node")) {
+                if (currentPrimitive.isDeleted()) {
+                    cmds.add(new DeleteCommand(targetDataSet.getPrimitiveById(currentPrimitive.getPrimitiveId())));
+                } else if (currentPrimitive.isModified()) {
+                    cmds.add(new ChangeCommand(ds,
+                            targetDataSet.getPrimitiveById(currentPrimitive.getPrimitiveId()), currentPrimitive));
+                } else if (currentPrimitive.isNew()) {
+                    cmds.add(new AddCommand(ds, currentPrimitive));
+                }
+            } else if (qName.equals("way")) {
+                ((Way) currentPrimitive).setNodes(currentWayNodes);
+                if (currentPrimitive.isDeleted()) {
+                    cmds.add(new DeleteCommand(targetDataSet.getPrimitiveById(currentPrimitive.getPrimitiveId())));
+                } else if (currentPrimitive.isModified()) {
+                    cmds.add(new ChangeCommand(ds,
+                            targetDataSet.getPrimitiveById(currentPrimitive.getPrimitiveId()), currentPrimitive));
+                } else if (currentPrimitive.isNew()) {
+                    cmds.add(new AddCommand(ds, currentPrimitive));
+                }
+            } else if (qName.equals("relation")) {
+                ((Relation) currentPrimitive).setMembers(currentRelationMembers);
+                if (currentPrimitive.isDeleted()) {
+                    cmds.add(new DeleteCommand(targetDataSet.getPrimitiveById(currentPrimitive.getPrimitiveId())));
+                } else if (currentPrimitive.isModified()) {
+                    cmds.add(new ChangeCommand(ds,
+                            targetDataSet.getPrimitiveById(currentPrimitive.getPrimitiveId()), currentPrimitive));
+                } else if (currentPrimitive.isNew()) {
+                    cmds.add(new AddCommand(ds, currentPrimitive));
+                }
+            }
+        }
+
+        @Override
+        public void comment(char[] ch, int start, int length) {
+            parentPlugin.printHistory(String.valueOf(ch));
+        }
+
+        @Override
+        public void startCDATA() {
+        }
+
+        @Override
+        public void endCDATA() {
+        }
+
+        @Override
+        public void startEntity(String name) {
+        }
+
+        @Override
+        public void endEntity(String name) {
+        }
+
+        @Override
+        public void startDTD(String name, String publicId, String systemId) {
+        }
+
+        @Override
+        public void endDTD() {
+        }
+
+        private double getDouble(Attributes atts, String value) {
+            return Double.parseDouble(atts.getValue(value));
+        }
+
+        private long getLong(Attributes atts, String name) throws SAXException {
+            String value = atts.getValue(name);
+            if (value == null) {
+                throwException(tr("Missing required attribute ''{0}''.", name));
+            }
+            try {
+                return Long.parseLong(value);
+            } catch (NumberFormatException e) {
+                throwException(tr("Illegal long value for attribute ''{0}''. Got ''{1}''.", name, value));
+            }
+            return 0; // should not happen
+        }
+
+        private User createUser(String uid, String name) throws SAXException {
+            if (uid == null) {
+                if (name == null)
+                    return null;
+                return User.createLocalUser(name);
+            }
+            try {
+                long id = Long.parseLong(uid);
+                return User.createOsmUser(id, name);
+            } catch (NumberFormatException e) {
+                throwException(tr("Illegal value for attribute ''uid''. Got ''{0}''.", uid));
+            }
+            return null;
+        }
+
+        void readCommon(Attributes atts, PrimitiveData current) throws SAXException {
+            current.setId(getLong(atts, "id"));
+            if (current.getUniqueId() == 0) {
+                throwException(tr("Illegal object with ID=0."));
+            }
+
+            String time = atts.getValue("timestamp");
+            if (time != null && time.length() != 0) {
+                current.setTimestamp(DateUtils.fromString(time));
+            }
+
+            String user = atts.getValue("user");
+            String uid = atts.getValue("uid");
+            current.setUser(createUser(uid, user));
+
+            String visible = atts.getValue("visible");
+            if (visible != null) {
+                current.setVisible(Boolean.parseBoolean(visible));
+            }
+
+            String versionString = atts.getValue("version");
+            int version = 0;
+            if (versionString != null) {
+                try {
+                    version = Integer.parseInt(versionString);
+                } catch (NumberFormatException e) {
+                    throwException(tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.",
+                            Long.toString(current.getUniqueId()), versionString));
+                }
+            }
+            current.setVersion(version);
+
+            String action = atts.getValue("action");
+            if (action == null) {
+                // do nothing
+            } else if (action.equals("delete")) {
+                current.setDeleted(true);
+                current.setModified(current.isVisible());
+            } else if (action.equals("modify")) {
+                current.setModified(true);
+            }
+
+            String v = atts.getValue("changeset");
+            if (v == null) {
+                current.setChangesetId(0);
+            } else {
+                try {
+                    current.setChangesetId(Integer.parseInt(v));
+                } catch (NumberFormatException e) {
+                    if (current.getUniqueId() <= 0) {
+                        Logging.warn(tr("Illegal value for attribute ''changeset'' on new object {1}. Got {0}. Resetting to 0.",
+                                v, current.getUniqueId()));
+                        current.setChangesetId(0);
+                    } else {
+                        throwException(tr("Illegal value for attribute ''changeset''. Got {0}.", v));
+                    }
+                }
+                if (current.getChangesetId() <= 0) {
+                    if (current.getUniqueId() <= 0) {
+                        Logging.warn(tr("Illegal value for attribute ''changeset'' on new object {1}. Got {0}. Resetting to 0.",
+                                v, current.getUniqueId()));
+                        current.setChangesetId(0);
+                    } else {
+                        throwException(tr("Illegal value for attribute ''changeset''. Got {0}.", v));
+                    }
+                }
+            }
+        }
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Parameter.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Parameter.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Parameter.java	(revision 34574)
@@ -0,0 +1,106 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+
+public class Parameter {
+    public boolean required;
+    public Type type;
+    public String name;
+    public String description;
+    private Object value;
+    private final ArrayList<OsmPrimitive> valueList;
+    protected float maxVal;
+    protected float minVal;
+    protected int maxInstances;
+
+    public Parameter() {
+        required = false;
+        maxInstances = 1;
+        maxVal = 0;
+        minVal = 0;
+        value = "";
+        valueList = new ArrayList<>();
+    }
+
+    public String getValue() {
+        String out = "";
+        switch (type) {
+        case POINT:
+            out = (String) value;
+            break;
+        case LENGTH:
+            out = String.valueOf(value);
+            break;
+        case NATURAL:
+            out = String.valueOf(value);
+            break;
+        case STRING:
+            out = String.valueOf(value);
+            break;
+        case RELAY:
+            out = String.valueOf(((Relay) value).getValue());
+            break;
+        case NODE:
+            out = String.valueOf(valueList.size()) + " " + tr("nodes");
+            break;
+        case WAY:
+            out = String.valueOf(valueList.size()) + " " + tr("ways");
+            break;
+        case RELATION:
+            out = String.valueOf(valueList.size()) + " " + tr("relations");
+            break;
+        case ANY:
+            out = String.valueOf(valueList.size()) + " " + tr("OSM objects");
+            break;
+        case USERNAME:
+            out = String.valueOf(value);
+            break;
+        case IMAGERYURL:
+            out = String.valueOf(value);
+            break;
+        case IMAGERYOFFSET:
+            out = String.valueOf(value);
+            break;
+        default:
+            break;
+        }
+        return out;
+    }
+
+    public Object getRawValue() {
+        return value;
+    }
+
+    public ArrayList<OsmPrimitive> getValueList() {
+        return valueList;
+    }
+
+    public void setValue(Object obj) {
+        if (type == Type.RELAY && obj instanceof String && value instanceof Relay) {
+            ((Relay) value).setValue((String) obj);
+        } else
+            value = obj;
+    }
+
+    public Collection<OsmPrimitive> getParameterObjects() {
+        ArrayList<OsmPrimitive> pObjects = new ArrayList<>();
+        if (isOsm()) {
+            if (maxInstances == 1) {
+                pObjects.add((OsmPrimitive) value);
+            } else {
+                return valueList;
+            }
+        }
+        return pObjects;
+    }
+
+    public boolean isOsm() {
+        return type == Type.NODE || type == Type.WAY || type == Type.RELATION || type == Type.ANY;
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/PointAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/PointAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/PointAction.java	(revision 34574)
@@ -0,0 +1,193 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.AWTEvent;
+import java.awt.Cursor;
+import java.awt.EventQueue;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+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.gui.MainApplication;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Logging;
+
+public class PointAction extends MapMode implements AWTEventListener {
+    private final CommandLine parentPlugin;
+    private final Cursor cursorCrosshair;
+    private final Cursor cursorJoinNode;
+    private Cursor currentCursor;
+    private Point mousePos;
+    private Node nearestNode;
+    private final ArrayList<String> pointList;
+    private boolean isCtrlDown;
+
+    public PointAction(CommandLine parentPlugin) {
+        super(null, "addsegment.png", null, ImageProvider.getCursor("crosshair", null));
+        this.parentPlugin = parentPlugin;
+        cursorCrosshair = ImageProvider.getCursor("crosshair", null);
+        cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
+        currentCursor = cursorCrosshair;
+        nearestNode = null;
+        pointList = new ArrayList<>();
+    }
+
+    @Override public void enterMode() {
+        super.enterMode();
+        if (getLayerManager().getEditDataSet() == null) {
+            MainApplication.getMap().selectSelectTool(false);
+            return;
+        }
+        currentCursor = cursorCrosshair;
+        MainApplication.getMap().mapView.addMouseListener(this);
+        MainApplication.getMap().mapView.addMouseMotionListener(this);
+        try {
+            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
+        } catch (SecurityException ex) {
+            Logging.warn(ex);
+        }
+    }
+
+    @Override public void exitMode() {
+        super.exitMode();
+        MainApplication.getMap().mapView.removeMouseListener(this);
+        MainApplication.getMap().mapView.removeMouseMotionListener(this);
+        try {
+            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
+        } catch (SecurityException ex) {
+            Logging.warn(ex);
+        }
+    }
+
+    @Override public void mousePressed(MouseEvent e) {
+        if (e.getButton() == MouseEvent.BUTTON1) {
+            if (isCtrlDown) {
+                if (pointList.size() > 0) {
+                    pointList.remove(pointList.size() - 1);
+                    updateTextEdit();
+                }
+            } else {
+                LatLon coor;
+                if (nearestNode == null)
+                    coor = MainApplication.getMap().mapView.getLatLon(e.getX(), e.getY());
+                else
+                    coor = nearestNode.getCoor();
+                if (coor.isOutSideWorld()) {
+                    JOptionPane.showMessageDialog(MainApplication.getMainFrame(), tr("Can not draw outside of the world."));
+                    return;
+                }
+                String point = String.valueOf(coor.getX()) + "," + String.valueOf(coor.getY());
+                int maxInstances = parentPlugin.currentCommand.parameters.get(parentPlugin.currentCommand.currentParameterNum).maxInstances;
+                if (maxInstances == 1) {
+                    parentPlugin.loadParameter(point, true);
+                } else {
+                    if (pointList.size() < maxInstances || maxInstances == 0) {
+                        pointList.add(point);
+                        updateTextEdit();
+                    } else
+                        Logging.info("Maximum instances!");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+        if (!MainApplication.getMap().mapView.isActiveLayerDrawable())
+            return;
+        processMouseEvent(e);
+        updCursor();
+        MainApplication.getMap().mapView.repaint();
+    }
+
+    @Override
+    public void eventDispatched(AWTEvent arg0) {
+        if (!(arg0 instanceof KeyEvent))
+            return;
+        KeyEvent ev = (KeyEvent) arg0;
+        isCtrlDown = (ev.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0;
+        if (ev.getKeyCode() == KeyEvent.VK_ESCAPE && ev.getID() == KeyEvent.KEY_PRESSED) {
+            ev.consume();
+            cancelDrawing();
+        }
+    }
+
+    private void updCursor() {
+        if (mousePos != null) {
+            if (!MainApplication.isDisplayingMapView())
+                return;
+            nearestNode = MainApplication.getMap().mapView.getNearestNode(mousePos, OsmPrimitive::isUsable);
+            if (nearestNode != null) {
+                setCursor(cursorJoinNode);
+            } else {
+                setCursor(cursorCrosshair);
+            }
+        }
+    }
+
+    private void processMouseEvent(MouseEvent e) {
+        if (e != null) {
+            mousePos = e.getPoint();
+        }
+    }
+
+    private void setCursor(final Cursor c) {
+        if (currentCursor.equals(c))
+            return;
+        try {
+            // We invoke this to prevent strange things from happening
+            EventQueue.invokeLater(() -> {
+                // Don't change cursor when mode has changed already
+                if (!(MainApplication.getMap().mapMode instanceof PointAction))
+                    return;
+                MainApplication.getMap().mapView.setCursor(c);
+            });
+            currentCursor = c;
+        } catch (Exception e) {
+            Logging.warn(e);
+        }
+    }
+
+    public void cancelDrawing() {
+        if (!MainApplication.isDisplayingMapView())
+            return;
+        MapFrame map = MainApplication.getMap();
+        map.statusLine.setHeading(-1);
+        map.statusLine.setAngle(-1);
+        map.mapView.repaint();
+        updateStatusLine();
+        parentPlugin.abortInput();
+    }
+
+    public String currentValue() {
+        String out = "";
+        boolean first = true;
+        for (String point : pointList) {
+            if (!first)
+                out += ";";
+            out += point;
+            first = false;
+        }
+        return out;
+    }
+
+    private void updateTextEdit() {
+        Parameter currentParameter = parentPlugin.currentCommand.parameters.get(parentPlugin.currentCommand.currentParameterNum);
+        String prefix = tr(currentParameter.description);
+        prefix += parentPlugin.commandSymbol;
+        String value = currentValue();
+        parentPlugin.textField.setText(prefix + value);
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/RelationAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/RelationAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/RelationAction.java	(revision 34574)
@@ -0,0 +1,42 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.awt.AWTEvent;
+import java.awt.event.AWTEventListener;
+import java.awt.event.KeyEvent;
+
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public class RelationAction extends MapMode implements AWTEventListener {
+    private final CommandLine parentPlugin;
+
+    public RelationAction(CommandLine parentPlugin) {
+        super(null, "addsegment.png", null, ImageProvider.getCursor("normal", null));
+        this.parentPlugin = parentPlugin;
+    }
+
+        @Override
+    public void eventDispatched(AWTEvent arg0) {
+        if (!(arg0 instanceof KeyEvent))
+            return;
+        KeyEvent ev = (KeyEvent) arg0;
+        if (ev.getKeyCode() == KeyEvent.VK_ESCAPE && ev.getID() == KeyEvent.KEY_PRESSED) {
+            ev.consume();
+            cancelDrawing();
+        }
+    }
+
+    public void cancelDrawing() {
+        if (!MainApplication.isDisplayingMapView())
+            return;
+        MapFrame map = MainApplication.getMap();
+        map.statusLine.setHeading(-1);
+        map.statusLine.setAngle(-1);
+        map.mapView.repaint();
+        updateStatusLine();
+        parentPlugin.abortInput();
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Relay.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Relay.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Relay.java	(revision 34574)
@@ -0,0 +1,55 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.util.HashMap;
+
+public class Relay {
+    static String marker = "\u0332";
+    private String optionsString;
+    private final HashMap<String, String> options;
+    private String value;
+
+    public Relay() {
+        optionsString = "";
+        value = "";
+        options = new HashMap<>();
+    }
+
+    public void setValue(String value) {
+        if (options.containsKey(value))
+            this.value = options.get(value);
+        else if (options.containsValue(value))
+            this.value = value;
+    }
+
+    public void addValue(String value) {
+        String letter = null;
+        if (!(options.containsValue(value))) {
+            int i = 0;
+            for (; i < value.length(); i++) {
+                letter = value.substring(i, i + 1).toLowerCase();
+                if (!options.containsKey(letter))
+                    break;
+            }
+            if (i == value.length()) {
+                letter = String.valueOf(System.currentTimeMillis());
+                optionsString = optionsString + (optionsString.length() == 0 ? "" : ", ") + value;
+            } else
+                optionsString = optionsString + (optionsString.length() == 0 ? "" : ", ") + value.substring(0, i) + marker + value.substring(i);
+            options.put(letter, value);
+        }
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public boolean isCorrectValue(String value) {
+        return options.containsValue(value) || options.containsKey(value.toLowerCase());
+    }
+
+    public String getOptionsString() {
+        return optionsString;
+    }
+}
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Type.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Type.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/Type.java	(revision 34574)
@@ -0,0 +1,4 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+public enum Type { NODE, WAY, RELATION, ANY, POINT, LENGTH, NATURAL, STRING, TRIGGER, RELAY, USERNAME, IMAGERYURL, IMAGERYOFFSET }
Index: applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/WayAction.java
===================================================================
--- applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/WayAction.java	(revision 34574)
+++ applications/editors/josm/plugins/CommandLine/src/org/openstreetmap/josm/plugins/commandline/WayAction.java	(revision 34574)
@@ -0,0 +1,20 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.commandline;
+
+import java.awt.Point;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.MainApplication;
+
+public class WayAction extends AbstractOsmAction<Way> {
+
+    public WayAction(CommandLine parentPlugin) {
+        super(parentPlugin, "joinway");
+    }
+
+    @Override
+    protected Way getNearest(Point mousePos) {
+        return MainApplication.getMap().mapView.getNearestWay(mousePos, OsmPrimitive::isUsable);
+    }
+}
