/* * 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. * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.referencing.operation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.opengis.metadata.quality.PositionalAccuracy; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.SingleOperation; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.ConcatenatedOperation; import org.opengis.referencing.operation.Transformation; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransformFactory; import org.geotools.referencing.wkt.Formatter; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.operation.transform.ConcatenatedTransform; import org.geotools.resources.UnmodifiableArrayList; import org.geotools.resources.Classes; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * An ordered sequence of two or more single coordinate operations. The sequence of operations is * constrained by the requirement that the source coordinate reference system of step * (<var>n</var>+1) must be the same as the target coordinate reference system of step * (<var>n</var>). The source coordinate reference system of the first step and the target * coordinate reference system of the last step are the source and target coordinate reference * system associated with the concatenated operation. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class DefaultConcatenatedOperation extends AbstractCoordinateOperation implements ConcatenatedOperation { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 4199619838029045700L; /** * The sequence of operations. */ private final List<SingleOperation> operations; /** * Constructs a concatenated operation from the specified name. * * @param name The operation name. * @param operations The sequence of operations. */ public DefaultConcatenatedOperation(final String name, final CoordinateOperation[] operations) { this(Collections.singletonMap(NAME_KEY, name), operations); } /** * Constructs a concatenated operation from a set of properties. * The properties given in argument follow the same rules than for the * {@link AbstractCoordinateOperation} constructor. * * @param properties Set of properties. Should contains at least {@code "name"}. * @param operations The sequence of operations. */ public DefaultConcatenatedOperation(final Map<String,?> properties, final CoordinateOperation[] operations) { this(properties, new ArrayList<SingleOperation>(operations!=null ? operations.length : 4), operations); } /** * Constructs a concatenated operation from a set of properties and a * {@linkplain MathTransformFactory math transform factory}. * The properties given in argument follow the same rules than for the * {@link AbstractCoordinateOperation} constructor. * * @param properties Set of properties. Should contains at least {@code "name"}. * @param operations The sequence of operations. * @param factory The math transform factory to use for math transforms concatenation. * @throws FactoryException if the factory can't concatenate the math transforms. */ public DefaultConcatenatedOperation(final Map<String,?> properties, final CoordinateOperation[] operations, final MathTransformFactory factory) throws FactoryException { this(properties, new ArrayList<SingleOperation>(operations!=null ? operations.length : 4), operations, factory); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private DefaultConcatenatedOperation(final Map<String,?> properties, final ArrayList<SingleOperation> list, final CoordinateOperation[] operations) { this(properties, expand(operations, list), list); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private DefaultConcatenatedOperation(final Map<String,?> properties, final ArrayList<SingleOperation> list, final CoordinateOperation[] operations, final MathTransformFactory factory) throws FactoryException { this(properties, expand(operations, list, factory, true), list); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private DefaultConcatenatedOperation(final Map<String,?> properties, final MathTransform transform, final List<SingleOperation> operations) { super(mergeAccuracy(properties, operations), operations.get(0).getSourceCRS(), operations.get(operations.size() - 1).getTargetCRS(), transform); this.operations = UnmodifiableArrayList.wrap( operations.toArray(new SingleOperation[operations.size()])); } /** * Work around for RFE #4093999 in Sun's bug database * ("Relax constraint on placement of this()/super() call in constructors"). */ private static MathTransform expand(final CoordinateOperation[] operations, final List<SingleOperation> list) { try { return expand(operations, list, null, true); } catch (FactoryException exception) { // Should not happen, since we didn't used any MathTransformFactory. throw new AssertionError(exception); } } /** * Transforms the list of operations into a list of single operations. This method * also check against null value and make sure that all CRS dimension matches. * * @param operations The array of operations to expand. * @param target The destination list in which to add {@code SingleOperation}. * @param factory The math transform factory to use, or {@code null} * @param wantTransform {@code true} if the concatenated math transform should be computed. * This is set to {@code false} only when this method invokes itself recursively. * @return The concatenated math transform. * @throws FactoryException if the factory can't concatenate the math transforms. */ private static MathTransform expand(final CoordinateOperation[] operations, final List<SingleOperation> target, final MathTransformFactory factory, final boolean wantTransform) throws FactoryException { MathTransform transform = null; ensureNonNull("operations", operations); for (int i=0; i<operations.length; i++) { ensureNonNull("operations", operations, i); final CoordinateOperation op = operations[i]; if (op instanceof SingleOperation) { target.add((SingleOperation) op); } else if (op instanceof ConcatenatedOperation) { final ConcatenatedOperation cop = (ConcatenatedOperation) op; final List<SingleOperation> cops = cop.getOperations(); expand(cops.toArray(new CoordinateOperation[cops.size()]), target, factory, false); } else { throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2, Classes.getClass(op), SingleOperation.class)); } /* * Check the CRS dimensions. */ if (i != 0) { final CoordinateReferenceSystem previous = operations[i-1].getTargetCRS(); final CoordinateReferenceSystem next = op .getSourceCRS(); if (previous!=null && next!=null) { final int dim1 = previous.getCoordinateSystem().getDimension(); final int dim2 = next.getCoordinateSystem().getDimension(); if (dim1 != dim2) { throw new IllegalArgumentException(Errors.format( ErrorKeys.MISMATCHED_DIMENSION_$2, dim1, dim2)); } } } /* * Concatenates the math transform. */ if (wantTransform) { final MathTransform step = op.getMathTransform(); if (transform == null) { transform = step; } else if (factory != null) { transform = factory.createConcatenatedTransform(transform, step); } else { transform = ConcatenatedTransform.create(transform, step); } } } if (wantTransform) { final int size = target.size(); if (size <= 1) { throw new IllegalArgumentException(Errors.format( ErrorKeys.MISSING_PARAMETER_$1, "operations[" + size + ']')); } } return transform; } /** * If no accuracy were specified in the given properties map, add all accuracies found in the * operation to concatenate. This method considers only {@link Transformation} components and * ignores all conversions. According ISO 19111, the accuracy attribute is allowed only for * transformations. However, this restriction is not enforced everywhere. The EPSG database * declares an accuracy of 0 meters for conversions, which is conceptually exact. Ourself we * are departing from the specification, since we are adding accuracy informations to a * concatenated operation. This departure should be considered as a convenience feature * only; accuracies are really relevant in transformations only. * <p> * There is also a technical reasons for ignoring conversions. If a concatenated operation * contains a datum shift (i.e. a transformation) with unknow accuracy, and a projection * (i.e. a conversion) with a declared 0 meter error, we don't want to declare this 0 meter * error as the concatenated operation's accuracy; it would be a false information. * <p> * Note that a concatenated operation typically contains an arbitrary amount of conversions, * but only one transformation. So considering transformation only usually means to pickup * only one operation in the given {@code operations} list. * * @todo We should use a Map and merge only one accuracy for each specification. */ private static Map<String,?> mergeAccuracy(final Map<String,?> properties, final List<? extends CoordinateOperation> operations) { if (!properties.containsKey(COORDINATE_OPERATION_ACCURACY_KEY)) { Set<PositionalAccuracy> accuracy = null; for (final CoordinateOperation op : operations) { if (op instanceof Transformation) { // See javadoc for a rational why we take only transformations in account. Collection<PositionalAccuracy> candidates = op.getCoordinateOperationAccuracy(); if (candidates!=null && !candidates.isEmpty()) { if (accuracy == null) { accuracy = new LinkedHashSet<PositionalAccuracy>(); } accuracy.addAll(candidates); } } } if (accuracy != null) { final Map<String,Object> merged = new HashMap<String,Object>(properties); merged.put(COORDINATE_OPERATION_ACCURACY_KEY, accuracy.toArray(new PositionalAccuracy[accuracy.size()])); return merged; } } return properties; } /** * Returns the sequence of operations. */ public List<SingleOperation> getOperations() { return operations; } /** * Compare this concatenated operation with the specified object for equality. * If {@code compareMetadata} is {@code true}, then all available properties are * compared including {@linkplain #getValidArea valid area} 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 DefaultConcatenatedOperation that = (DefaultConcatenatedOperation) object; return equals(this.operations, that.operations, compareMetadata); } return false; } /** * Returns a hash code value for this concatenated operation. */ @Override public int hashCode() { return operations.hashCode() ^ (int)serialVersionUID; } /** * {@inheritDoc} */ @Override protected String formatWKT(final Formatter formatter) { final String label = super.formatWKT(formatter); for (final Iterator it=operations.iterator(); it.hasNext();) { formatter.append((CoordinateOperation) it.next()); } return label; } }