JTSOpCmd.java
/*
* Copyright (c) 2019 Martin Davis.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jtstest.cmd;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import org.locationtech.jts.JTSVersion;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKTConstants;
import org.locationtech.jtstest.cmd.JTSOpRunner.OpParams;
import org.locationtech.jtstest.command.CommandLine;
import org.locationtech.jtstest.command.Option;
import org.locationtech.jtstest.command.OptionSpec;
import org.locationtech.jtstest.command.ParseException;
import org.locationtech.jtstest.function.DoubleKeyMap;
import org.locationtech.jtstest.geomfunction.BaseGeometryFunction;
import org.locationtech.jtstest.geomfunction.FilterGeometryFunction;
import org.locationtech.jtstest.geomfunction.GeometryFunction;
import org.locationtech.jtstest.geomfunction.GeometryFunctionRegistry;
import org.locationtech.jtstest.util.io.MultiFormatReader;
/**
* A CLI to run JTS TestBuilder operations.
* Allows easier execution of JTS functions on test data for debugging purposes.
* <p>
* Examples:
*
* <pre>
* --- Compute the area of a WKT geometry, output it
* jtsop -a some-file-with-geom.wkt area
*
* --- Validate geometries from a WKT file using limit and offset
* jtsop -a some-file-with-geom.wkt -limit 100 -offset 40 isValid
*
* --- Compute the unary union of a WKT geometry, output as WKB
* jtsop -a some-file-with-geom.wkt -f wkb Overlay.unaryUnion
*
* --- Compute the union of two geometries in WKT and WKB, output as WKT
* jtsop -a some-file-with-geom.wkt -b some-other-geom.wkb Overlay.Union
*
* --- Compute the buffer of distance 10 of a WKT geometry, output as GeoJSON
* jtsop -a some-file-with-geom.wkt -f geojson Buffer.buffer 10
*
* --- Compute the buffer of a literal geometry, output as WKT
* jtsop -a "POINT (10 10)" Buffer.buffer 10
*
* --- Compute buffers of multiple sizes
* jtsop -a "POINT (10 10)" Buffer.buffer 1,10,100
*
* --- Run op for each A
* jtsop -a "MULTIPOINT ((10 10), (20 20))" -eacha Buffer.buffer
*
* --- Output a literal geometry as GeoJSON
* jtsop -a "POINT (10 10)" -f geojson
*
* --- Run op but don't output result (quiet mode)
* jtsop -a "MULTIPOINT ((10 10), (20 20))" -q Buffer.buffer
* </pre>
*
* @author Martin Davis
*
*/
public class JTSOpCmd {
// TODO: add option -ab to read both geoms from a file
// TODO: allow -a stdin to indicate reading from stdin.
public static final String ERR_INVALID_ARG_PARAM = "Invalid argument parameter";
private static final String MACRO_VAL = "val";
public static void main(String[] args)
{
JTSOpCmd cmd = new JTSOpCmd();
int rc = 1;
try {
OpParams cmdArgs = cmd.parseArgs(args);
cmd.execute(cmdArgs);
rc = 0;
}
catch (CommandError e) {
// for command errors, just print the message
System.err.println(e.getMessage() );
}
catch (ParseException e) {
System.err.println(e.getMessage() );
}
catch (Exception e) {
// unexpected errors get a stack track to help debugging
e.printStackTrace();
}
System.exit(rc); // err code
}
private static CommandLine createCmdLine() {
CommandLine commandLine = new CommandLine('-');
commandLine.addOptionSpec(new OptionSpec(CommandOptions.GEOMFUNC, OptionSpec.NARGS_ONE_OR_MORE))
.addOptionSpec(new OptionSpec(CommandOptions.TIME, 0))
.addOptionSpec(new OptionSpec(CommandOptions.VERBOSE, 0))
.addOptionSpec(new OptionSpec(CommandOptions.V, 0))
.addOptionSpec(new OptionSpec(CommandOptions.HELP, 0))
.addOptionSpec(new OptionSpec(CommandOptions.OP, 0))
.addOptionSpec(new OptionSpec(CommandOptions.GEOMA, 1))
.addOptionSpec(new OptionSpec(CommandOptions.GEOMB, 1))
.addOptionSpec(new OptionSpec(CommandOptions.GEOMAB, 1))
.addOptionSpec(new OptionSpec(CommandOptions.COLLECT, 0))
//.addOptionSpec(new OptionSpec(CommandOptions.EACH, 1))
.addOptionSpec(new OptionSpec(CommandOptions.EACHA, 0))
.addOptionSpec(new OptionSpec(CommandOptions.EACHB, 0))
.addOptionSpec(new OptionSpec(CommandOptions.INDEX, 0))
.addOptionSpec(new OptionSpec(CommandOptions.EXPLODE, 0))
.addOptionSpec(new OptionSpec(CommandOptions.FORMAT, 1))
.addOptionSpec(new OptionSpec(CommandOptions.LIMIT, 1))
.addOptionSpec(new OptionSpec(CommandOptions.OFFSET, 1))
.addOptionSpec(new OptionSpec(CommandOptions.OUTPUT, 1))
.addOptionSpec(new OptionSpec(CommandOptions.REPEAT, 1))
.addOptionSpec(new OptionSpec(CommandOptions.QUIET, 0))
.addOptionSpec(new OptionSpec(CommandOptions.SRID, 1))
.addOptionSpec(new OptionSpec(CommandOptions.WHERE, 2))
.addOptionSpec(new OptionSpec(CommandOptions.VALIDATE, 0))
.addOptionSpec(new OptionSpec(OptionSpec.OPTION_FREE_ARGS, OptionSpec.NARGS_ONE_OR_MORE));
return commandLine;
}
static final String[] helpDoc = new String[] {
"",
"Usage: jtsop - CLI for JTS operations",
" [ -a <wkt> | <wkb> | stdin | <filename.ext> ]",
" [ -b <wkt> | <wkb> | stdin | <filename.ext> ]",
" [ -ab <wkt> | <wkb> | stdin | <filename.ext> ]",
" [ -limit N ]",
" [ -offset N ]",
" [ -collect ]",
" [ -eacha ]",
" [ -eachb ]",
" [ -index ]",
" [ -repeat N ]",
" [ -where (eq | ne | ge | gt | le | lt) N ]",
" [ -validate ]",
" [ -explode",
" [ -srid SRID ]",
" [ -f ( txt | wkt | wkb | geojson | gml | svg ) ]",
" [ -q",
" [ -time ]",
" [ -v, -verbose ]",
" [ -o filename ]",
" [ -help ]",
" [ -geomfunc classname ]",
" [ -op ]",
" [ op [ args... ]]",
" op name of the operation (in format Category.op)",
" args one or more scalar arguments to the operation",
" - To run over multiple arguments use v1,v2,v3 OR val(v1,v2,v3,..)",
"",
"===== Input options:",
" -a Geometry A: literal, stdin (WKT or WKB), or filename (extension: WKT, WKB, GeoJSON, GML, SHP)",
" -b Geometry A: literal, stdin (WKT or WKB), or filename (extension: WKT, WKB, GeoJSON, GML, SHP)",
" -limit Limits the number of geometries read from A, or B if specified",
" -offset Uses an offset to read geometries from A, or B if specified",
"===== Operation options:",
" -collect execute op on collection of A geometries",
" -eacha execute op on each element of A",
" -eachb execute op on each element of B",
" -index index the B geometries",
" -repeat repeat the operation N times",
" -where cond v output geometry where operation result matches condition and value.",
" Conditions are: eq, ne, ge, gt, le, lt",
" -validate validate the result of each operation",
" -geomfunc specifies class providing geometry operations",
" -op separator to delineate operation arguments",
"===== Output options:",
" -srid sets the SRID on output geometries",
" -explode output atomic geometries",
" -f output format to use. Default is txt/wkt",
" -q quiet mode - result is not output",
" -o filename write result output to filename",
"===== Logging options:",
" -time display execution time",
" -v, -verbose display information about execution",
" -help print a list of available operations"
};
private void printHelp(boolean showFunctions) {
System.out.println("JTSOp -- Version " + JTSVersion.CURRENT_VERSION);
for (String s : helpDoc) {
System.out.println(s);
}
if (showFunctions) {
System.out.println();
System.out.println("Operations:");
printFunctions();
}
}
private void printFunctions() {
//TODO: include any loaded functions
DoubleKeyMap funcMap = funcRegistry.getCategorizedGeometryFunctions();
@SuppressWarnings("unchecked")
Collection<String> categories = funcMap.keySet();
for (String category : categories) {
@SuppressWarnings("unchecked")
Collection<GeometryFunction> funcs = funcMap.values(category);
for (GeometryFunction fun : funcs) {
System.out.println(category + "." + functionDesc(fun));
}
}
}
private static String functionDesc(GeometryFunction fun) {
// TODO: display this in a command arg style
BaseGeometryFunction geomFun = (BaseGeometryFunction) fun;
return geomFun.getSignature();
//return geomFun.getName();
}
private GeometryFunctionRegistry funcRegistry = GeometryFunctionRegistry.createTestBuilderRegistry();
private CommandLine commandLine = createCmdLine();
private boolean isHelp = false;
private boolean isHelpWithFunctions = false;
private JTSOpRunner opRunner;
public JTSOpCmd() {
opRunner = new JTSOpRunner();
}
public void captureOutput() {
opRunner.captureOutput();
}
public void captureResult() {
opRunner.captureResult();
}
public List<Geometry> getResultGeometry() {
return opRunner.getResultGeometry();
}
public void replaceStdIn(InputStream inStream) {
opRunner.replaceStdIn(inStream);
}
public String getOutput() {
return opRunner.getOutput();
}
public String[] getOutputLines() {
String outAll = opRunner.getOutput();
return outAll.split("\n");
}
public static boolean isFilename(String arg) {
if (arg == null) return false;
if (MultiFormatReader.isWKB(arg)) return false;
if (isWKT(arg)) return false;
/*
if (arg.indexOf("/") > 0
|| arg.indexOf("\\") > 0
|| arg.indexOf(":") > 0)
return true;
*/
return true;
}
private static boolean isWKT(String arg) {
// TODO: make this smarter?
boolean hasParen = (arg.indexOf("(") > 0) && arg.indexOf(")") > 0;
if (hasParen) return true;
if (arg.toUpperCase().endsWith(" " + WKTConstants.EMPTY)) return true;
return false;
}
void execute(JTSOpRunner.OpParams cmdArgs) {
if (isHelp || isHelpWithFunctions) {
printHelp(isHelpWithFunctions);
return;
}
opRunner.setRegistry(funcRegistry);
opRunner.execute(cmdArgs);
}
JTSOpRunner.OpParams parseArgs(String[] args) throws ParseException, ClassNotFoundException {
if (args.length == 0) {
isHelp = true;
return null;
}
commandLine.parse(args);
OpParams cmdArgs = new JTSOpRunner.OpParams();
String argA = commandLine.getOptionArg(CommandOptions.GEOMA, 0);
if (argA != null) {
if (isFilename(argA)) {
cmdArgs.fileA = argA;
}
else {
cmdArgs.geomA = argA;
}
}
String argB = commandLine.getOptionArg(CommandOptions.GEOMB, 0);
if (argB != null) {
if (isFilename(argB)) {
cmdArgs.fileB = argB;
}
else {
cmdArgs.geomB = argB;
}
}
String argAB = commandLine.getOptionArg(CommandOptions.GEOMAB, 0);
if (argAB != null) {
cmdArgs.isGeomAB = true;
if (isFilename(argAB)) {
cmdArgs.fileA = argAB;
}
else {
cmdArgs.geomA = argAB;
}
}
cmdArgs.isCollect = commandLine.hasOption(CommandOptions.COLLECT);
cmdArgs.isExplode = commandLine.hasOption(CommandOptions.EXPLODE);
int paramLimit = commandLine.hasOption(CommandOptions.LIMIT)
? commandLine.getOptionArgAsInt(CommandOptions.LIMIT, 0)
: -1;
int paramOffset = commandLine.hasOption(CommandOptions.OFFSET)
? commandLine.getOptionArgAsInt(CommandOptions.OFFSET, 0)
: 0;
cmdArgs.format = commandLine.hasOption(CommandOptions.FORMAT)
? commandLine.getOptionArg(CommandOptions.FORMAT, 0)
: CommandOptions.FORMAT_TXT;
cmdArgs.srid = commandLine.hasOption(CommandOptions.SRID)
? commandLine.getOptionArgAsInt(CommandOptions.SRID, 0)
: -1;
cmdArgs.isIndexed = commandLine.hasOption(CommandOptions.INDEX);
cmdArgs.isQuiet = commandLine.hasOption(CommandOptions.QUIET);
cmdArgs.outputFile = commandLine.hasOption(CommandOptions.OUTPUT)
? commandLine.getOptionArg(CommandOptions.OUTPUT, 1)
: null;
cmdArgs.repeat = commandLine.hasOption(CommandOptions.REPEAT)
? commandLine.getOptionArgAsInt(CommandOptions.REPEAT, 0)
: 1;
cmdArgs.validate = commandLine.hasOption(CommandOptions.VALIDATE);
cmdArgs.isFilter = commandLine.hasOption(CommandOptions.WHERE);
cmdArgs.filterOp = cmdArgs.isFilter ?
parseFilterOp(commandLine.getOptionArg(CommandOptions.WHERE, 0))
: 0;
cmdArgs.filterVal = cmdArgs.isFilter ?
commandLine.getOptionArgAsNum(CommandOptions.WHERE, 1)
: 0;
cmdArgs.eachA = commandLine.hasOption(CommandOptions.EACHA);
cmdArgs.eachB = commandLine.hasOption(CommandOptions.EACHB);
boolean isVerbose = commandLine.hasOption(CommandOptions.VERBOSE)
|| commandLine.hasOption(CommandOptions.V);
opRunner.setVerbose(isVerbose);
opRunner.setTime(commandLine.hasOption(CommandOptions.TIME));
isHelpWithFunctions = commandLine.hasOption(CommandOptions.HELP);
if (commandLine.hasOption(CommandOptions.GEOMFUNC)) {
Option opt = commandLine.getOption(CommandOptions.GEOMFUNC);
for (int i = 0; i < opt.getNumArgs(); i++) {
String geomFuncClassname = opt.getArg(i);
try {
funcRegistry.add(geomFuncClassname);
System.out.println("Added Geometry Functions from: " + geomFuncClassname);
} catch (ClassNotFoundException ex) {
System.out.println("Unable to load function class: " + geomFuncClassname);
}
}
}
String[] freeArgs = commandLine.getOptionArgs(OptionSpec.OPTION_FREE_ARGS);
if (freeArgs != null) {
if (freeArgs.length >= 1) {
cmdArgs.operation = freeArgs[0];
}
if (freeArgs.length >= 2) {
cmdArgs.argList = parseOpArg(freeArgs[1]);
}
}
/**
* ====== Apply extra parameter logic
*/
//--- apply limit to A if no B, or else to B
// This allows applying a binary op with a fixed LHS to a limited set of RHS geoms
if (OpParams.isGeometryInput(cmdArgs.fileB, cmdArgs.geomB)) {
cmdArgs.limitB = paramLimit;
cmdArgs.offsetB = paramOffset;
}
else {
cmdArgs.limitA = paramLimit;
cmdArgs.offsetA = paramOffset;
}
return cmdArgs;
}
private String[] parseOpArg(String arg) {
if (isArgMultiValues(arg)) {
return parseValues(arg);
}
// no other macros, for now
if (arg.contains("("))
throw new CommandError(ERR_INVALID_ARG_PARAM, arg);
// default is a single arg value
return new String[] { arg };
}
/**
* Detects various syntaxes for values:
* - (1,2,3)
* - val(1,2,3)
* - 1,2,3
*
* @param arg
* @return
*/
private boolean isArgMultiValues(String arg) {
if (arg.startsWith("(")) return true;
if (arg.startsWith(MACRO_VAL + "(")) return true;
boolean hasParen = arg.indexOf('(') >= 0;
boolean hasComma = arg.indexOf(',') >= 0;
if (hasComma && ! hasParen) return true;
return false;
}
private String[] parseValues(String valuesExpr) {
boolean hasParenL = valuesExpr.indexOf('(') >= 0;
boolean hasParenR = valuesExpr.indexOf(')') >= 0;
boolean hasParen = hasParenL || hasParenR;
if (hasParen) {
return parseMacroArgs(valuesExpr);
}
// assume expr is an arg list
return valuesExpr.split(",");
}
private String[] parseMacroArgs(String macroTerm) {
int indexLeft = macroTerm.indexOf('(');
int indexRight = macroTerm.indexOf(')');
// check for missing L or R parent
if (indexLeft < 0 || indexRight <= 0)
throw new CommandError(ERR_INVALID_ARG_PARAM, macroTerm);
// TODO: error if no R paren
String args = macroTerm.substring(indexLeft + 1, indexRight);
return args.split(",");
}
private int parseFilterOp(String opStr) {
if ("eq".equalsIgnoreCase(opStr)) return FilterGeometryFunction.OP_EQ;
if ("ne".equalsIgnoreCase(opStr)) return FilterGeometryFunction.OP_NE;
if ("ge".equalsIgnoreCase(opStr)) return FilterGeometryFunction.OP_GE;
if ("gt".equalsIgnoreCase(opStr)) return FilterGeometryFunction.OP_GT;
if ("le".equalsIgnoreCase(opStr)) return FilterGeometryFunction.OP_LE;
if ("lt".equalsIgnoreCase(opStr)) return FilterGeometryFunction.OP_LT;
throw new CommandError(ERR_INVALID_ARG_PARAM, opStr);
}
}