/* * 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.Map; import java.util.HashMap; import java.util.LinkedList; import java.util.Collections; import java.awt.RenderingHints; import javax.measure.converter.ConversionException; import org.opengis.metadata.quality.PositionalAccuracy; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.FactoryException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.*; import static org.opengis.referencing.IdentifiedObject.NAME_KEY; import org.geotools.factory.Hints; import org.geotools.metadata.iso.citation.Citations; import org.geotools.metadata.iso.quality.PositionalAccuracyImpl; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.factory.ReferencingFactory; import org.geotools.referencing.factory.ReferencingFactoryContainer; import org.geotools.referencing.cs.AbstractCS; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.resources.Classes; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.util.CanonicalSet; import org.geotools.util.Utilities; import static org.geotools.referencing.CRS.equalsIgnoreMetadata; /** * Base class for coordinate operation factories. This class provides helper methods for the * construction of building blocks. It doesn't figure out any operation path by itself. This * more "intelligent" job is left to subclasses. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public abstract class AbstractCoordinateOperationFactory extends ReferencingFactory implements CoordinateOperationFactory { /** * The identifier for an identity operation. */ protected static final ReferenceIdentifier IDENTITY = new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.IDENTITY)); /** * The identifier for conversion using an affine transform for axis swapping and/or * unit conversions. */ protected static final ReferenceIdentifier AXIS_CHANGES = new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.AXIS_CHANGES)); /** * The identifier for a transformation which is a datum shift. * * @see PositionalAccuracyImpl#DATUM_SHIFT_APPLIED */ protected static final ReferenceIdentifier DATUM_SHIFT = new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.DATUM_SHIFT)); /** * The identifier for a transformation which is a datum shift without * {@linkplain org.geotools.referencing.datum.BursaWolfParameters Bursa Wolf parameters}. * Only the changes in ellipsoid axis-length are taken in account. Such ellipsoid shifts * are approximative and may have 1 kilometer error. This transformation is allowed * only if the factory was created with {@link Hints#LENIENT_DATUM_SHIFT} set to * {@link Boolean#TRUE}. * * @see PositionalAccuracyImpl#DATUM_SHIFT_OMITTED */ protected static final ReferenceIdentifier ELLIPSOID_SHIFT = new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.ELLIPSOID_SHIFT)); /** * The identifier for a geocentric conversion. */ protected static final ReferenceIdentifier GEOCENTRIC_CONVERSION = new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.GEOCENTRIC_TRANSFORM)); /** * The identifier for an inverse operation. */ protected static final ReferenceIdentifier INVERSE_OPERATION = new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational(VocabularyKeys.INVERSE_OPERATION)); /** * The set of helper methods on factories. * * @see #getFactoryGroup */ private final ReferencingFactoryContainer factories; /** * The underlying math transform factory. This factory is used * for constructing {@link MathTransform} objects for all * {@linkplain CoordinateOperation coordinate operations}. * * @see #getMathTransformFactory */ private final MathTransformFactory mtFactory; /** * A pool of coordinate operation. This pool is used in order * to returns instance of existing operations when possible. */ private final CanonicalSet<CoordinateOperation> pool = CanonicalSet.newInstance(CoordinateOperation.class); /** * Tells if {@link FactoryGroup#hints} has been invoked. It must be invoked exactly once, * but can't be invoked in the constructor because it causes a {@link StackOverflowError} * in some situations. */ private boolean hintsInitialized; /** * Constructs a coordinate operation factory using the specified hints. * This constructor recognizes the {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS}, * {@link Hints#DATUM_FACTORY DATUM} and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM} * {@code FACTORY} hints. * * @param userHints The hints, or {@code null} if none. */ public AbstractCoordinateOperationFactory(final Hints userHints) { this(userHints, NORMAL_PRIORITY); } /** * Constructs a coordinate operation factory using the specified hints and priority. * This constructor recognizes the {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS}, * {@link Hints#DATUM_FACTORY DATUM} and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM} * {@code FACTORY} hints. * * @param userHints The hints, or {@code null} if none. * @param priority The priority for this factory, as a number between * {@link #MINIMUM_PRIORITY MINIMUM_PRIORITY} and * {@link #MAXIMUM_PRIORITY MAXIMUM_PRIORITY} inclusive. * * @since 2.2 */ public AbstractCoordinateOperationFactory(final Hints userHints, final int priority) { super(priority); factories = ReferencingFactoryContainer.instance(userHints); mtFactory = factories.getMathTransformFactory(); } /** * If the specified factory is an instance of {@code AbstractCoordinateOperationFactory}, * fetch the {@link FactoryGroup} from this instance instead of from the hints. This * constructor is strictly reserved for factory subclasses that are wrapper around an * other factory, like {@link BufferedCoordinateOperationFactory}. */ AbstractCoordinateOperationFactory(final CoordinateOperationFactory factory, final Hints hints, final int priority) { super(priority); if (factory instanceof AbstractCoordinateOperationFactory) { factories = ((AbstractCoordinateOperationFactory) factory).getFactoryContainer(); } else { factories = ReferencingFactoryContainer.instance(hints); } mtFactory = factories.getMathTransformFactory(); } /** * Returns the implementation hints for this factory. The returned map contains values for * {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS}, {@link Hints#DATUM_FACTORY DATUM} * and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM} {@code FACTORY} hints. Other values * may be provided as well, at implementation choice. */ @Override public Map<RenderingHints.Key,?> getImplementationHints() { synchronized (hints) { // Note: avoid lock on public object. if (!hintsInitialized) { initializeHints(); hintsInitialized = true; // Set only after success. } } return super.getImplementationHints(); } /** * Invoked when the {@link #hints} map should be initialized. This method may * be overridden by subclasses like {@link BufferedCoordinateOperationFactory}. */ void initializeHints() { assert Thread.holdsLock(hints); final ReferencingFactoryContainer factories = getFactoryContainer(); hints.putAll(factories.getImplementationHints()); } /** * Returns the underlying math transform factory. This factory * is used for constructing {@link MathTransform} objects for * all {@linkplain CoordinateOperation coordinate operations}. * * @return The underlying math transform factory. */ public final MathTransformFactory getMathTransformFactory() { return mtFactory; } /** * Returns the set of helper methods on factories. */ final ReferencingFactoryContainer getFactoryContainer() { return factories; } /** * Returns an affine transform between two coordinate systems. Only units and * axis order (e.g. transforming from (NORTH,WEST) to (EAST,NORTH)) are taken * in account. * <p> * Example: If coordinates in {@code sourceCS} are (x,y) pairs in metres and * coordinates in {@code targetCS} are (-y,x) pairs in centimetres, then the * transformation can be performed as below: * * <pre><blockquote> * [-y(cm)] [ 0 -100 0 ] [x(m)] * [ x(cm)] = [ 100 0 0 ] [y(m)] * [ 1 ] [ 0 0 1 ] [1 ] * </blockquote></pre> * * @param sourceCS The source coordinate system. * @param targetCS The target coordinate system. * @return The transformation from {@code sourceCS} to {@code targetCS} as * an affine transform. Only axis orientation and units are taken in account. * @throws OperationNotFoundException If the affine transform can't be constructed. * * @see AbstractCS#swapAndScaleAxis */ protected Matrix swapAndScaleAxis(final CoordinateSystem sourceCS, final CoordinateSystem targetCS) throws OperationNotFoundException { try { return AbstractCS.swapAndScaleAxis(sourceCS,targetCS); } catch (IllegalArgumentException exception) { throw new OperationNotFoundException(getErrorMessage(sourceCS, targetCS), exception); } catch (ConversionException exception) { throw new OperationNotFoundException(getErrorMessage(sourceCS, targetCS), exception); } // No attempt to catch ClassCastException since such // exception would indicates a programming error. } /** * Returns the specified identifier in a map to be given to coordinate operation constructors. * In the special case where the {@code name} identifier is {@link #DATUM_SHIFT} or * {@link #ELLIPSOID_SHIFT}, the map will contains extra informations like positional * accuracy. * * @todo In the datum shift case, an operation version is mandatory but unknow at this time. * However, we noticed that the EPSG database do not always defines a version neither. * Consequently, the Geotools implementation relax the rule requirying an operation * version and we do not try to provide this information here for now. */ private static Map<String,Object> getProperties(final ReferenceIdentifier name) { final Map<String,Object> properties; if (name==DATUM_SHIFT || name==ELLIPSOID_SHIFT) { properties = new HashMap<String,Object>(4); properties.put(NAME_KEY, name); properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new PositionalAccuracy[] { name==DATUM_SHIFT ? PositionalAccuracyImpl.DATUM_SHIFT_APPLIED : PositionalAccuracyImpl.DATUM_SHIFT_OMITTED}); } else { properties = Collections.singletonMap(NAME_KEY, (Object) name); } return properties; } /** * Creates a coordinate operation from a matrix, which usually describes an affine tranform. * A default {@link OperationMethod} object is given to this transform. In the special case * where the {@code name} identifier is {@link #DATUM_SHIFT} or {@link #ELLIPSOID_SHIFT}, * the operation will be an instance of {@link Transformation} instead of the usual * {@link Conversion}. * * @param name The identifier for the operation to be created. * @param sourceCRS The source coordinate reference system. * @param targetCRS The target coordinate reference system. * @param matrix The matrix which describe an affine transform operation. * @return The conversion or transformation. * @throws FactoryException if the operation can't be created. */ protected CoordinateOperation createFromAffineTransform( final ReferenceIdentifier name, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final Matrix matrix) throws FactoryException { final MathTransform transform = mtFactory.createAffineTransform(matrix); final Map<String,?> properties = getProperties(name); final Class<? extends Operation> type = properties.containsKey(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY) ? Transformation.class : Conversion.class; return createFromMathTransform(properties, sourceCRS, targetCRS, transform, ProjectiveTransform.ProviderAffine.getProvider(transform.getSourceDimensions(), transform.getTargetDimensions()), type); } /** * Creates a coordinate operation from a set of parameters. * The {@linkplain OperationMethod operation method} is inferred automatically, * if possible. * * @param name The identifier for the operation to be created. * @param sourceCRS The source coordinate reference system. * @param targetCRS The target coordinate reference system. * @param parameters The parameters. * @return The conversion or transformation. * @throws FactoryException if the operation can't be created. */ protected CoordinateOperation createFromParameters( final ReferenceIdentifier name, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final ParameterValueGroup parameters) throws FactoryException { final Map<String,?> properties = getProperties(name); final MathTransform transform = mtFactory.createParameterizedTransform(parameters); final OperationMethod method = mtFactory.getLastMethodUsed(); return createFromMathTransform(properties, sourceCRS, targetCRS, transform, method, Operation.class); } /** * Creates a coordinate operation from a math transform. * * @param name The identifier for the operation to be created. * @param sourceCRS The source coordinate reference system. * @param targetCRS The destination coordinate reference system. * @param transform The math transform. * @return A coordinate operation using the specified math transform. * @throws FactoryException if the operation can't be constructed. */ protected CoordinateOperation createFromMathTransform( final ReferenceIdentifier name, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final MathTransform transform) throws FactoryException { return createFromMathTransform(Collections.singletonMap(NAME_KEY, name), sourceCRS, targetCRS, transform, null, CoordinateOperation.class); } /** * Creates a coordinate operation from a math transform. * If the specified math transform is already a coordinate operation, and if source * and target CRS match, then {@code transform} is returned with no change. * Otherwise, a new coordinate operation is created. * * @param properties The properties to give to the operation. * @param sourceCRS The source coordinate reference system. * @param targetCRS The destination coordinate reference system. * @param transform The math transform. * @param method The operation method, or {@code null}. * @param type The required super-class (e.g. <code>{@linkplain Transformation}.class</code>). * @return A coordinate operation using the specified math transform. * @throws FactoryException if the operation can't be constructed. */ protected CoordinateOperation createFromMathTransform( final Map<String,?> properties, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final MathTransform transform, final OperationMethod method, final Class<? extends CoordinateOperation> type) throws FactoryException { CoordinateOperation operation; if (transform instanceof CoordinateOperation) { operation = (CoordinateOperation) transform; if (Utilities.equals(operation.getSourceCRS(), sourceCRS) && Utilities.equals(operation.getTargetCRS(), targetCRS) && Utilities.equals(operation.getMathTransform(), transform)) { if (operation instanceof Operation) { if (Utilities.equals(((Operation) operation).getMethod(), method)) { return operation; } } else { return operation; } } } operation = DefaultOperation.create(properties, sourceCRS, targetCRS, transform, method, type); operation = pool.unique(operation); return operation; } /** * Constructs a defining conversion from a set of properties. * * @param properties Set of properties. Should contains at least {@code "name"}. * @param method The operation method. * @param parameters The parameter values. * @return The defining conversion. * @throws FactoryException if the object creation failed. * * @see DefiningConversion * * @since 2.5 */ public Conversion createDefiningConversion( final Map<String,?> properties, final OperationMethod method, final ParameterValueGroup parameters) throws FactoryException { Conversion conversion = new DefiningConversion(properties, method, parameters); conversion = pool.unique(conversion); return conversion; } /** * Creates a concatenated operation from a sequence of operations. * * @param properties Set of properties. Should contains at least {@code "name"}. * @param operations The sequence of operations. * @return The concatenated operation. * @throws FactoryException if the object creation failed. */ public CoordinateOperation createConcatenatedOperation( final Map<String,?> properties, final CoordinateOperation[] operations) throws FactoryException { CoordinateOperation operation; operation = new DefaultConcatenatedOperation(properties, operations, mtFactory); operation = pool.unique(operation); return operation; } /** * Concatenate two operation steps. If an operation is an {@link #AXIS_CHANGES}, * it will be included as part of the second operation instead of creating an * {@link ConcatenatedOperation}. If a concatenated operation is created, it * will get an automatically generated name. * * @param step1 The first step, or {@code null} for the identity operation. * @param step2 The second step, or {@code null} for the identity operation. * @return A concatenated operation, or {@code null} if all arguments was nul. * @throws FactoryException if the operation can't be constructed. */ protected CoordinateOperation concatenate(final CoordinateOperation step1, final CoordinateOperation step2) throws FactoryException { if (step1 == null) return step2; if (step2 == null) return step1; if (false) { // Note: we sometime get this assertion failure if the user provided CRS with two // different ellipsoids but an identical TOWGS84 conversion infos (which is // usually wrong, but still happen). assert equalsIgnoreMetadata(step1.getTargetCRS(), step2.getSourceCRS()) : "CRS 1 =" + step1.getTargetCRS() + '\n' + "CRS 2 =" + step2.getSourceCRS(); } if (isIdentity(step1)) return step2; if (isIdentity(step2)) return step1; final MathTransform mt1 = step1.getMathTransform(); final MathTransform mt2 = step2.getMathTransform(); final CoordinateReferenceSystem sourceCRS = step1.getSourceCRS(); final CoordinateReferenceSystem targetCRS = step2.getTargetCRS(); CoordinateOperation step = null; if (step1.getName()==AXIS_CHANGES && mt1.getSourceDimensions()==mt1.getTargetDimensions()) step = step2; if (step2.getName()==AXIS_CHANGES && mt2.getSourceDimensions()==mt2.getTargetDimensions()) step = step1; if (step instanceof Operation) { /* * Applies only on operation in order to avoid merging with PassThroughOperation. * Also applies only if the transform to hide has identical source and target * dimensions in order to avoid mismatch with the method's dimensions. */ return createFromMathTransform(AbstractIdentifiedObject.getProperties(step), sourceCRS, targetCRS, mtFactory.createConcatenatedTransform(mt1, mt2), ((Operation) step).getMethod(), CoordinateOperation.class); } return createConcatenatedOperation(getTemporaryName(sourceCRS, targetCRS), new CoordinateOperation[] {step1, step2}); } /** * Concatenate three transformation steps. If the first and/or the last operation is an * {@link #AXIS_CHANGES}, it will be included as part of the second operation instead of * creating an {@link ConcatenatedOperation}. If a concatenated operation is created, it * will get an automatically generated name. * * @param step1 The first step, or {@code null} for the identity operation. * @param step2 The second step, or {@code null} for the identity operation. * @param step3 The third step, or {@code null} for the identity operation. * @return A concatenated operation, or {@code null} if all arguments were null. * @throws FactoryException if the operation can't be constructed. */ protected CoordinateOperation concatenate(final CoordinateOperation step1, final CoordinateOperation step2, final CoordinateOperation step3) throws FactoryException { if (step1 == null) return concatenate(step2, step3); if (step2 == null) return concatenate(step1, step3); if (step3 == null) return concatenate(step1, step2); assert equalsIgnoreMetadata(step1.getTargetCRS(), step2.getSourceCRS()) : step1; assert equalsIgnoreMetadata(step2.getTargetCRS(), step3.getSourceCRS()) : step3; if (isIdentity(step1)) return concatenate(step2, step3); if (isIdentity(step2)) return concatenate(step1, step3); if (isIdentity(step3)) return concatenate(step1, step2); if (step1.getName() == AXIS_CHANGES) return concatenate(concatenate(step1, step2), step3); if (step3.getName() == AXIS_CHANGES) return concatenate(step1, concatenate(step2, step3)); final CoordinateReferenceSystem sourceCRS = step1.getSourceCRS(); final CoordinateReferenceSystem targetCRS = step3.getTargetCRS(); return createConcatenatedOperation(getTemporaryName(sourceCRS, targetCRS), new CoordinateOperation[] {step1, step2, step3}); } /** * Returns {@code true} if the specified operation is an identity conversion. * This method always returns {@code false} for transformations even if their * associated math transform is an identity one, because such transformations * are usually datum shift and must be visible. */ private static boolean isIdentity(final CoordinateOperation operation) { return (operation instanceof Conversion) && operation.getMathTransform().isIdentity(); } /** * Returns the inverse of the specified operation. * * @param operation The operation to invert. * @return The inverse of {@code operation}. * @throws NoninvertibleTransformException if the operation is not invertible. * @throws FactoryException if the operation creation failed for an other reason. * * @since 2.3 */ protected CoordinateOperation inverse(final CoordinateOperation operation) throws NoninvertibleTransformException, FactoryException { final CoordinateReferenceSystem sourceCRS = operation.getSourceCRS(); final CoordinateReferenceSystem targetCRS = operation.getTargetCRS(); final Map<String,Object> properties = AbstractIdentifiedObject.getProperties(operation, null); properties.putAll(getTemporaryName(targetCRS, sourceCRS)); if (operation instanceof ConcatenatedOperation) { final LinkedList<CoordinateOperation> inverted = new LinkedList<CoordinateOperation>(); for (final CoordinateOperation op : ((ConcatenatedOperation) operation).getOperations()) { inverted.addFirst(inverse(op)); } return createConcatenatedOperation(properties, inverted.toArray(new CoordinateOperation[inverted.size()])); } final MathTransform transform = operation.getMathTransform().inverse(); final Class<? extends CoordinateOperation> type = AbstractCoordinateOperation.getType(operation); final OperationMethod method = (operation instanceof Operation) ? ((Operation) operation).getMethod() : null; return createFromMathTransform(properties, targetCRS, sourceCRS, transform, method, type); } ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// //////////// //////////// //////////// M I S C E L L A N E O U S //////////// //////////// //////////// ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// /** * Returns the dimension of the specified coordinate system, * or {@code 0} if the coordinate system is null. */ static int getDimension(final CoordinateReferenceSystem crs) { return (crs!=null) ? crs.getCoordinateSystem().getDimension() : 0; } /** * An identifier for temporary objects. This identifier manage a count of temporary * identifier. The count is appended to the identifier name (e.g. "WGS84 (step 1)"). */ private static final class TemporaryIdentifier extends NamedIdentifier { /** For cross-version compatibility. */ private static final long serialVersionUID = -2784354058026177076L; /** The parent identifier. */ private final ReferenceIdentifier parent; /** The temporary object count. */ private final int count; /** Constructs an identifier derived from the specified one. */ public TemporaryIdentifier(final ReferenceIdentifier parent) { this(parent, ((parent instanceof TemporaryIdentifier) ? ((TemporaryIdentifier) parent).count : 0) + 1); } /** Work around for RFE #4093999 in Sun's bug database */ private TemporaryIdentifier(final ReferenceIdentifier parent, final int count) { super(Citations.GEOTOOLS, unwrap(parent).getCode() + " (step " + count + ')'); this.parent = parent; this.count = count; } /** Returns the parent identifier for the specified identifier, if any. */ public static ReferenceIdentifier unwrap(ReferenceIdentifier identifier) { while (identifier instanceof TemporaryIdentifier) { identifier = ((TemporaryIdentifier) identifier).parent; } return identifier; } } /** * Returns the name of the GeoAPI interface implemented by the specified object. * In addition, the name may be added between brackets. */ private static String getClassName(final IdentifiedObject object) { if (object != null) { Class type = object.getClass(); final Class[] interfaces = type.getInterfaces(); for (int i=0; i<interfaces.length; i++) { final Class candidate = interfaces[i]; if (candidate.getName().startsWith("org.opengis.referencing.")) { type = candidate; break; } } String name = Classes.getShortName(type); final ReferenceIdentifier id = object.getName(); if (id != null) { name = name + '[' + id.getCode() + ']'; } return name; } return null; } /** * Returns a temporary name for object derived from the specified one. * * @param source The CRS to base name on, or {@code null} if none. */ static Map<String,Object> getTemporaryName(final IdentifiedObject source) { final Map<String,Object> properties = new HashMap<String,Object>(4); properties.put(NAME_KEY, new TemporaryIdentifier(source.getName())); properties.put(IdentifiedObject.REMARKS_KEY, Vocabulary.formatInternational( VocabularyKeys.DERIVED_FROM_$1, getClassName(source))); return properties; } /** * Returns a temporary name for object derived from a concatenation. * * @param source The CRS to base name on, or {@code null} if none. */ static Map<String,?> getTemporaryName(final CoordinateReferenceSystem source, final CoordinateReferenceSystem target) { final String name = getClassName(source) + " \u21E8 " + getClassName(target); return Collections.singletonMap(NAME_KEY, name); } /** * Returns an error message for "No path found from sourceCRS to targetCRS". * This is used for the construction of {@link OperationNotFoundException}. * * @param source The source CRS. * @param target The target CRS. * @return A default error message. */ protected static String getErrorMessage(final IdentifiedObject source, final IdentifiedObject target) { return Errors.format(ErrorKeys.NO_TRANSFORMATION_PATH_$2, getClassName(source), getClassName(target)); } /** * Makes sure an argument is non-null. * * @param name Argument name. * @param object User argument. * @throws IllegalArgumentException if {@code object} is null. */ protected static void ensureNonNull(final String name, final Object object) throws IllegalArgumentException { if (object == null) { throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name)); } } }