// License: GPL. For details, see LICENSE file.
package 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.Main;
import org.openstreetmap.josm.actions.mapmode.MapMode;
import org.openstreetmap.josm.command.SequenceCommand;
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.MainMenu;
import org.openstreetmap.josm.gui.MapFrame;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
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.tools.HttpClient;
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 = Main.pref.getPluginsDirectory().getAbsolutePath() + "/CommandLine/";
@SuppressWarnings("serial")
public CommandLine(PluginInformation info) {
super(info);
commandSymbol = ": ";
history = new History(100);
historyField = new DisableShortcutsOnFocusGainedTextField();
textField = new CommandTextField();
if (Main.main.menu != null) {
commandMenu = Main.main.menu.addMenu("Commands", tr("Commands"), KeyEvent.VK_O,
Main.main.menu.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) {
if (Main.map == null)
return;
DataSet ds = Main.getLayerManager().getEditDataSet();
if (ds == null)
return;
currentCommand = command;
currentCommand.resetLoading();
parseSelection(ds.getSelected());
if (!(Main.map.mapMode instanceof AnyAction
|| Main.map.mapMode instanceof DummyAction
|| Main.map.mapMode instanceof LengthAction
|| Main.map.mapMode instanceof NodeAction
|| Main.map.mapMode instanceof PointAction
|| Main.map.mapMode instanceof RelationAction
|| Main.map.mapMode instanceof WayAction)) {
previousMode = Main.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(new Runnable() {
@Override
public void run() {
historyField.setText(text);
}
});
}
private void loadCommands() {
commands = (new Loader(getPluginDir())).load();
if (commands.isEmpty()) {
if (!GraphicsEnvironment.isHeadless() && JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Main.parent,
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(getPluginDir())).load();
JOptionPane.showMessageDialog(Main.parent, tr("Default commands have been successfully installed"),
tr("Success"), JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
Main.warn(e);
JOptionPane.showMessageDialog(Main.parent,
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 = Main.pref.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 = new File(getPluginDir());
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);
Main.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 = Main.getLayerManager().getEditDataSet();
if (currentDataSet != null) {
currentDataSet.clearSelection();
Main.map.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(Main.pref.get("osm-server.username", null), true);
action = new DummyAction(this);
break;
case IMAGERYURL:
Layer layer = Main.getLayerManager().getActiveLayer();
if (layer != null) {
if (!(layer instanceof ImageryLayer)) {
List<ImageryLayer> imageryLayers = Main.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 = Main.getLayerManager().getActiveLayer();
if (olayer != null) {
if (!(olayer instanceof ImageryLayer)) {
List<ImageryLayer> imageryLayers = Main.getLayerManager().getLayersOfType(ImageryLayer.class);
if (imageryLayers.size() == 1) {
olayer = imageryLayers.get(0);
} else {
endInput();
return;
}
}
}
loadParameter((String.valueOf(((ImageryLayer) olayer).getDx()) + "," + String.valueOf(((ImageryLayer) olayer).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);
Main.map.mapView.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
}
public void activate() {
textField.requestFocus();
textField.setCaretPosition(textField.getText().length());
}
public void deactivate() {
Main.map.mapView.requestFocus();
}
public void abortInput() {
printHistory(tr("Aborted") + ".");
endInput();
}
public void endInput() {
setMode(Mode.IDLE);
Main.map.selectMapMode(previousMode);
Main.map.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 {
Main.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 = Main.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(new File(getPluginDir()));
final StringBuilder debugstr = new StringBuilder();
// debug: print resulting cmdline
for (String s : builder.command()) {
debugstr.append(s + " ");
}
debugstr.append("\n");
Main.info(debugstr.toString());
final ToolProcess tp = new ToolProcess();
try {
tp.process = builder.start();
} catch (final IOException e) {
synchronized (debugstr) {
Main.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) {
Main.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) {
Main.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 = Main.getLayerManager().getLayersOfType(GpxLayer.class);
for (GpxLayer gpxLayer : gpxLayers) {
gpxFilter.addGpxData(gpxLayer.data);
}
gpxWriter.write(gpxFilter.getGpxData());
} catch (IOException e2) {
Main.warn(e2);
}
}
Utils.close(osmWriter);
synchronized (syncObj) {
if (currentCommand.asynchronous) {
tp.running = false;
syncObj.notifyAll();
}
}
});
// Read stdout stream
final DataSet currentDataSet = Main.getLayerManager().getEditDataSet();
final CommandLine that = this;
Thread osmParseThread = new Thread(new Runnable() {
@Override
public void run() {
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(new Runnable() {
@Override
public void run() {
Main.main.undoRedo.add(cmd);
}
});
}
} catch (Exception e) {
Main.warn(e);
} finally {
synchronized (syncObj) {
tp.running = false;
syncObj.notifyAll();
}
}
}
});
osmParseThread.start();
osmWriteThread.start();
synchronized (syncObj) {
try {
syncObj.wait(Main.pref.getInteger("commandline.timeout", 20000));
} catch (InterruptedException e) {
Main.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) {
Main.warn(e);
}
}
@Override
protected void cancel() {
synchronized (syncObj) {
tp.running = false;
tp.process.destroy();
syncObj.notifyAll();
endInput();
}
}
@Override
protected void finish() {
}
}).start();
}
endInput();
}
}