/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.referencing; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParsePosition; import java.text.ParseException; import java.util.Arrays; import java.util.Locale; import java.util.StringTokenizer; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.CoordinateOperationFactory; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.operation.TransformException; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.MismatchedDimensionException; import org.geotools.geometry.GeneralDirectPosition; import org.geotools.io.TableWriter; import org.geotools.measure.Measure; import org.geotools.referencing.crs.AbstractCRS; import org.geotools.referencing.wkt.AbstractConsole; import org.geotools.referencing.wkt.Parser; import org.geotools.referencing.wkt.Preprocessor; import org.geotools.resources.Arguments; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; /** * A console for executing CRS operations from the command line. * Instructions are read from the {@linkplain System#in standard input stream} * and results are sent to the {@linkplain System#out standard output stream}. * Instructions include: * * <table> * <tr><td nowrap valign="top">{@code SET} <var>name</var> {@code =} <var>wkt</var></td><td> * Set the specified <var>name</var> as a shortcut for the specified Well Know * Text (<var>wkt</var>). This WKT can contains other shortcuts defined previously.</td></tr> * * <tr><td nowrap valign="top">{@code transform = } <var>wkt</var></td><td> * Set explicitly a {@linkplain MathTransform math transform} to use for * coordinate transformations. This instruction is a more direct alternative to the usage of * {@code source crs} and {@code target crs} instruction.</td></tr> * * <tr><td nowrap valign="top">{@code source crs = } <var>wkt</var></td><td> * Set the source {@linkplain CoordinateReferenceSystem coordinate reference * system} to the specified object. This object can be specified as a Well Know Text * (<var>wkt</var>) or as a shortcut previously set.</td></tr> * * <tr><td nowrap valign="top">{@code target crs = } <var>wkt</var></td><td> * Set the target {@linkplain CoordinateReferenceSystem coordinate reference * system} to the specified object. This object can be specified as a Well Know Text * (<var>wkt</var>) or as a shortcut previously set. Once both source and target * CRS are specified a {@linkplain MathTransform math transform} from source to * target CRS is automatically infered.</td></tr> * * <tr><td nowrap valign="top">{@code source pt = } <var>coord</var></td><td> * Transforms the specified coordinates from source CRS to target CRS * and prints the result.</td></tr> * * <tr><td nowrap valign="top">{@code target pt = } <var>coord</var></td><td> * Inverse transforms the specified coordinates from target CRS to source CRS * and prints the result.</td></tr> * * <tr><td nowrap valign="top">{@code test tolerance = } <var>vector</var></td><td> * Set the maximum difference between the transformed source point and the * target point. Once this value is set, every occurence of the {@code target pt} instruction * will trig this comparaison. If a greater difference is found, an exception is thrown or a * message is printed to the error stream.</td></tr> * * <tr><td nowrap valign="top">{@code print set}</td><td> * Prints the set of shortcuts defined in previous calls to {@code SET} instruction.</td></tr> * * <tr><td nowrap valign="top">{@code print crs}</td><td> * Prints the source and target {@linkplain CoordinateReferenceSystem coordinate reference system} * {@linkplain MathTransform math transform} and its inverse as Well Know Text (wkt).</td></tr> * * <tr><td nowrap valign="top">{@code print pts}</td><td> * Prints the source and target points, their transformed points, and the distance between * them.</td></tr> * * <tr><td nowrap valign="top">{@code exit}</td><td> * Quit the console.</td></tr> * </table> * * @since 2.1 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class Console extends AbstractConsole { /** * The locale for number parser. */ private final Locale locale = Locale.US; /** * The number format to use for reading coordinate points. */ private final NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); /** * The number separator in vectors. Usually {@code ,}, but could * also be {@code ;} if the coma is already used as the decimal * separator. */ private final String numberSeparator; /** * The coordinate operation factory to use. */ private final CoordinateOperationFactory factory = ReferencingFactoryFinder.getCoordinateOperationFactory(null); /** * The source and target CRS, or {@code null} if not yet determined. */ private CoordinateReferenceSystem sourceCRS, targetCRS; /** * Source and target coordinate points, or {@code null} if not yet determined. */ private DirectPosition sourcePosition, targetPosition; /** * The math transform, or {@code null} if not yet determined. */ private MathTransform transform; /** * The tolerance value. If non-null, the difference between the computed and the specified * target point will be compared against this tolerance threshold. If it is greater, a message * will be printed. */ private double[] tolerance; /** * The last error thats occured while processing an instruction. * Used in order to print the stack trace on request. */ private transient Exception lastError; /** * Creates a new console instance using {@linkplain System#in standard input stream}, * {@linkplain System#out standard output stream}, {@linkplain System#err error output stream} * and the system default line separator. */ public Console() { super(new Preprocessor(new Parser())); numberSeparator = getNumberSeparator(numberFormat); } /** * Creates a new console instance using the specified input stream. * * @param in The input stream. */ public Console(final LineNumberReader in) { super(new Preprocessor(new Parser()), in); numberSeparator = getNumberSeparator(numberFormat); } /** * Returns the character to use as a number separator. * As a side effect, this method also adjust the minimum and maximum digits. */ private static String getNumberSeparator(final NumberFormat numberFormat) { numberFormat.setGroupingUsed(false); numberFormat.setMinimumFractionDigits(6); numberFormat.setMaximumFractionDigits(6); if (numberFormat instanceof DecimalFormat) { final char decimalSeparator = ((DecimalFormat) numberFormat) .getDecimalFormatSymbols().getDecimalSeparator(); if (decimalSeparator == ',') { return ";"; } } return ","; } /** * Run the console from the command line. Before to process all instructions * from the {@linkplain System#in standard input stream}, this method first * process the following optional command-line arguments: * <P> * <TABLE CELLPADDING='0' CELLSPACING='0'> * <TR><TD NOWRAP><CODE>-load</CODE> <VAR><filename></VAR></TD> * <TD> Load a definition file before to run instructions from * the standard input stream.</TD></TR> * <TR><TD NOWRAP><CODE>-encoding</CODE> <VAR><code></VAR></TD> * <TD> Set the character encoding.</TD></TR> * <TR><TD NOWRAP><CODE>-locale</CODE> <VAR><language></VAR></TD> * <TD> Set the language for the output (e.g. "fr" for French).</TD></TR> * </TABLE> * * @param args the command line arguments */ public static void main(String[] args) { final Arguments arguments = new Arguments(args); final String load = arguments.getOptionalString("-load" ); final String file = arguments.getOptionalString("-file" ); args = arguments.getRemainingArguments(0); Locale.setDefault(arguments.locale); final LineNumberReader input; final Console console; /* * The usual way to execute instructions from a file is to redirect the standard input * stream using the standard DOS/Unix syntax (e.g. "< thefile.txt"). However, we also * accept a "-file" argument for the same purpose. It is easier to debug. On DOS system, * it also use the system default encoding instead of the command-line one. */ if (file == null) { input = null; console = new Console(); } else try { input = new LineNumberReader(new FileReader(file)); console = new Console(input); console.setPrompt(null); } catch (IOException exception) { System.err.println(exception.getLocalizedMessage()); return; } /* * Load predefined shorcuts. The file must be in the form "name = WKT". An example * of such file is the property file used by the property-based authority factory. */ if (load != null) try { final LineNumberReader in = new LineNumberReader(new FileReader(load)); try { console.loadDefinitions(in); } catch (ParseException exception) { console.reportError(exception); in.close(); return; } in.close(); } catch (IOException exception) { console.reportError(exception); return; } /* * Run all instructions and close the stream if it was a file one. */ console.run(); if (input != null) try { input.close(); } catch (IOException exception) { console.reportError(exception); } } /** * Execute the specified instruction. * * @param instruction The instruction to execute. * @throws IOException if an I/O operation failed while writting to the * {@linkplain #out output stream}. * @throws ParseException if a line can't be parsed. * @throws FactoryException If a transform can't be created. * @throws TransformException if a transform failed. */ protected void execute(String instruction) throws IOException, ParseException, FactoryException, TransformException { String value = null; int i = instruction.indexOf('='); if (i >= 0) { value = instruction.substring(i+1).trim(); instruction = instruction.substring(0,i).trim(); } final StringTokenizer keywords = new StringTokenizer(instruction); if (keywords.hasMoreTokens()) { final String key0 = keywords.nextToken(); if (!keywords.hasMoreTokens()) { // ------------------------------- // exit // ------------------------------- if (key0.equalsIgnoreCase("exit")) { if (value != null) { throw unexpectedArgument("exit"); } stop(); return; } // ------------------------------- // stacktrace // ------------------------------- if (key0.equalsIgnoreCase("stacktrace")) { if (value != null) { throw unexpectedArgument("stacktrace"); } if (lastError != null) { lastError.printStackTrace(err); } return; } // ------------------------------- // transform = <the transform> // ------------------------------- if (key0.equalsIgnoreCase("transform")) { transform = (MathTransform) parseObject(value, MathTransform.class); sourceCRS = null; targetCRS = null; return; } } else { final String key1 = keywords.nextToken(); if (!keywords.hasMoreTokens()) { // ------------------------------- // print definition|crs|points // ------------------------------- if (key0.equalsIgnoreCase("print")) { if (value != null) { throw unexpectedArgument("print"); } if (key1.equalsIgnoreCase("set")) { printDefinitions(); return; } if (key1.equalsIgnoreCase("crs")) { printCRS(); return; } if (key1.equalsIgnoreCase("pts")) { printPts(); return; } } // ------------------------------- // set <name> = <wkt> // ------------------------------- if (key0.equalsIgnoreCase("set")) { addDefinition(key1, value); return; } // ------------------------------- // test tolerance = <vector> // ------------------------------- if (key0.equalsIgnoreCase("test")) { if (key1.equalsIgnoreCase("tolerance")) { tolerance = parseVector(value); return; } } // ------------------------------- // source|target crs = <wkt> // ------------------------------- if (key1.equalsIgnoreCase("crs")) { if (key0.equalsIgnoreCase("source")) { sourceCRS = (CoordinateReferenceSystem) parseObject(value, CoordinateReferenceSystem.class); transform = null; return; } if (key0.equalsIgnoreCase("target")) { targetCRS = (CoordinateReferenceSystem) parseObject(value, CoordinateReferenceSystem.class); transform = null; return; } } // ------------------------------- // source|target pt = <coords> // ------------------------------- if (key1.equalsIgnoreCase("pt")) { if (key0.equalsIgnoreCase("source")) { sourcePosition = new GeneralDirectPosition(parseVector(value)); return; } if (key0.equalsIgnoreCase("target")) { targetPosition = new GeneralDirectPosition(parseVector(value)); if (tolerance!=null && sourcePosition!=null) { update(); if (transform != null) { test(); } } return; } } } } } throw new ParseException(Errors.format(ErrorKeys.ILLEGAL_INSTRUCTION_$1, instruction), 0); } /** * Executes the "{@code print crs}" instruction. */ private void printCRS() throws FactoryException, IOException { final Locale locale = null; final Vocabulary resources = Vocabulary.getResources(locale); final TableWriter table = new TableWriter(out, TableWriter.SINGLE_VERTICAL_LINE); table.setMultiLinesCells(true); char separator = TableWriter.SINGLE_HORIZONTAL_LINE; if (sourceCRS!=null || targetCRS!=null) { table.writeHorizontalSeparator(); table.write(resources.getString(VocabularyKeys.SOURCE_CRS)); table.nextColumn(); table.write(resources.getString(VocabularyKeys.TARGET_CRS)); table.nextLine(); table.writeHorizontalSeparator(); if (sourceCRS != null) { table.write(parser.format(sourceCRS)); } table.nextColumn(); if (targetCRS != null) { table.write(parser.format(targetCRS)); } table.nextLine(); separator = TableWriter.DOUBLE_HORIZONTAL_LINE; } /* * Format the math transform and its inverse, if any. */ update(); if (transform != null) { table.nextLine(separator); table.write(resources.getString(VocabularyKeys.MATH_TRANSFORM)); table.nextColumn(); table.write(resources.getString(VocabularyKeys.INVERSE_TRANSFORM)); table.nextLine(); table.writeHorizontalSeparator(); table.write(parser.format(transform)); table.nextColumn(); try { table.write(parser.format(transform.inverse())); } catch (NoninvertibleTransformException exception) { table.write(exception.getLocalizedMessage()); } table.nextLine(); } table.writeHorizontalSeparator(); table.flush(); } /** * Print the source and target point, and their transforms. * * @throws FactoryException if the transform can't be computed. * @throws TransformException if a transform failed. * @throws IOException if an error occured while writing to the output stream. */ private void printPts() throws FactoryException, TransformException, IOException { update(); DirectPosition transformedSource = null; DirectPosition transformedTarget = null; String targetException = null; if (transform != null) { if (sourcePosition != null) { transformedSource = transform.transform(sourcePosition, null); } if (targetPosition != null) try { transformedTarget = transform.inverse().transform(targetPosition, null); } catch (NoninvertibleTransformException exception) { targetException = exception.getLocalizedMessage(); if (sourcePosition != null) { final GeneralDirectPosition p; transformedTarget = p = new GeneralDirectPosition(sourcePosition.getDimension()); Arrays.fill(p.ordinates, Double.NaN); } } } final Locale locale = null; final Vocabulary resources = Vocabulary.getResources(locale); final TableWriter table = new TableWriter(out, 0); table.setMultiLinesCells(true); table.writeHorizontalSeparator(); table.setAlignment(TableWriter.ALIGN_RIGHT); if (sourcePosition != null) { table.write(resources.getLabel(VocabularyKeys.SOURCE_POINT)); print(sourcePosition, table); print(transformedSource, table); table.nextLine(); } if (targetPosition != null) { table.write(resources.getLabel(VocabularyKeys.TARGET_POINT)); print(transformedTarget, table); print(targetPosition, table); table.nextLine(); } if (sourceCRS!=null && targetCRS!=null) { table.write(resources.getLabel(VocabularyKeys.DISTANCE)); printDistance(sourceCRS, sourcePosition, transformedTarget, table); printDistance(targetCRS, targetPosition, transformedSource, table); table.nextLine(); } table.writeHorizontalSeparator(); table.flush(); if (targetException != null) { out.write(targetException); out.write(lineSeparator); } } /** * Print the specified point to the specified table. * This helper method is for use by {@link #printPts}. * * @param point The point to print, or {@code null} if none. * @throws IOException if an error occured while writting to the output stream. */ private void print(final DirectPosition point, final TableWriter table) throws IOException { if (point != null) { table.nextColumn(); table.write(" ("); final double[] coords = point.getCoordinates(); for (int i=0; i<coords.length; i++) { if (i != 0) { table.write(", "); } table.nextColumn(); table.write(numberFormat.format(coords[i])); } table.write(')'); } } /** * Print the distance between two points using the specified CRS. */ private void printDistance(final CoordinateReferenceSystem crs, final DirectPosition position1, final DirectPosition position2, final TableWriter table) throws IOException { if (position1 == null) { // Note: 'position2' is checked below, *after* blank columns insertion. return; } for (int i=crs.getCoordinateSystem().getDimension(); --i>=0;) { table.nextColumn(); } if (position2 != null) { if (crs instanceof AbstractCRS) try { final Measure distance; distance = ((AbstractCRS)crs).distance(position1.getCoordinates(), position2.getCoordinates()); table.setAlignment(TableWriter.ALIGN_RIGHT); table.write(numberFormat.format(distance.doubleValue())); table.write(" "); table.nextColumn(); table.write(String.valueOf(distance.getUnit())); table.setAlignment(TableWriter.ALIGN_LEFT); return; } catch (UnsupportedOperationException ignore) { /* * Underlying CRS do not supports distance computation. * Left the column blank. */ } } table.nextColumn(); } /////////////////////////////////////////////////////////// //////// //////// //////// H E L P E R M E T H O D S //////// //////// //////// /////////////////////////////////////////////////////////// /** * Invoked automatically when the {@code target pt} instruction were executed and a * {@code test tolerance} were previously set. The default implementation compares * the transformed source point with the expected target point. If a mismatch greater than * the tolerance error is found, an exception is thrown. Subclasses may overrides this * method in order to performs more tests. * * @throws TransformException if the source point can't be transformed, or a mistmatch is found. * @throws MismatchedDimensionException if the transformed source point doesn't have the * expected dimension. */ protected void test() throws TransformException, MismatchedDimensionException { final DirectPosition transformedSource = transform.transform(sourcePosition, null); final int sourceDim = transformedSource.getDimension(); final int targetDim = targetPosition.getDimension(); if (sourceDim != targetDim) { throw new MismatchedDimensionException(Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$2, sourceDim, targetDim)); } for (int i=0; i<sourceDim; i++) { // Use '!' for catching NaN. if (!(Math.abs(transformedSource.getOrdinate(i) - targetPosition.getOrdinate(i)) <= tolerance[Math.min(i, tolerance.length-1)])) { throw new TransformException("Expected " + targetPosition + " but got " + transformedSource); } } } /** * Check if the specified string start and end with the specified delimitors, * and returns the string without the delimitors. * * @param text The string to check. * @param start The delimitor required at the string begining. * @param end The delimitor required at the string end. */ private static String removeDelimitors(String text, final char start, final char end) { text = text.trim(); final int endPos = text.length()-1; if (endPos >= 1) { if (text.charAt(0)==start && text.charAt(endPos)==end) { text = text.substring(1, endPos).trim(); } } return text; } /** * Parse a vector of values. Vectors are used for coordinate points. * Example: * <pre> * (46.69439222, 13.91405611, 41.21) * </pre> * * @param text The vector to parse. * @return The vector as floating point numbers. * @throws ParseException if a number can't be parsed. */ private double[] parseVector(String text) throws ParseException { text = removeDelimitors(text, '(', ')'); final StringTokenizer st = new StringTokenizer(text, numberSeparator); final double[] values = new double[st.countTokens()]; for (int i=0; i<values.length; i++) { // Note: we need to convert the number to upper-case because // NumberParser seems to accepts "1E-10" but not "1e-10". final String token = st.nextToken().trim().toUpperCase(locale); final ParsePosition position = new ParsePosition(0); final Number result = numberFormat.parse(token, position); if (position.getIndex() != token.length()) { throw new ParseException(Errors.format(ErrorKeys.UNPARSABLE_NUMBER_$1, token), position.getErrorIndex()); } values[i] = result.doubleValue(); } return values; } /** * Update the internal state after a change, before to apply transformation. * The most important change is to update the math transform, if needed. */ private void update() throws FactoryException { if (transform==null && sourceCRS!=null && targetCRS!=null) { transform = factory.createOperation(sourceCRS, targetCRS).getMathTransform(); } } /** * Constructs an exception saying that an argument was unexpected. * * @param instruction The instruction name. * @return The exception to throws. */ private static ParseException unexpectedArgument(final String instruction) { return new ParseException(Errors.format(ErrorKeys.UNEXPECTED_ARGUMENT_FOR_INSTRUCTION_$1, instruction), 0); } /** * {@inheritDoc} * * @param exception The exception to report. */ @Override protected void reportError(final Exception exception) { super.reportError(exception); lastError = exception; } }