/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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. * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.referencing.operation; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.Operation; import org.opengis.referencing.operation.Projection; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.OperationMethod; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.util.InternationalString; import org.geotools.parameter.Parameters; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.operation.transform.AbstractMathTransform; import org.geotools.referencing.operation.transform.ConcatenatedTransform; import org.geotools.referencing.operation.transform.PassThroughTransform; import org.geotools.referencing.wkt.Formatter; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; /** * Definition of an algorithm used to perform a coordinate operation. Most operation * methods use a number of operation parameters, although some coordinate conversions * use none. Each coordinate operation using the method assigns values to these parameters. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @see DefaultOperation */ public class DefaultOperationMethod extends AbstractIdentifiedObject implements OperationMethod { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -98032729598205972L; /** * List of localizable properties. To be given to {@link AbstractIdentifiedObject} constructor. */ private static final String[] LOCALIZABLES = {FORMULA_KEY}; /** * Formula(s) or procedure used by this operation method. This may be a reference to a * publication. Note that the operation method may not be analytic, in which case this * attribute references or contains the procedure, not an analytic formula. */ private final InternationalString formula; /** * Number of dimensions in the source CRS of this operation method. */ protected final int sourceDimensions; /** * Number of dimensions in the target CRS of this operation method. */ protected final int targetDimensions; /** * The set of parameters, or {@code null} if none. */ private final ParameterDescriptorGroup parameters; /** * Convenience constructor that creates an operation method from a math transform. * The information provided in the newly created object are approximative, and * usually acceptable only as a fallback when no other information are available. * * @param transform The math transform to describe. */ public DefaultOperationMethod(final MathTransform transform) { this(getProperties(transform), transform.getSourceDimensions(), transform.getTargetDimensions(), getDescriptor(transform)); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private static Map<String,?> getProperties(final MathTransform transform) { ensureNonNull("transform", transform); final Map<String,?> properties; if (transform instanceof AbstractMathTransform) { final AbstractMathTransform mt = (AbstractMathTransform) transform; properties = getProperties(mt.getParameterDescriptors(), null); } else { properties = Collections.singletonMap(NAME_KEY, Vocabulary.format(VocabularyKeys.UNKNOWN)); } return properties; } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). * This code should have been merged with {@code getProperties} above. */ private static ParameterDescriptorGroup getDescriptor(final MathTransform transform) { ParameterDescriptorGroup descriptor = null; if (transform instanceof AbstractMathTransform) { descriptor = ((AbstractMathTransform) transform).getParameterDescriptors(); } return descriptor; } /** * Constructs a new operation method with the same values than the specified one. * This copy constructor provides a way to wrap an arbitrary implementation into a * Geotools one or a user-defined one (as a subclass), usually in order to leverage * some implementation-specific API. This constructor performs a shallow copy, * i.e. the properties are not cloned. * * @param method The operation method to copy. */ public DefaultOperationMethod(final OperationMethod method) { super(method); formula = method.getFormula(); parameters = method.getParameters(); sourceDimensions = method.getSourceDimensions(); targetDimensions = method.getTargetDimensions(); } /** * Constructs a new operation method with the same values than the specified one * except the dimensions. * * @param method The operation method to copy. * @param sourceDimensions Number of dimensions in the source CRS of this operation method. * @param targetDimensions Number of dimensions in the target CRS of this operation method. */ public DefaultOperationMethod(final OperationMethod method, final int sourceDimensions, final int targetDimensions) { super(method); this.formula = method.getFormula(); this.parameters = method.getParameters(); this.sourceDimensions = sourceDimensions; this.targetDimensions = targetDimensions; ensurePositive("sourceDimensions", sourceDimensions); ensurePositive("targetDimensions", targetDimensions); } /** * Constructs an operation method from a set of properties and a descriptor group. * The properties given in argument follow the same rules than for the * {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}. * Additionally, the following properties are understood by this construtor: * <br><br> * <table border='1'> * <tr bgcolor="#CCCCFF" class="TableHeadingColor"> * <th nowrap>Property name</th> * <th nowrap>Value type</th> * <th nowrap>Value given to</th> * </tr> * <tr> * <td nowrap> {@link #FORMULA_KEY "formula"} </td> * <td nowrap> {@link String} or {@link InternationalString} </td> * <td nowrap> {@link #getFormula}</td> * </tr> * </table> * * @param properties Set of properties. Should contains at least {@code "name"}. * @param sourceDimensions Number of dimensions in the source CRS of this operation method. * @param targetDimensions Number of dimensions in the target CRS of this operation method. * @param parameters The set of parameters, or {@code null} if none. */ public DefaultOperationMethod(final Map<String,?> properties, final int sourceDimensions, final int targetDimensions, final ParameterDescriptorGroup parameters) { this(properties, new HashMap<String,Object>(), sourceDimensions, targetDimensions, parameters); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private DefaultOperationMethod(final Map<String,?> properties, final Map<String,Object> subProperties, final int sourceDimensions, final int targetDimensions, ParameterDescriptorGroup parameters) { super(properties, subProperties, LOCALIZABLES); formula = (InternationalString) subProperties.get(FORMULA_KEY); // 'parameters' may be null, which is okay. A null value will // make serialization smaller and faster than an empty object. this.parameters = parameters; this.sourceDimensions = sourceDimensions; this.targetDimensions = targetDimensions; ensurePositive("sourceDimensions", sourceDimensions); ensurePositive("targetDimensions", targetDimensions); } /** * Ensure that the specified value is positive. * An {@link IllegalArgumentException} is throws if it is not. * * @param name The parameter name. * @param value The parameter value. * @throws IllegalArgumentException if the specified value is not positive. */ private static void ensurePositive(final String name, final int value) throws IllegalArgumentException { if (value < 0) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, name, value)); } } /** * Formula(s) or procedure used by this operation method. This may be a reference to a * publication. Note that the operation method may not be analytic, in which case this * attribute references or contains the procedure, not an analytic formula. */ public InternationalString getFormula() { return formula; } /** * Number of dimensions in the source CRS of this operation method. * * @return The dimension of source CRS. */ public int getSourceDimensions() { return sourceDimensions; } /** * Number of dimensions in the target CRS of this operation method. */ public int getTargetDimensions() { return targetDimensions; } /** * Returns the set of parameters. */ public ParameterDescriptorGroup getParameters() { return (parameters!=null) ? parameters : Parameters.EMPTY_GROUP; } /** * Returns the operation type. Current implementation returns {@code Projection.class} for * proper WKT formatting using an unknow implementation. But the {@link MathTransformProvider} * subclass (with protected access) will overrides this method with a more conservative default * value. * * @return The GeoAPI interface implemented by this operation. */ Class<? extends Operation> getOperationType() { return Projection.class; } /** * Compare this operation method with the specified object for equality. * If {@code compareMetadata} is {@code true}, then all available * properties are compared including {@linkplain #getFormula formula}. * * @param object The object to compare to {@code this}. * @param compareMetadata {@code true} for performing a strict comparaison, or * {@code false} for comparing only properties relevant to transformations. * @return {@code true} if both objects are equal. */ @Override public boolean equals(final AbstractIdentifiedObject object, final boolean compareMetadata) { if (object == this) { return true; // Slight optimization. } if (super.equals(object, compareMetadata)) { final DefaultOperationMethod that = (DefaultOperationMethod) object; if (this.sourceDimensions == that.sourceDimensions && this.targetDimensions == that.targetDimensions && equals(this.parameters, that.parameters, compareMetadata)) { return !compareMetadata || Utilities.equals(this.formula, that.formula); } } return false; } /** * Returns a hash code value for this operation method. */ @Override public int hashCode() { int code = (int)serialVersionUID + sourceDimensions + 37*targetDimensions; if (parameters != null) { code = code * 37 + parameters.hashCode(); } return code; } /** * Format the inner part of a * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well * Known Text</cite> (WKT)</A> element. * * @param formatter The formatter to use. * @return The WKT element name. */ @Override protected String formatWKT(final Formatter formatter) { if (Projection.class.isAssignableFrom(getOperationType())) { return "PROJECTION"; } return super.formatWKT(formatter); } /** * Returns {@code true} if the specified transform is likely to exists only for axis switch * and/or unit conversions. The heuristic rule checks if the transform is backed by a square * matrix with exactly one non-null value in each row and each column. This method is used * for implementation of the {@link #checkDimensions} method only. */ private static boolean isTrivial(final MathTransform transform) { if (transform instanceof LinearTransform) { final Matrix matrix = ((LinearTransform) transform).getMatrix(); final int size = matrix.getNumRow(); if (matrix.getNumCol() == size) { for (int j=0; j<size; j++) { int n1=0, n2=0; for (int i=0; i<size; i++) { if (matrix.getElement(j,i) != 0) n1++; if (matrix.getElement(i,j) != 0) n2++; } if (n1 != 1 || n2 != 1) { return false; } } return true; } } return false; } /** * Checks if an operation method and a math transform have a compatible number of source * and target dimensions. In the particular case of a {@linkplain PassThroughTransform pass * through transform} with more dimension than the expected number, the check will rather be * performed against the {@linkplain PassThroughTransform#getSubTransform sub transform}. * <p> * This convenience method is provided for argument checking. * * @param method The operation method to compare to the math transform, or {@code null}. * @param transform The math transform to compare to the operation method, or {@code null}. * @throws MismatchedDimensionException if the number of dimensions are incompatibles. * * @todo The check for {@link ConcatenatedTransform} and {@link PassThroughTransform} works * only for Geotools implementation. */ public static void checkDimensions(final OperationMethod method, MathTransform transform) throws MismatchedDimensionException { if (method!=null && transform!=null) { int actual, expected=method.getSourceDimensions(); while ((actual=transform.getSourceDimensions()) > expected) { if (transform instanceof ConcatenatedTransform) { // Ignore axis switch and unit conversions. final ConcatenatedTransform c = (ConcatenatedTransform) transform; if (isTrivial(c.transform1)) { transform = c.transform2; } else if (isTrivial(c.transform2)) { transform = c.transform1; } else { // The transform is something more complex than an axis switch. // Stop the loop with the current illegal transform and let the // exception be thrown after the loop. break; } } else if (transform instanceof PassThroughTransform) { transform = ((PassThroughTransform) transform).getSubTransform(); } else { break; } } final String name; if (actual != expected) { name = "sourceDimensions"; } else { actual = transform.getTargetDimensions(); expected = method.getTargetDimensions(); if (actual != expected) { name = "targetDimensions"; } else { return; } } throw new IllegalArgumentException(Errors.format( ErrorKeys.MISMATCHED_DIMENSION_$3, name, actual, expected)); } } }