blob: 7d8773423dc114b932e245ba212f3b35a8c0740d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.console;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Locale;
import java.util.logging.Logger;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.BufferedReader;
import java.text.NumberFormat;
import javax.measure.Unit;
import javax.measure.IncommensurableException;
import org.opengis.metadata.Metadata;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.ReferenceSystem;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.SingleOperation;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
import org.opengis.referencing.operation.ConcatenatedOperation;
import org.opengis.referencing.operation.PassThroughOperation;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.privy.Formulas;
import org.apache.sis.referencing.privy.DirectPositionView;
import org.apache.sis.referencing.privy.ReferencingUtilities;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.DataStores;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.base.CodeType;
import org.apache.sis.system.Modules;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.privy.X364;
import org.apache.sis.io.LineAppender;
import org.apache.sis.io.TableAppender;
import org.apache.sis.io.stream.IOUtilities;
import org.apache.sis.io.wkt.Colors;
import org.apache.sis.io.wkt.Transliterator;
import org.apache.sis.io.wkt.WKTFormat;
import org.apache.sis.io.wkt.Warnings;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.Units;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.logging.Logging;
// Specific to the main and geoapi-3.1 branches:
import org.opengis.geometry.MismatchedDimensionException;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.ObjectDomain;
/**
* The "transform" subcommand.
* The output is a comma separated values (CSV) file, with {@code '#'} as the first character of comment lines.
* The source and target CRS are mandatory and can be specified as EPSG codes, WKT strings, or read from data files.
* Those information are passed as options:
*
* <ul>
* <li>{@code --sourceCRS}: the coordinate reference system of input points.</li>
* <li>{@code --targetCRS}: the coordinate reference system of output points.</li>
* <li>{@code --operation}: the coordinate operation from source CRS to target CRS.</li>
* </ul>
*
* The {@code --operation} parameter is optional.
* If provided, then the {@code --sourceCRS} and {@code --targetCRS} parameters become optional.
* If the operation is specified together with the source and/or target CRS, then the operation
* is used in the middle and conversions from/to the specified CRS are concatenated before/after
* the specified operation.
*
* @author Martin Desruisseaux (Geomatys)
*/
final class TransformCommand extends FormattedOutputCommand {
/**
* The logger for command-line tools.
* Currently defined in this class because it is the only one to use directly this logger.
*/
private static final Logger LOGGER = Logger.getLogger(Modules.CONSOLE);
/**
* The coordinate operation from the given source CRS to target CRS.
* This is always the forward operation. The inverse needs to be used
* if the {@link #inverse} flag is {@code true}.
*/
private CoordinateOperation operation;
/**
* The user supplied or inferred source and target CRS. They are not necessarily the source and target CRS of
* the transform to apply on coordinate tuples, because they need to be swapped if {@link #inverse} is true.
*
* @see #getEffectiveCRS(boolean)
*/
private CoordinateReferenceSystem sourceCRS, targetCRS;
/**
* Whether to use the inverse of the transform given by {@link #operation}.
* The transform will be inverted after all other options have been applied.
*/
private final boolean inverse;
/**
* The transformation from source CRS to the domain of validity CRS, or {@code null} if none.
*/
private MathTransform toDomainOfValidity;
/**
* Resources for {@link #printHeader(short)}.
*/
private final Vocabulary resources;
/**
* Where to write the header before the data.
*/
private TableAppender outHeader;
/**
* The format to use for writing coordinate values.
*/
private NumberFormat coordinateFormat;
/**
* Width of coordinate values, in number of characters in coordinate {@link String} representations.
*/
private int coordinateWidth;
/**
* Suggested number of fraction digits for each coordinate values.
*/
private int[] numFractionDigits;
/**
* We will switch to scientific notation if the coordinate value to format is greater than this value.
*/
private double[] thresholdForScientificNotation;
/**
* The error message to report after we transformed all coordinates, or {@code null}.
*/
private String errorMessage;
/**
* The cause of {@link #errorMessage}, or {@code null} if none.
*/
private NumberFormatException errorCause;
/**
* Returns valid options for the {@code "transform"} commands.
*/
private static EnumSet<Option> options() {
return EnumSet.of(Option.INVERSE, Option.SOURCE_CRS, Option.TARGET_CRS, Option.OPERATION, Option.VERBOSE,
Option.LOCALE, Option.TIMEZONE, Option.ENCODING, Option.COLORS, Option.HELP, Option.DEBUG);
}
/**
* Creates the {@code "transform"} sub-command.
*/
TransformCommand(final int commandIndex, final Object[] args) throws InvalidOptionException {
super(commandIndex, args, options(), OutputFormat.WKT, OutputFormat.TEXT);
resources = Vocabulary.forLocale(locale);
inverse = options.containsKey(Option.INVERSE);
}
/**
* Creates the coordinate operation from the given authority code or file path.
* The result is assigned to the {@link #operation} field.
*
* @param identifier the authority code or file path.
* @throws InvalidOptionException if the coordinate operation cannot be read from the given identifier.
* @throws FactoryException if the operation failed for another reason.
*/
private void fetchOperation(Object identifier) throws Exception {
if (identifier instanceof CharSequence) {
final String c = identifier.toString();
if (CodeType.guess(c).isCRS) try {
var factory = (CoordinateOperationAuthorityFactory) CRS.getAuthorityFactory(null);
operation = factory.createCoordinateOperation(c);
return;
} catch (NoSuchAuthorityCodeException e) {
throw illegalOptionValue(Option.OPERATION, identifier, e);
}
identifier = IOUtilities.toFileOrURL(c, "UTF-8");
}
final Object path = identifier; // Because lambda function require effectively final variable.
if (path != null) {
operation = new OperationParser(path).read()
.orElseThrow(() -> illegalOptionValue(Option.OPERATION, path, null));
}
}
/**
* Fetches the source or target coordinate reference system from the value given to the specified option.
*
* @param option either {@link Option#SOURCE_CRS} or {@link Option#TARGET_CRS}.
* @param mandatory whether the option is mandatory.
* @return the coordinate reference system for the given option.
* @throws InvalidOptionException if the given option is missing or have an invalid value.
* @throws FactoryException if the operation failed for another reason.
*/
private CoordinateReferenceSystem fetchCRS(final Option option, final boolean mandatory) throws Exception {
final Object identifier = mandatory ? getMandatoryOption(option) : options.get(option);
if (identifier == null) {
return null;
}
if (identifier instanceof CharSequence) {
final String c = identifier.toString();
if (CodeType.guess(c).isCRS) try {
return CRS.forCode(c);
} catch (NoSuchAuthorityCodeException e) {
throw illegalOptionValue(option, identifier, e);
}
}
final Metadata metadata;
try (DataStore store = DataStores.open(identifier)) {
metadata = store.getMetadata();
}
if (metadata != null) {
for (final ReferenceSystem rs : metadata.getReferenceSystemInfo()) {
if (rs instanceof CoordinateReferenceSystem) {
return (CoordinateReferenceSystem) rs;
}
}
}
throw new InvalidOptionException(Errors.format(Errors.Keys.UnspecifiedCRS), option.label());
}
/**
* Creates the exception to throw for an illegal option value.
*/
private static InvalidOptionException illegalOptionValue(Option option, Object identifier, Exception cause) {
final String name = option.label();
return new InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, identifier), cause, name);
}
/**
* Transforms coordinates from the files given in argument or from the standard input stream.
*
* @return 0 on success, or an exit code if the command failed for a reason other than an uncaught Java exception.
*/
@Override
public int run() throws Exception {
fetchOperation(options.get(Option.OPERATION));
sourceCRS = fetchCRS(Option.SOURCE_CRS, operation == null);
targetCRS = fetchCRS(Option.TARGET_CRS, operation == null);
if (sourceCRS == null) sourceCRS = operation.getSourceCRS(); // May still be null.
if (targetCRS == null) targetCRS = operation.getTargetCRS();
/*
* Read all coordinates, so we can compute the area of interest.
* This will be used when searching for a coordinate operation.
*/
GeographicBoundingBox areaOfInterest = null;
List<double[]> points = List.of();
final boolean useStandardInput = useStandardInput();
if (useStandardInput || !files.isEmpty()) {
if (useStandardInput) {
try (LineNumberReader in = new LineNumberReader(new InputStreamReader(System.in, encoding))) {
points = readCoordinates(in, "stdin");
}
} else {
for (final Object file : files) {
final StorageConnector c = inputConnector(file);
try (BufferedReader in = IOUtilities.toBuffered(c.commit(Reader.class, "transform"))) {
points = readCoordinates(in, file);
}
}
}
try {
final CoordinateReferenceSystem crs = getEffectiveCRS(false);
final GeographicCRS domainOfValidityCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs, false, false);
if (domainOfValidityCRS != null) {
toDomainOfValidity = CRS.findOperation(crs, domainOfValidityCRS, null).getMathTransform();
areaOfInterest = computeAreaOfInterest(points);
}
} catch (FactoryException e) {
warning(e);
}
}
/*
* Now find or complete the coordinate operation. Note that the source and target CRS may be null
* if and only if a coordinate operation was explicitly specified. In that case, CRS are optional.
* If both a coordinate operation and CRS are present, this code ensures that the operation inputs
* and outputs match the specified CRS.
*/
if (operation == null) {
operation = CRS.findOperation(sourceCRS, targetCRS, areaOfInterest);
} else {
final var steps = new ArrayList<CoordinateOperation>(3);
if (sourceCRS != null) {
CoordinateReferenceSystem step = operation.getSourceCRS();
if (step != null && step != sourceCRS) {
steps.add(CRS.findOperation(sourceCRS, step, areaOfInterest));
}
}
steps.add(operation);
if (targetCRS != null) {
CoordinateReferenceSystem step = operation.getTargetCRS();
if (step != null && step != targetCRS) {
steps.add(CRS.findOperation(step, targetCRS, areaOfInterest));
}
}
if (steps.size() > 1) {
var factory = DefaultCoordinateOperationFactory.provider();
var properties = IdentifiedObjects.getProperties(operation, CoordinateOperation.IDENTIFIERS_KEY);
operation = factory.createConcatenatedOperation(properties, steps.toArray(CoordinateOperation[]::new));
}
}
/*
* At this point, the coordinate operation has been fully constructed. Replace the source and target CRS
* by the ones specified in the operation because they may have more accurate metadata (name, identifier)
* if they were fetched from the EPSG database.
*/
CoordinateReferenceSystem crs;
if ((crs = operation.getSourceCRS()) != null) sourceCRS = crs;
if ((crs = operation.getTargetCRS()) != null) targetCRS = crs;
/*
* Prints the header: source CRS, target CRS, operation steps and positional accuracy.
*/
outHeader = new TableAppender(new LineAppender(out), " ");
outHeader.setMultiLinesCells(true);
printHeader(Vocabulary.Keys.Source); printNameAndIdentifier(getEffectiveCRS(false), false);
printHeader(Vocabulary.Keys.Destination); printNameAndIdentifier(getEffectiveCRS(true), false);
printHeader(Vocabulary.Keys.Operations); printOperations(operation, false);
outHeader.nextLine();
printDomainOfValidity(operation.getDomains());
printAccuracy(CRS.getLinearAccuracy(operation));
if (options.containsKey(Option.VERBOSE)) {
printDetails();
}
outHeader.flush();
outHeader = null;
/*
* At this point we finished to write the header. If there is at least one input file,
* compute the number of digits to format and perform the actual coordinate operations.
*/
if (!points.isEmpty()) {
coordinateWidth = 15; // Must be set before computeNumFractionDigits(…).
coordinateFormat = NumberFormat.getInstance(Locale.US);
coordinateFormat.setGroupingUsed(false);
final CoordinateSystem cs = getEffectiveCRS(true).getCoordinateSystem();
computeNumFractionDigits(cs);
out.println();
printAxes(cs);
out.println();
transform(points);
if (errorMessage != null) {
error(errorMessage, errorCause);
}
}
return 0;
}
/**
* Appends the ANSI X3.64 sequence if colors are enabled, otherwise does nothing.
*/
private void outHeader(final X364 code) {
if (colors) {
outHeader.append(code.sequence());
}
}
/**
* Prints the character for commented lines.
*/
private void printCommentLinePrefix() {
outHeader(X364.FOREGROUND_GRAY);
outHeader.append("# ");
outHeader(X364.FOREGROUND_DEFAULT);
}
/**
* Prints the given string after the prefix comment. This is used for formatting
* some metadata in the header before to print transformed coordinates.
*
* @param key a {@code Vocabulary.Keys} constant for the header to print.
*/
private void printHeader(final short key) throws IOException {
printCommentLinePrefix();
resources.appendLabel(key, outHeader);
outHeader.nextColumn();
}
/**
* Prints the name and authority code (if any) of the given object.
*
* @param object the object for which to print name and identifier.
* @param idRequired {@code true} for printing the name only if an identifier is present.
* @return whether this method has printed something.
*/
private boolean printNameAndIdentifier(final IdentifiedObject object, final boolean idRequired) {
final String identifier = IdentifiedObjects.toString(IdentifiedObjects.getIdentifier(object, null));
if (idRequired && identifier == null) {
return false;
}
outHeader.append(object.getName().getCode());
if (identifier != null) {
outHeader.append(' ');
outHeader(X364.FOREGROUND_CYAN);
outHeader.append('(');
outHeader.append(identifier);
outHeader.append(')');
outHeader(X364.FOREGROUND_DEFAULT);
}
if (!idRequired) {
outHeader.nextLine();
}
return true;
}
/**
* Prints a summary of the given coordinate operation as a sequence of steps.
* If the operations is specified by EPSG, prints the EPSG name and code.
* Otherwise prints only the operation method names, since the coordinate operation names
* generated by SIS are not very meaningful.
*
* @param step the coordinate operation to print as a sequence of steps.
*/
private void printOperations(final CoordinateOperation step, boolean isNext) {
if (isNext) {
isNext = false;
outHeader(X364.FOREGROUND_GREEN);
outHeader.append(" → ");
outHeader(X364.FOREGROUND_DEFAULT);
}
if (!printNameAndIdentifier(step, true)) {
if (step instanceof ConcatenatedOperation) {
for (final CoordinateOperation op : ((ConcatenatedOperation) step).getOperations()) {
printOperations(op, isNext);
isNext = true;
}
} else if (step instanceof PassThroughOperation) {
printOperations(((PassThroughOperation) step).getOperation(), false);
} else if (step instanceof SingleOperation) {
outHeader.append(((SingleOperation) step).getMethod().getName().getCode());
}
}
}
/**
* Prints the accuracy.
*/
private void printAccuracy(double accuracy) throws IOException {
if (accuracy >= 0) {
if (accuracy == 0) {
accuracy = Formulas.LINEAR_TOLERANCE;
}
printHeader(Vocabulary.Keys.Accuracy);
outHeader(X364.FOREGROUND_YELLOW); // Same as Colors.DEFAULT for ElementKind.NUMBER
outHeader.append(Double.toString(accuracy));
outHeader(X364.FOREGROUND_DEFAULT);
outHeader.append(" metres");
outHeader.nextLine();
}
}
/**
* Prints a textual description of the domain of validity. This method tries to reduce the string length by
* the use of some heuristic rules based on the syntax used in EPSG dataset. For example the following string:
*
* <blockquote>Canada - onshore and offshore - Alberta; British Columbia (BC); Manitoba; New Brunswick (NB);
* Newfoundland and Labrador; Northwest Territories (NWT); Nova Scotia (NS); Nunavut; Ontario; Prince Edward
* Island (PEI); Quebec; Saskatchewan; Yukon.</blockquote>
*
* is replaced by:
*
* <blockquote>Canada - onshore and offshore</blockquote>
*/
private void printDomainOfValidity(final Collection<ObjectDomain> domains) throws IOException {
for (final ObjectDomain domain : domains) {
final Extent extent = domain.getDomainOfValidity();
final InternationalString description = extent.getDescription();
if (description != null) {
String text = description.toString(locale);
if (text.length() >= 80) {
int end = text.indexOf(';');
if (end >= 0) {
int s = text.lastIndexOf('-', end);
if (s >= 0) {
end = s;
}
text = text.substring(0, end).trim();
}
}
printHeader(Vocabulary.Keys.Domain);
outHeader.append(text);
outHeader.nextLine();
return;
}
}
}
/**
* Prints the coordinate operation or math transform in Well Known Text format.
* This information is printed only if the {@code --verbose} option was specified.
*/
private void printDetails() throws IOException, NoninvertibleTransformException {
final WKTFormat f = new WKTFormat(locale, timezone);
if (colors) f.setColors(Colors.DEFAULT);
f.setConvention(convention);
CharSequence[] lines = CharSequences.splitOnEOL(f.format(debug ? getMathTransform() : operation));
for (int i=0; i<lines.length; i++) {
if (i == 0) {
printHeader(Vocabulary.Keys.Details);
} else {
printCommentLinePrefix();
outHeader.nextColumn();
}
outHeader.append(lines[i]);
outHeader.nextLine();
}
final Warnings warnings = f.getWarnings();
if (warnings != null) {
lines = CharSequences.splitOnEOL(warnings.toString());
if (lines.length != 0) { // Paranoiac check.
printHeader(Vocabulary.Keys.Note);
outHeader.append(lines[0]);
outHeader.nextLine();
}
}
}
/**
* Prints a quoted text in the given color.
* If the given text contains the quote character, it will be escaped.
*/
private void printQuotedText(String text, int fieldWidth, final X364 color) {
final boolean quoted;
if (text.indexOf('"') >= 0) {
text = text.replace("\"", "\"\"");
quoted = true;
} else {
quoted = (text.indexOf(',') >= 0);
}
if (quoted) fieldWidth -= 2;
out.print(CharSequences.spaces(fieldWidth - text.length()));
color(color);
if (quoted) out.print('"');
out.print(text);
if (quoted) out.print('"');
color(X364.FOREGROUND_DEFAULT);
}
/*
* Prints the names of all coordinate system axes on the first row.
* This method does not add EOL character.
* This method opportunistically computes the suggested precision for formatting values.
*
* @throws ConversionException should never happen.
*/
private void printAxes(final CoordinateSystem cs) {
final int targetDim = cs.getDimension();
for (int i=0; i<targetDim; i++) {
if (i != 0) {
out.print(',');
}
final CoordinateSystemAxis axis = cs.getAxis(i);
String name = axis.getName().getCode();
name = Transliterator.DEFAULT.toShortAxisName(cs, axis.getDirection(), name);
final String unit = axis.getUnit().toString();
if (!unit.isEmpty()) {
name = name + " (" + unit + ')';
}
printQuotedText(name, coordinateWidth, X364.FOREGROUND_CYAN);
}
}
/**
* Computes the suggested precision for printing values in the given units.
*
* @throws IncommensurableException should never happen.
*/
private void computeNumFractionDigits(final CoordinateSystem cs) throws IncommensurableException {
final int dimension = cs.getDimension();
numFractionDigits = new int[dimension];
thresholdForScientificNotation = new double[dimension];
for (int i=0; i<dimension; i++) {
final Unit<?> unit = cs.getAxis(0).getUnit();
final Unit<?> source;
double precision;
if (Units.isLinear(unit)) {
precision = Formulas.LINEAR_TOLERANCE;
source = Units.METRE;
} else if (Units.isAngular(unit)) {
precision = Formulas.ANGULAR_TOLERANCE;
source = Units.DEGREE;
} else {
precision = 0.001;
source = unit;
}
precision = source.getConverterToAny(unit).convert(precision);
if (precision > 0) {
numFractionDigits[i] = Math.max(DecimalFunctions.fractionDigitsForDelta(precision, false) + 1, 0);
}
thresholdForScientificNotation[i] = MathFunctions.pow10(coordinateWidth - 1 - numFractionDigits[i]);
}
}
/**
* Reads all coordinates.
* This method ignores empty and comment lines.
*
* @param in the stream from where to read coordinates.
* @param filename the filename, for error reporting only.
* @return the coordinate values.
*/
private List<double[]> readCoordinates(final BufferedReader in, Object filename) throws IOException {
final List<double[]> points = new ArrayList<>();
try {
String line;
while ((line = in.readLine()) != null) {
final int start = CharSequences.skipLeadingWhitespaces(line, 0, line.length());
if (start < line.length() && line.charAt(start) != '#') {
points.add(CharSequences.parseDoubles(line, ','));
}
}
} catch (NumberFormatException e) {
if ((filename = IOUtilities.toString(filename)) == null) filename = "?";
Object line = (in instanceof LineNumberReader) ? ((LineNumberReader) in).getLineNumber() : "?";
errorMessage = Errors.format(Errors.Keys.ErrorInFileAtLine_2, filename, line);
errorCause = e;
}
return points;
}
/**
* Computes the geographic area of interest from the given points.
* This method ignores the points having an unexpected number of dimensions since it is not this
* method's job to report those issues (they will be reported by {@link #transform(List)} instead.
*/
private GeographicBoundingBox computeAreaOfInterest(final List<double[]> points) {
final int dimension = toDomainOfValidity.getSourceDimensions();
final double[] domainCoordinate = new double[toDomainOfValidity.getTargetDimensions()];
if (domainCoordinate.length >= 2) {
double xmin = Double.POSITIVE_INFINITY;
double ymin = Double.POSITIVE_INFINITY;
double xmax = Double.NEGATIVE_INFINITY;
double ymax = Double.NEGATIVE_INFINITY;
for (final double[] coordinates : points) {
if (coordinates.length == dimension) {
try {
toDomainOfValidity.transform(coordinates, 0, domainCoordinate, 0, 1);
} catch (TransformException e) {
warning(e);
continue;
}
final double x = domainCoordinate[0];
final double y = domainCoordinate[1];
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
}
if (xmin < xmax && ymin < ymax) {
return new DefaultGeographicBoundingBox(xmin, xmax, ymin, ymax);
}
}
return null;
}
/**
* Returns the source or target CRS, taking in account whether the inverse transform is requested.
*
* @param target {@code false} for the source CRS, or {@code true} for the target CRS.
* @return the source or target CRS.
*/
private CoordinateReferenceSystem getEffectiveCRS(final boolean target) {
return (target ^ inverse) ? targetCRS : sourceCRS;
}
/**
* {@return the math transform of the operation, inverted if requested by user}.
*/
private MathTransform getMathTransform() throws NoninvertibleTransformException {
MathTransform mt = operation.getMathTransform();
if (inverse) mt = mt.inverse();
return mt;
}
/**
* Transforms the given coordinates.
*/
private void transform(final List<double[]> points) throws TransformException {
final MathTransform mt = getMathTransform();
final int dimension = mt.getSourceDimensions();
final double[] result = new double[mt.getTargetDimensions()];
final double[] domainCoordinate;
final DirectPositionView positionInDomain;
final ImmutableEnvelope domainOfValidity;
final GeographicBoundingBox bbox;
if (toDomainOfValidity != null && (bbox = CRS.getGeographicBoundingBox(operation)) != null) {
domainOfValidity = new ImmutableEnvelope(bbox);
domainCoordinate = new double[toDomainOfValidity.getTargetDimensions()];
positionInDomain = new DirectPositionView.Double(domainCoordinate);
} else {
domainOfValidity = null;
domainCoordinate = null;
positionInDomain = null;
}
for (final double[] coordinates : points) {
if (coordinates.length != dimension) {
throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimensionForCRS_3,
getEffectiveCRS(false).getName().getCode(), dimension, coordinates.length));
}
/*
* At this point we got the coordinates and they have the expected number of dimensions.
* Now perform the coordinate operation and print each coordinate values. We will switch
* to scientific notation if the coordinate is much larger than expected.
*/
mt.transform(coordinates, 0, result, 0, 1);
for (int i=0; i<result.length; i++) {
if (i != 0) {
out.print(',');
}
final double value = result[i];
final String s;
if (Math.abs(value) >= thresholdForScientificNotation[i]) {
s = Double.toString(value);
} else {
coordinateFormat.setMinimumFractionDigits(numFractionDigits[i]);
coordinateFormat.setMaximumFractionDigits(numFractionDigits[i]);
s = coordinateFormat.format(value);
}
out.print(CharSequences.spaces(coordinateWidth - s.length()));
out.print(s);
}
/*
* Append a warning after the transformed coordinate values if the source coordinate was outside
* the domain of validity. A failure to perform a coordinate transformation is also considered as
* being out of the domain of valididty.
*/
if (domainOfValidity != null) {
boolean inside;
try {
toDomainOfValidity.transform(coordinates, 0, domainCoordinate, 0, 1);
inside = domainOfValidity.contains(positionInDomain);
} catch (TransformException e) {
inside = false;
warning(e);
}
if (!inside) {
out.print(", ");
printQuotedText(Errors.forLocale(locale).getString(Errors.Keys.OutsideDomainOfValidity), 0, X364.FOREGROUND_RED);
}
}
out.println();
}
}
/**
* Reports the given exception as an ignorable one. We consider {@link FactoryException} or
* {@link TransformException} as ignorable exceptions only if they occurred while computing
* whether a point is inside the domain of validity. Failure to answer that question is
* considered as an indication that the point is outside the domain of validity.
*/
private static void warning(final Exception e) {
Logging.recoverableException(LOGGER, TransformCommand.class, "run", e);
}
}