/* * 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.Collection; import java.util.Collections; import java.util.Iterator; import java.util.HashMap; import java.util.Map; import javax.measure.unit.SI; import javax.measure.unit.Unit; import org.opengis.metadata.extent.Extent; import org.opengis.metadata.quality.Result; import org.opengis.metadata.quality.QuantitativeResult; import org.opengis.metadata.quality.PositionalAccuracy; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.ConcatenatedOperation; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.Transformation; import org.opengis.referencing.operation.Operation; import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.Projection; import org.opengis.referencing.operation.PlanarProjection; import org.opengis.referencing.operation.CylindricalProjection; import org.opengis.referencing.operation.ConicProjection; import org.opengis.referencing.IdentifiedObject; import org.opengis.util.InternationalString; import org.opengis.util.Record; import org.geotools.metadata.iso.quality.PositionalAccuracyImpl; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.crs.AbstractDerivedCRS; import org.geotools.referencing.wkt.Formatter; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Establishes an association between a source and a target coordinate reference system, * and provides a {@linkplain MathTransform transform} for transforming coordinates in * the source CRS to coordinates in the target CRS. Many but not all coordinate operations (from * {@linkplain CoordinateReferenceSystem coordinate reference system} <VAR>A</VAR> to * {@linkplain CoordinateReferenceSystem coordinate reference system} <VAR>B</VAR>) * also uniquely define the inverse operation (from * {@linkplain CoordinateReferenceSystem coordinate reference system} <VAR>B</VAR> to * {@linkplain CoordinateReferenceSystem coordinate reference system} <VAR>A</VAR>). * In some cases, the operation method algorithm for the inverse operation is the same * as for the forward algorithm, but the signs of some operation parameter values must * be reversed. In other cases, different algorithms are required for the forward and * inverse operations, but the same operation parameter values are used. If (some) * entirely different parameter values are needed, a different coordinate operation * shall be defined. * <p> * This class is conceptually <cite>abstract</cite>, even if it is technically possible to * instantiate it. Typical applications should create instances of the most specific subclass with * {@code Default} prefix instead. An exception to this rule may occurs when it is not possible to * identify the exact type. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class AbstractCoordinateOperation extends AbstractIdentifiedObject implements CoordinateOperation { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 1237358357729193885L; /** * An empty array of positional accuracy. This is usefull for fetching accuracies as an array, * using the following idiom: * <blockquote><pre> * {@linkplain #getPositionalAccuracy()}.toArray(EMPTY_ACCURACY_ARRAY); * </pre></blockquote> */ public static final PositionalAccuracy[] EMPTY_ACCURACY_ARRAY = new PositionalAccuracy[0]; /** * List of localizable properties. To be given to {@link AbstractIdentifiedObject} constructor. */ private static final String[] LOCALIZABLES = {SCOPE_KEY}; /** * The source CRS, or {@code null} if not available. */ protected final CoordinateReferenceSystem sourceCRS; /** * The target CRS, or {@code null} if not available. */ protected final CoordinateReferenceSystem targetCRS; /** * Version of the coordinate transformation * (i.e., instantiation due to the stochastic nature of the parameters). */ final String operationVersion; /** * Estimate(s) of the impact of this operation on point accuracy, or {@code null} * if none. */ private final Collection<PositionalAccuracy> coordinateOperationAccuracy; /** * Area in which this operation is valid, or {@code null} if not available. */ protected final Extent domainOfValidity; /** * Description of domain of usage, or limitations of usage, for which this operation is valid. */ private final InternationalString scope; /** * Transform from positions in the {@linkplain #getSourceCRS source coordinate reference system} * to positions in the {@linkplain #getTargetCRS target coordinate reference system}. */ protected final MathTransform transform; /** * Constructs a new coordinate operation with the same values than the specified * defining conversion, together with the specified source and target CRS. This * constructor is used by {@link DefaultConversion} only. */ AbstractCoordinateOperation(final Conversion definition, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final MathTransform transform) { super(definition); this.sourceCRS = sourceCRS; this.targetCRS = targetCRS; this.operationVersion = definition.getOperationVersion(); this.coordinateOperationAccuracy = definition.getCoordinateOperationAccuracy(); this.domainOfValidity = definition.getDomainOfValidity(); this.scope = definition.getScope(); this.transform = transform; } /** * Constructs a coordinate operation from a set of properties. * 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: * <p> * <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> {@value org.opengis.referencing.operation.CoordinateOperation#OPERATION_VERSION_KEY} </td> * <td nowrap> {@link String} </td> * <td nowrap> {@link #getOperationVersion}</td> * </tr> * <tr> * <td nowrap> {@value org.opengis.referencing.operation.CoordinateOperation#COORDINATE_OPERATION_ACCURACY_KEY} </td> * <td nowrap> <code>{@linkplain PositionalAccuracy}[]</code> </td> * <td nowrap> {@link #getCoordinateOperationAccuracy}</td> * </tr> * <tr> * <td nowrap> {@value org.opengis.referencing.operation.CoordinateOperation#DOMAIN_OF_VALIDITY_KEY} </td> * <td nowrap> {@link Extent} </td> * <td nowrap> {@link #getDomainOfValidity}</td> * </tr> * <tr> * <td nowrap> {@value org.opengis.referencing.operation.CoordinateOperation#SCOPE_KEY} </td> * <td nowrap> {@link String} or {@link InternationalString} </td> * <td nowrap> {@link #getScope}</td> * </tr> * </table> * * @param properties Set of properties. Should contains at least {@code "name"}. * @param sourceCRS The source CRS. * @param targetCRS The target CRS. * @param transform Transform from positions in the {@linkplain #getSourceCRS source CRS} * to positions in the {@linkplain #getTargetCRS target CRS}. */ public AbstractCoordinateOperation(final Map<String,?> properties, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final MathTransform transform) { this(properties, new HashMap<String,Object>(), sourceCRS, targetCRS, transform); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private AbstractCoordinateOperation(final Map<String,?> properties, final Map<String,Object> subProperties, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final MathTransform transform) { super(properties, subProperties, LOCALIZABLES); PositionalAccuracy[] positionalAccuracy; domainOfValidity = (Extent) subProperties.get(DOMAIN_OF_VALIDITY_KEY); scope = (InternationalString) subProperties.get(SCOPE_KEY); operationVersion = (String) subProperties.get(OPERATION_VERSION_KEY); positionalAccuracy = (PositionalAccuracy[]) subProperties.get(COORDINATE_OPERATION_ACCURACY_KEY); if (positionalAccuracy==null || positionalAccuracy.length==0) { positionalAccuracy = null; } else { positionalAccuracy = positionalAccuracy.clone(); for (int i=0; i<positionalAccuracy.length; i++) { ensureNonNull(COORDINATE_OPERATION_ACCURACY_KEY, positionalAccuracy, i); } } this.coordinateOperationAccuracy = asSet(positionalAccuracy); this.sourceCRS = sourceCRS; this.targetCRS = targetCRS; this.transform = transform; validate(); } /** * Checks the validity of this operation. This method is invoked by the constructor after * every fields have been assigned. It can be overriden by subclasses if different rules * should be applied. * <p> * {@link DefaultConversion} overrides this method in order to allow null values, providing * that all of {@code transform}, {@code sourceCRS} and {@code targetCRS} are null together. * Note that null values are not allowed for transformations, so {@link DefaultTransformation} * does not override this method. * * @throws IllegalArgumentException if at least one of {@code transform}, {@code sourceCRS} * or {@code targetCRS} is invalid. We throw this kind of exception rather than * {@link IllegalStateException} because this method is invoked by the constructor * for checking argument validity. */ void validate() throws IllegalArgumentException { ensureNonNull ("sourceCRS", transform); ensureNonNull ("targetCRS", transform); ensureNonNull ("transform", transform); checkDimension("sourceCRS", sourceCRS, transform.getSourceDimensions()); checkDimension("targetCRS", targetCRS, transform.getTargetDimensions()); } /** * Checks if a reference coordinate system has the expected number of dimensions. * * @param name The argument name. * @param crs The coordinate reference system to check. * @param expected The expected number of dimensions. */ private static void checkDimension(final String name, final CoordinateReferenceSystem crs, final int expected) { final int actual = crs.getCoordinateSystem().getDimension(); if (actual != expected) { throw new IllegalArgumentException(Errors.format( ErrorKeys.MISMATCHED_DIMENSION_$3, name, actual, expected)); } } /** * Returns the source CRS. */ public CoordinateReferenceSystem getSourceCRS() { return sourceCRS; } /** * Returns the target CRS. */ public CoordinateReferenceSystem getTargetCRS() { return targetCRS; } /** * Version of the coordinate transformation (i.e., instantiation due to the stochastic * nature of the parameters). Mandatory when describing a transformation, and should not * be supplied for a conversion. * * @return The coordinate operation version, or {@code null} in none. */ public String getOperationVersion() { return operationVersion; } /** * Estimate(s) of the impact of this operation on point accuracy. Gives * position error estimates for target coordinates of this coordinate * operation, assuming no errors in source coordinates. * * @return The position error estimates, or an empty collection if not available. * * @see #getAccuracy() * * @since 2.4 */ public Collection<PositionalAccuracy> getCoordinateOperationAccuracy() { if (coordinateOperationAccuracy == null) { return Collections.emptySet(); } return coordinateOperationAccuracy; } /** * Estimate(s) of the impact of this operation on point accuracy. Gives * position error estimates for target coordinates of this coordinate * operation, assuming no errors in source coordinates. * * @return The position error estimates, or an empty collection if not available. * * @see #getAccuracy() * * @deprecated Renamed as {@link #getCoordinateOperationAccuracy}. */ public Collection<PositionalAccuracy> getPositionalAccuracy() { if (coordinateOperationAccuracy == null) { return Collections.emptySet(); } return coordinateOperationAccuracy; } /** * Convenience method returning the accuracy in meters. The default implementation delegates * to <code>{@linkplain #getAccuracy(CoordinateOperation) getAccuracy}(this)</code>. Subclasses * should override this method if they can provide a more accurate algorithm. * * @return The accuracy in meters, or NaN if unknown. * * @since 2.2 */ public double getAccuracy() { return getAccuracy0(this); } /** * Convenience method returning the accuracy in meters for the specified operation. This method * try each of the following procedures and returns the first successful one: * * <ul> * <li>If a {@linkplain QuantitativeResult quantitative} positional accuracy is found with a * linear unit, then this accuracy estimate is converted to {@linkplain SI#METER meters} * and returned.</li> * * <li>Otherwise, if the operation is a {@linkplain Conversion conversion}, then returns * 0 since a conversion is by definition accurates up to rounding errors.</li> * * <li>Otherwise, if the operation is a {@linkplain Transformation transformation}, then * checks if the datum shift were applied with the help of Bursa-Wolf parameters. * This procedure looks for Geotools-specific * {@link PositionalAccuracyImpl#DATUM_SHIFT_APPLIED DATUM_SHIFT_APPLIED} and * {@link PositionalAccuracyImpl#DATUM_SHIFT_OMITTED DATUM_SHIFT_OMITTED} metadata. * If a datum shift has been applied, returns 25 meters. If a datum shift should have * been applied but has been omitted, returns 1000 meters. The 1000 meters value is * higher than the highest value (999 meters) found in the EPSG database version 6.7. * The 25 meters value is the next highest value found in the EPSG database for a * significant number of transformations. * * <li>Otherwise, if the operation is a {@linkplain ConcatenatedOperation concatenated one}, * returns the sum of the accuracy of all components.</li> * </ul> * * @param operation The operation to inspect for accuracy. * @return The accuracy estimate (always in meters), or NaN if unknow. * * @since 2.2 */ public static double getAccuracy(final CoordinateOperation operation) { if (operation instanceof AbstractCoordinateOperation) { // Maybe the user overridden this method... return ((AbstractCoordinateOperation) operation).getAccuracy(); } return getAccuracy0(operation); } /** * Implementation of {@code getAccuracy} methods, both the ordinary and the * static member variants. The {@link #getAccuracy()} method can't invoke * {@link #getAccuracy(CoordinateOperation)} directly since it would cause * never-ending recursive calls. */ private static double getAccuracy0(final CoordinateOperation operation) { final Collection<PositionalAccuracy> accuracies = operation.getCoordinateOperationAccuracy(); if (accuracies != null) for (final PositionalAccuracy accuracy : accuracies) { if (accuracy != null) for (final Result result : accuracy.getResults()) { if (result instanceof QuantitativeResult) { final QuantitativeResult quantity = (QuantitativeResult) result; final Collection<? extends Record> records = quantity.getValues(); if (records != null) { final Unit<?> unit = quantity.getValueUnit(); if (unit!=null && SI.METER.isCompatible(unit)) { for (final Record record : records) { for (final Object value : record.getAttributes().values()) { if (value instanceof Number) { double v = ((Number) value).doubleValue(); v = unit.getConverterTo(SI.METER).convert(v); return v; } } } } } } } } /* * No quantitative, linear accuracy were found. If the coordinate operation is actually * a conversion, the accuracy is up to rounding error (i.e. conceptually 0) by definition. */ if (operation instanceof Conversion) { return 0; } /* * If the coordinate operation is actually a transformation, checks if Bursa-Wolf * parameters were available for the datum shift. This is Geotools-specific. * See javadoc for a rational about the return values choosen. */ if (operation instanceof Transformation) { if (!accuracies.contains(PositionalAccuracyImpl.DATUM_SHIFT_OMITTED)) { if (accuracies.contains(PositionalAccuracyImpl.DATUM_SHIFT_APPLIED)) { return 25; } } return 1000; } /* * If the coordinate operation is a compound of other coordinate operations, returns * the sum of their accuracy, skipping unknow ones. */ double accuracy = Double.NaN; if (operation instanceof ConcatenatedOperation) { final Collection components = ((ConcatenatedOperation) operation).getOperations(); for (final Iterator it=components.iterator(); it.hasNext();) { final double candidate = Math.abs(getAccuracy((CoordinateOperation) it.next())); if (!Double.isNaN(candidate)) { if (Double.isNaN(accuracy)) { accuracy = candidate; } else { accuracy += candidate; } } } } return accuracy; } /** * Area or region or timeframe in which this coordinate operation is valid. * Returns {@code null} if not available. * * @since 2.4 */ public Extent getDomainOfValidity() { return domainOfValidity; } /** * Area in which this operation is valid. * * @return Coordinate operation valid area, or {@code null} if not available. * * @deprecated Renamed {@link #getDomainOfValidity}. */ public Extent getValidArea() { return domainOfValidity; } /** * Description of domain of usage, or limitations of usage, for which this operation is valid. */ public InternationalString getScope() { return scope; } /** * Gets the math transform. The math transform will transform positions in the * {@linkplain #getSourceCRS source coordinate reference system} into positions * in the {@linkplain #getTargetCRS target coordinate reference system}. */ public MathTransform getMathTransform() { return transform; } /** * Returns the most specific GeoAPI interface implemented by the specified operation. * * @param object A coordinate operation. * @return The most specific GeoAPI interface * (e.g. <code>{@linkplain Transformation}.class</code>). */ public static Class<? extends CoordinateOperation> getType(final CoordinateOperation object) { if (object instanceof Transformation) return Transformation.class; if (object instanceof ConicProjection) return ConicProjection.class; if (object instanceof CylindricalProjection) return CylindricalProjection.class; if (object instanceof PlanarProjection) return PlanarProjection.class; if (object instanceof Projection) return Projection.class; if (object instanceof Conversion) return Conversion.class; if (object instanceof Operation) return Operation.class; return CoordinateOperation.class; } /** * Compares this coordinate operation with the specified object for equality. * If {@code compareMetadata} is {@code true}, then all available properties are * compared including {@linkplain #getDomainOfValidity domain of validity} and * {@linkplain #getScope scope}. * * @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 AbstractCoordinateOperation that = (AbstractCoordinateOperation) object; if (equals(this.sourceCRS, that.sourceCRS, compareMetadata) && Utilities.equals(this.transform, that.transform)) // See comment in DefaultOperation.equals(...) about why we compare MathTransform. { if (compareMetadata) { if (!Utilities.equals(this.domainOfValidity, that.domainOfValidity) || !Utilities.equals(this.scope, that.scope) || !Utilities.equals(this.coordinateOperationAccuracy, that.coordinateOperationAccuracy)) { return false; } } /* * Avoid never-ending recursivity: AbstractDerivedCRS has a 'conversionFromBase' * field that is set to this AbstractCoordinateOperation. */ final Boolean comparing = AbstractDerivedCRS._COMPARING.get(); if (comparing!=null && comparing.booleanValue()) { return true; } try { AbstractDerivedCRS._COMPARING.set(Boolean.TRUE); return equals(this.targetCRS, that.targetCRS, compareMetadata); } finally { AbstractDerivedCRS._COMPARING.set(Boolean.FALSE); } } } return false; } /** * Returns a hash code value for this coordinate operation. */ @Override public int hashCode() { int code = (int)serialVersionUID; if (sourceCRS != null) code ^= sourceCRS.hashCode(); if (targetCRS != null) code ^= targetCRS.hashCode(); if (transform != null) code ^= transform.hashCode(); return code; } /** * Format this operation as a pseudo-WKT format. No WKT format were defined for coordinate * operation at the time this method was written. This method may change in any future version * until a standard format is found. * * @param formatter The formatter to use. * @return The WKT element name. */ @Override protected String formatWKT(final Formatter formatter) { append(formatter, sourceCRS, "SOURCE"); append(formatter, targetCRS, "TARGET"); return super.formatWKT(formatter); } /** * Append the identifier for the specified object name (possibly {@code null}) to the specified * formatter. * * @param formatter The formatter where to append the object name. * @param object The object to append, or {@code null} if none. * @param type The label to put in front of the object name. */ @SuppressWarnings("serial") static void append(final Formatter formatter, final IdentifiedObject object, final String type) { if (object != null) { final Map<String,Object> properties = new HashMap<String,Object>(4); properties.put(IdentifiedObject.NAME_KEY, formatter.getName(object)); properties.put(IdentifiedObject.IDENTIFIERS_KEY, formatter.getIdentifier(object)); formatter.append((IdentifiedObject) new AbstractIdentifiedObject(properties) { @Override protected String formatWKT(final Formatter formatter) { /* * Do not invoke super.formatWKT(formatter), since it doesn't do anything * more than invoking 'formatter.setInvalidWKT(...)' (we ignore the value * returned). This method will rather be invoked by the enclosing class. */ return type; } }); } } }