blob: f50d2a494c30666b2bd95c390ae5e0f727e942c7 [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.referencing.operation;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Collections;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchemaType;
import org.opengis.util.InternationalString;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.operation.Formula;
import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.Projection;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.SingleOperation;
import org.opengis.referencing.crs.GeneralDerivedCRS;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.jaxb.gco.StringAdapter;
import org.apache.sis.internal.jaxb.referencing.CC_OperationMethod;
import org.apache.sis.internal.referencing.NilReferencingObject;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.metadata.Identifiers;
import org.apache.sis.internal.metadata.MetadataUtilities;
import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
import org.apache.sis.parameter.Parameterized;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.io.wkt.ElementKind;
import org.apache.sis.io.wkt.FormattableObject;
import static org.apache.sis.util.ArgumentChecks.*;
/**
* Describes the algorithm and parameters used to perform a coordinate operation. An {@code OperationMethod}
* is a kind of metadata: it does not perform any coordinate operation (e.g. map projection) by itself, but
* tells us what is needed in order to perform such operation.
*
* <p>The most important parts of an {@code OperationMethod} are its {@linkplain #getName() name} and its
* {@linkplain #getParameters() group of parameter descriptors}. The parameter descriptors do not contain
* any value, but tell us what are the expected parameters, together with their units of measurement.</p>
*
* <div class="note"><b>Example:</b>
* An operation method named “<cite>Mercator (variant A)</cite>” (EPSG:9804) expects the following parameters:
* <ul>
* <li>“<cite>Latitude of natural origin</cite>” in degrees. Default value is 0°.</li>
* <li>“<cite>Longitude of natural origin</cite>” in degrees. Default value is 0°.</li>
* <li>“<cite>Scale factor at natural origin</cite>” as a dimensionless number. Default value is 1.</li>
* <li>“<cite>False easting</cite>” in metres. Default value is 0 m.</li>
* <li>“<cite>False northing</cite>” in metres. Default value is 0 m.</li>
* </ul></div>
*
* In Apache SIS implementation, the {@linkplain #getName() name} is the only mandatory property. However it is
* recommended to provide also {@linkplain #getIdentifiers() identifiers} (e.g. “EPSG:9804” in the above example)
* since names can sometime be ambiguous or be spelled in different ways.
*
* <h2>Departure from the ISO 19111 standard</h2>
* The following properties are mandatory according ISO 19111,
* but may be missing under some conditions in Apache SIS:
* <ul>
* <li>The {@linkplain #getFormula() formula} if it has not been provided to the
* {@linkplain #DefaultOperationMethod(Map, Integer, Integer, ParameterDescriptorGroup) constructor}, or if it
* can not be {@linkplain #DefaultOperationMethod(MathTransform) inferred from the given math transform}.</li>
* <li>The {@linkplain #getParameters() parameters} if the {@link #DefaultOperationMethod(MathTransform)}
* constructor can not infer them.</li>
* </ul>
*
* <h2>Relationship with other classes or interfaces</h2>
* {@code OperationMethod} describes parameters without providing any value (except sometime default values).
* When values have been assigned to parameters, the result is a {@link SingleOperation}.
* Note that there is different kinds of {@code SingleOperation} depending on the nature and accuracy of the
* coordinate operation. See {@link #getOperationType()} for more information.
*
* <p>The interface performing the actual work of taking coordinates in the
* {@linkplain AbstractCoordinateOperation#getSourceCRS() source CRS} and calculating the new coordinates in the
* {@linkplain AbstractCoordinateOperation#getTargetCRS() target CRS} is {@link MathTransform}.
* In order to allow Apache SIS to instantiate those {@code MathTransform}s from given parameter values,
* {@code DefaultOperationMethod} subclasses should implement the
* {@link org.apache.sis.referencing.operation.transform.MathTransformProvider} interface.</p>
*
* <h2>Immutability and thread safety</h2>
* This class is immutable and thread-safe if all properties given to the constructor are also immutable and thread-safe.
* It is strongly recommended for all subclasses to be thread-safe, especially the
* {@link org.apache.sis.referencing.operation.transform.MathTransformProvider} implementations to be used with
* {@link org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.7
*
* @see DefaultConversion
* @see DefaultTransformation
* @see org.apache.sis.referencing.operation.transform.MathTransformProvider
*
* @since 0.5
* @module
*/
@XmlType(name = "OperationMethodType", propOrder = {
"formulaCitation",
"formulaDescription",
"sourceDimensions",
"targetDimensions",
"descriptors"
})
@XmlRootElement(name = "OperationMethod")
public class DefaultOperationMethod extends AbstractIdentifiedObject implements OperationMethod {
/*
* NOTE FOR JAVADOC WRITER:
* The "method" word is ambiguous here, because it can be "Java method" or "coordinate operation method".
* In this class, we reserve the "method" word for "coordinate operation method" as much as possible.
*/
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = 2870579345991143357L;
/**
* 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.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setFormulaCitation(Citation)}
* or {@link #setFormulaDescription(String)}.</p>
*/
private Formula formula;
/**
* Number of dimensions in the source CRS of this operation method.
* May be {@code null} if this method can work with any number of
* source dimensions (e.g. <cite>Affine Transform</cite>).
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setSourceDimensions(Integer)}</p>
*
* @see #getSourceDimensions()
*/
private Integer sourceDimensions;
/**
* Number of dimensions in the target CRS of this operation method.
* May be {@code null} if this method can work with any number of
* target dimensions (e.g. <cite>Affine Transform</cite>).
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setTargetDimensions(Integer)}</p>
*
* @see #getTargetDimensions()
*/
private Integer targetDimensions;
/**
* The set of parameters, or {@code null} if none.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setDescriptors(GeneralParameterDescriptor[])}</p>
*/
private ParameterDescriptorGroup parameters;
/**
* Constructs an operation method from a set of properties and a descriptor group. The properties map is given
* unchanged to the {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}.
* In addition to the properties documented in the parent constructor,
* the following properties are understood by this constructor:
*
* <table class="sis">
* <caption>Recognized properties (non exhaustive list)</caption>
* <tr>
* <th>Property name</th>
* <th>Value type</th>
* <th>Returned by</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.operation.OperationMethod#FORMULA_KEY}</td>
* <td>{@link Formula}, {@link Citation} or {@link CharSequence}</td>
* <td>{@link #getFormula()}</td>
* </tr>
* <tr>
* <th colspan="3" class="hsep">Defined in parent classes (reminder)</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
* <td>{@link #getName()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
* <td>{@link org.opengis.util.GenericName} or {@link CharSequence} (optionally as array)</td>
* <td>{@link #getAlias()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
* <td>{@link #getIdentifiers()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getRemarks()}</td>
* </tr>
* </table>
*
* The source and target dimensions may be {@code null} if this method can work
* with any number of dimensions (e.g. <cite>Affine Transform</cite>).
*
* @param properties set of properties. Shall contain at least {@code "name"}.
* @param sourceDimensions number of dimensions in the source CRS of this operation method, or {@code null}.
* @param targetDimensions number of dimensions in the target CRS of this operation method, or {@code null}.
* @param parameters description of parameters expected by this operation.
*/
public DefaultOperationMethod(final Map<String,?> properties,
final Integer sourceDimensions,
final Integer targetDimensions,
final ParameterDescriptorGroup parameters)
{
super(properties);
if (sourceDimensions != null) ensurePositive("sourceDimensions", sourceDimensions);
if (targetDimensions != null) ensurePositive("targetDimensions", targetDimensions);
ensureNonNull("parameters", parameters);
Object value = properties.get(FORMULA_KEY);
if (value == null || value instanceof Formula) {
formula = (Formula) value;
} else if (value instanceof Citation) {
formula = new DefaultFormula((Citation) value);
} else if (value instanceof CharSequence) {
formula = new DefaultFormula((CharSequence) value);
} else {
throw new IllegalArgumentException(Errors.getResources(properties)
.getString(Errors.Keys.IllegalPropertyValueClass_2, FORMULA_KEY, value.getClass()));
}
this.parameters = parameters;
this.sourceDimensions = sourceDimensions;
this.targetDimensions = targetDimensions;
}
/**
* Convenience constructor that creates an operation method from a math transform.
* The information provided in the newly created object are approximations, 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) {
super(getProperties(transform));
sourceDimensions = transform.getSourceDimensions();
targetDimensions = transform.getTargetDimensions();
if (transform instanceof Parameterized) {
parameters = ((Parameterized) transform).getParameterDescriptors();
} else {
parameters = null;
}
formula = null;
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
@Workaround(library="JDK", version="1.7")
private static Map<String,?> getProperties(final MathTransform transform) {
ensureNonNull("transform", transform);
if (transform instanceof Parameterized) {
final ParameterDescriptorGroup parameters = ((Parameterized) transform).getParameterDescriptors();
if (parameters != null) {
return getProperties(parameters, null);
}
}
return Collections.singletonMap(NAME_KEY, NilReferencingObject.UNNAMED);
}
/**
* Returns the properties to be given to an identified object derived from the specified one.
* This method returns the same properties than the supplied argument
* (as of <code>{@linkplain IdentifiedObjects#getProperties getProperties}(info)</code>),
* except for the following:
*
* <ul>
* <li>The {@linkplain IdentifiedObject#getName() name}'s authority is replaced by the specified one.</li>
* <li>All {@linkplain IdentifiedObject#getIdentifiers identifiers} are removed, because the new object
* to be created is probably not endorsed by the original authority.</li>
* </ul>
*
* This method returns a mutable map. Consequently, callers can add their own identifiers
* directly to this map if they wish.
*
* @param info the identified object to view as a properties map.
* @param authority the new authority for the object to be created,
* or {@code null} if it is not going to have any declared authority.
* @return the identified object properties in a mutable map.
*/
private static Map<String,Object> getProperties(final IdentifiedObject info, final Citation authority) {
final Map<String,Object> properties = new HashMap<>(IdentifiedObjects.getProperties(info));
properties.put(NAME_KEY, new NamedIdentifier(authority, info.getName().getCode()));
properties.remove(IDENTIFIERS_KEY);
return properties;
}
/**
* Creates a new operation method with the same values than the specified one.
* This copy constructor provides a way to convert an arbitrary implementation into a SIS one
* or a user-defined one (as a subclass), usually in order to leverage some implementation-specific API.
*
* <p>This constructor performs a shallow copy, i.e. the properties are not cloned.</p>
*
* @param method the operation method to copy.
*
* @see #castOrCopy(OperationMethod)
*/
protected DefaultOperationMethod(final OperationMethod method) {
super(method);
formula = method.getFormula();
parameters = method.getParameters();
sourceDimensions = method.getSourceDimensions();
targetDimensions = method.getTargetDimensions();
}
/**
* Returns a SIS operation method implementation with the same values than the given arbitrary implementation.
* If the given object is {@code null}, then {@code null} is returned.
* Otherwise if the given object is already a SIS implementation, then the given object is returned unchanged.
* Otherwise a new SIS implementation is created and initialized to the attribute values of the given object.
*
* @param object the object to get as a SIS implementation, or {@code null} if none.
* @return a SIS implementation containing the values of the given object (may be the
* given object itself), or {@code null} if the argument was null.
*/
public static DefaultOperationMethod castOrCopy(final OperationMethod object) {
return (object == null) || (object instanceof DefaultOperationMethod)
? (DefaultOperationMethod) object : new DefaultOperationMethod(object);
}
/**
* Constructs a new operation method with the same values than the specified one except the dimensions.
* The source and target dimensions may be {@code null} if this method can work with any number of dimensions
* (e.g. <cite>Affine Transform</cite>).
*
* @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.
*/
private DefaultOperationMethod(final OperationMethod method,
final Integer sourceDimensions,
final Integer targetDimensions)
{
super(method);
this.formula = method.getFormula();
this.parameters = method.getParameters();
this.sourceDimensions = sourceDimensions;
this.targetDimensions = targetDimensions;
}
/**
* Returns an operation method with different dimensions, if we are allowed to change dimensionality.
* This method accepts to change a dimension only if the value specified by the original method
* is {@code null}. Otherwise an {@link IllegalArgumentException} is thrown.
*
* @param method the operation method to redimension.
* @param sourceDimensions the desired new source dimensions.
* @param methodSource the current number of source dimensions (may be {@code null}).
* @param targetDimensions the desired new target dimensions.
* @param methodTarget the current number of target dimensions (may be {@code null}).
* @throws IllegalArgumentException if the given dimensions are illegal for this operation method.
*/
private static OperationMethod redimension(final OperationMethod method,
final int sourceDimensions, final Integer methodSource,
final int targetDimensions, final Integer methodTarget)
{
boolean sourceValids = (methodSource != null) && (methodSource == sourceDimensions);
boolean targetValids = (methodTarget != null) && (methodTarget == targetDimensions);
if (sourceValids && targetValids) {
return method;
}
sourceValids |= (methodSource == null);
targetValids |= (methodTarget == null);
ensurePositive("sourceDimensions", sourceDimensions);
ensurePositive("targetDimensions", targetDimensions);
if (!sourceValids || !targetValids) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalOperationDimension_3,
method.getName().getCode(), sourceDimensions, targetDimensions));
}
return new DefaultOperationMethod(method, sourceDimensions, targetDimensions);
}
/**
* Returns an operation method with different dimensions, if we are allowed to change dimensionality.
* The need to change an {@code OperationMethod} dimensionality may occur in two contexts:
*
* <ul class="verbose">
* <li>When the original method can work with any number of dimensions. Those methods do not know
* in advance the number of dimensions, which is fixed only after the actual {@link MathTransform}
* instance has been created.
* Example: <cite>Affine</cite> conversion.</li>
* <li>When a three-dimensional method can also be used in the two-dimensional case, typically by
* assuming that the ellipsoidal height is zero everywhere.
* Example: <cite>Molodensky</cite> transform.</li>
* </ul>
*
* This {@code redimension(…)} implementation performs the following choice:
*
* <ul class="verbose">
* <li>If the given method is an instance of {@code DefaultOperationMethod}, then delegate to
* {@link #redimension(int, int)} in order to allow subclasses to defines their own policy.
* For example the <cite>Molodensky</cite> method needs to override.</li>
* <li>Otherwise for each dimension (<var>source</var> and <var>target</var>):
* <ul>
* <li>If the corresponding dimension of the given method is {@code null}, then
* set that dimension to the given value in a new {@code OperationMethod}.</li>
* <li>Otherwise if the given value is not equal to the corresponding dimension
* in the given method, throw an {@link IllegalArgumentException}.</li>
* </ul>
* </li>
* </ul>
*
* @param method the operation method to redimension, or {@code null}.
* @param sourceDimensions the desired number of input dimensions.
* @param targetDimensions the desired number of output dimensions.
* @return the redimensioned operation method, or {@code null} if the given method was null,
* or {@code method} if no change is needed.
* @throws IllegalArgumentException if the given dimensions are illegal for the given operation method.
*/
public static OperationMethod redimension(OperationMethod method,
final int sourceDimensions, final int targetDimensions)
{
if (method != null) {
if (method instanceof DefaultOperationMethod) {
return ((DefaultOperationMethod) method).redimension(sourceDimensions, targetDimensions);
} else {
method = redimension(method, sourceDimensions, method.getSourceDimensions(),
targetDimensions, method.getTargetDimensions());
}
}
return method;
}
/**
* Returns this operation method with different dimensions, if we are allowed to change dimensionality.
* See {@link #redimension(OperationMethod, int, int)} for more information.
*
* <p>The default implementation performs the following choice:
* for each dimension (<var>source</var> and <var>target</var>):</p>
* <ul>
* <li>If the corresponding dimension of the given method is {@code null}, then
* set that dimension to the given value in a new {@code OperationMethod}.</li>
* <li>Otherwise if the given value is not equal to the corresponding dimension
* in the given method, throw an {@link IllegalArgumentException}.</li>
* </ul>
*
* Subclasses should override this method if they can work with different number of dimensions.
* For example a <cite>Molodensky</cite> transform usually works in a three-dimensional space,
* but can also work in a two-dimensional space by assuming that the ellipsoidal height is zero
* everywhere.
*
* @param sourceDimensions the desired number of input dimensions.
* @param targetDimensions the desired number of output dimensions.
* @return the redimensioned operation method, or {@code this} if no change is needed.
* @throws IllegalArgumentException if the given dimensions are illegal for this operation method.
*
* @since 0.6
*/
public OperationMethod redimension(final int sourceDimensions, final int targetDimensions) {
return redimension(this, sourceDimensions, this.sourceDimensions,
targetDimensions, this.targetDimensions);
}
/**
* Returns the GeoAPI interface implemented by this class.
* The SIS implementation returns {@code OperationMethod.class}.
*
* <div class="note"><b>Note for implementers:</b>
* Subclasses usually do not need to override this information since GeoAPI does not define {@code OperationMethod}
* sub-interface. Overriding possibility is left mostly for implementers who wish to extend GeoAPI with their
* own set of interfaces.</div>
*
* @return {@code OperationMethod.class} or a user-defined sub-interface.
*/
@Override
public Class<? extends OperationMethod> getInterface() {
return OperationMethod.class;
}
/**
* Returns the base interface of the {@code CoordinateOperation} instances that use this method.
* The base {@code CoordinateOperation} interface is usually one of the following subtypes:
*
* <ul class="verbose">
* <li>{@link org.opengis.referencing.operation.Transformation}
* if the coordinate operation has some errors (typically of a few metres) because of the empirical process by
* which the operation parameters were determined. Those errors do not depend on the floating point precision
* or the accuracy of the implementation algorithm.</li>
* <li class="verbose">{@link org.opengis.referencing.operation.Conversion}
* if the coordinate operation is theoretically of infinite precision, ignoring the limitations of floating
* point arithmetic (including rounding errors) and the approximations implied by finite series expansions.</li>
* <li>{@link org.opengis.referencing.operation.Projection}
* if the coordinate operation is a conversion (as defined above) converting geodetic latitudes and longitudes
* to plane (map) coordinates. This type can optionally be refined with one of the
* {@link org.opengis.referencing.operation.CylindricalProjection},
* {@link org.opengis.referencing.operation.ConicProjection} or
* {@link org.opengis.referencing.operation.PlanarProjection} subtypes.</li>
* </ul>
*
* In case of doubt, {@code getOperationType()} can conservatively return the base type.
* The default implementation returns {@code SingleOperation.class},
* which is the most conservative return value.
*
* @return interface implemented by all coordinate operations that use this method.
*
* @see org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#getAvailableMethods(Class)
*/
public Class<? extends SingleOperation> getOperationType() {
return SingleOperation.class;
}
/**
* 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.
*
* <div class="note"><b>Departure from the ISO 19111 standard:</b>
* this property is mandatory according ISO 19111, but optional in Apache SIS.</div>
*
* @return the formula used by this method, or {@code null} if unknown.
*
* @see DefaultFormula
* @see org.apache.sis.referencing.operation.transform.MathTransformProvider
*/
@Override
public Formula getFormula() {
return formula;
}
/**
* Number of dimensions in the source CRS of this operation method.
* May be null if unknown, as in an <cite>Affine Transform</cite>.
*
* @return the dimension of source CRS, or {@code null} if unknown.
*
* @see org.apache.sis.referencing.operation.transform.AbstractMathTransform#getSourceDimensions()
*/
@Override
@XmlElement(name = "sourceDimensions")
@XmlSchemaType(name = "positiveInteger")
public Integer getSourceDimensions() {
return sourceDimensions;
}
/**
* Number of dimensions in the target CRS of this operation method.
* May be null if unknown, as in an <cite>Affine Transform</cite>.
*
* @return the dimension of target CRS, or {@code null} if unknown.
*
* @see org.apache.sis.referencing.operation.transform.AbstractMathTransform#getTargetDimensions()
*/
@Override
@XmlElement(name = "targetDimensions")
@XmlSchemaType(name = "positiveInteger")
public Integer getTargetDimensions() {
return targetDimensions;
}
/**
* Returns the set of parameters.
*
* <div class="note"><b>Departure from the ISO 19111 standard:</b>
* this property is mandatory according ISO 19111, but may be null in Apache SIS if the
* {@link #DefaultOperationMethod(MathTransform)} constructor has been unable to infer it
* or if this {@code OperationMethod} has been read from an incomplete GML document.</div>
*
* @return the parameters, or {@code null} if unknown.
*
* @see DefaultConversion#getParameterDescriptors()
* @see DefaultConversion#getParameterValues()
*/
@Override
public ParameterDescriptorGroup getParameters() {
return parameters;
}
/**
* Compares this operation method with the specified object for equality.
* If the {@code mode} argument value is {@link ComparisonMode#STRICT STRICT} or
* {@link ComparisonMode#BY_CONTRACT BY_CONTRACT}, then all available properties
* are compared including the {@linkplain #getFormula() formula}.
*
* @param object the object to compare to {@code this}.
* @param mode {@link ComparisonMode#STRICT STRICT} for performing a strict comparison, or
* {@link ComparisonMode#IGNORE_METADATA IGNORE_METADATA} for comparing only
* properties relevant to transformations.
* @return {@code true} if both objects are equal.
*/
@Override
public boolean equals(final Object object, final ComparisonMode mode) {
if (object == this) {
return true; // Slight optimization.
}
if (super.equals(object, mode)) {
switch (mode) {
case STRICT: {
// Name and identifiers have been compared by super.equals(object, mode).
final DefaultOperationMethod that = (DefaultOperationMethod) object;
return Objects.equals(this.formula, that.formula) &&
Objects.equals(this.sourceDimensions, that.sourceDimensions) &&
Objects.equals(this.targetDimensions, that.targetDimensions) &&
Objects.equals(this.parameters, that.parameters);
}
case BY_CONTRACT: {
// Name and identifiers have been compared by super.equals(object, mode).
if (!Objects.equals(getFormula(), ((OperationMethod) object).getFormula())) {
return false;
}
break;
}
default: {
/*
* Name and identifiers have been ignored by super.equals(object, mode).
* Since they are significant for OperationMethod, we compare them here.
*
* According ISO 19162 (Well Known Text representation of Coordinate Reference Systems),
* identifiers shall have precedence over name at least in the case of operation methods
* and parameters.
*/
final OperationMethod that = (OperationMethod) object;
final Boolean match = Identifiers.hasCommonIdentifier(getIdentifiers(), that.getIdentifiers());
if (match != null) {
if (!match) {
return false;
}
} else if (!isHeuristicMatchForName(that.getName().getCode())
&& !IdentifiedObjects.isHeuristicMatchForName(that, getName().getCode()))
{
return false;
}
break;
}
}
final OperationMethod that = (OperationMethod) object;
return Objects.equals(getSourceDimensions(), that.getSourceDimensions()) &&
Objects.equals(getTargetDimensions(), that.getTargetDimensions()) &&
Utilities.deepEquals(getParameters(), that.getParameters(), mode);
}
return false;
}
/**
* Invoked by {@code hashCode()} for computing the hash code when first needed.
* See {@link org.apache.sis.referencing.AbstractIdentifiedObject#computeHashCode()}
* for more information.
*
* @return the hash code value. This value may change in any future Apache SIS version.
*/
@Override
protected long computeHashCode() {
return super.computeHashCode() + Objects.hash(sourceDimensions, targetDimensions, parameters);
}
/**
* Formats this operation as a <cite>Well Known Text</cite> {@code Method[…]} element.
*
* @return {@code "Method"} (WKT 2) or {@code "Projection"} (WKT 1).
*
* @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#118">WKT 2 specification §17.2.3</a>
*/
@Override
protected String formatTo(final Formatter formatter) {
final boolean isWKT1 = formatter.getConvention().majorVersion() == 1;
/*
* The next few lines below are basically a copy of the work done by super.formatTo(formatter),
* which search for the name to write inside METHOD["name"]. The difference is in the fallback
* executed if we do not find a name for the given authority.
*/
final Citation authority = formatter.getNameAuthority();
String name = IdentifiedObjects.getName(this, authority);
ElementKind kind = ElementKind.METHOD;
if (name == null) {
/*
* No name found for the given authority. We may use the primary name as a fallback.
* But before doing that, maybe we can find the name that we are looking for in the
* hard-coded values in the 'org.apache.sis.internal.referencing.provider' package.
* The typical use case is when this DefaultOperationMethod has been instantiated
* by the EPSG factory using only the information found in the EPSG database.
*
* We can find the hard-coded names by looking at the ParameterDescriptorGroup of the
* enclosing ProjectedCRS or DerivedCRS. This is because that parameter descriptor was
* typically provided by the 'org.apache.sis.internal.referencing.provider' package in
* order to create the MathTransform associated with the enclosing CRS. The enclosing
* CRS is either the immediate parent in WKT 1, or the parent of the parent in WKT 2.
*/
final FormattableObject parent = formatter.getEnclosingElement(isWKT1 ? 1 : 2);
if (parent instanceof GeneralDerivedCRS) {
final Conversion conversion = ((GeneralDerivedCRS) parent).getConversionFromBase();
if (conversion != null) { // Should never be null, but let be safe.
final ParameterDescriptorGroup descriptor;
if (conversion instanceof Parameterized) { // Usual case in SIS implementation.
descriptor = ((Parameterized) conversion).getParameterDescriptors();
} else {
descriptor = conversion.getParameterValues().getDescriptor();
}
name = IdentifiedObjects.getName(descriptor, authority);
}
}
if (name == null) {
name = IdentifiedObjects.getName(this, null);
if (name == null) {
name = Vocabulary.getResources(formatter.getLocale()).getString(Vocabulary.Keys.Unnamed);
kind = ElementKind.NAME; // Because the "Unnamed" string is not a real OperationMethod name.
}
}
}
formatter.append(name, kind);
if (isWKT1) {
/*
* The WKT 1 keyword is "PROJECTION", which imply that the operation method should be of type
* org.opengis.referencing.operation.Projection. So strictly speaking only the first check in
* the following 'if' statement is relevant.
*
* Unfortunately in many cases we do not know the operation type, because the method that we
* invoked - getOperationType() - is not a standard OGC/ISO property, so this information is
* usually not provided in XML documents for example. The user could also have instantiated
* DirectOperationMethod directly without creating a subclass. Consequently we also accept to
* format the keyword as "PROJECTION" if the operation type *could* be a projection. This is
* the second check in the following 'if' statement.
*
* In other words, the combination of those two checks exclude the following operation types:
* Transformation, ConcatenatedOperation, PassThroughOperation, or any user-defined type that
* do not extend Projection. All other operation types are accepted.
*/
final Class<? extends SingleOperation> type = getOperationType();
if (Projection.class.isAssignableFrom(type) || type.isAssignableFrom(Projection.class)) {
return WKTKeywords.Projection;
}
formatter.setInvalidWKT(this, null);
}
return WKTKeywords.Method;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// XML support with JAXB ////////
//////// ////////
//////// The following methods are invoked by JAXB using reflection (even if ////////
//////// they are private) or are helpers for other methods invoked by JAXB. ////////
//////// Those methods can be safely removed if Geographic Markup Language ////////
//////// (GML) support is not needed. ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Creates a new object in which every attributes are set to a null value.
* <strong>This is not a valid object.</strong> This constructor is strictly
* reserved to JAXB, which will assign values to the fields using reflexion.
*/
private DefaultOperationMethod() {
super(org.apache.sis.internal.referencing.NilReferencingObject.INSTANCE);
}
/**
* Invoked by JAXB at unmarshalling time.
*
* @see #getSourceDimensions()
*/
private void setSourceDimensions(final Integer value) {
if (sourceDimensions == null) {
sourceDimensions = value;
} else {
MetadataUtilities.propertyAlreadySet(DefaultOperationMethod.class, "setSourceDimensions", "sourceDimensions");
}
}
/**
* Invoked by JAXB at unmarshalling time.
*
* @see #getTargetDimensions()
*/
private void setTargetDimensions(final Integer value) {
if (targetDimensions == null) {
targetDimensions = value;
} else {
MetadataUtilities.propertyAlreadySet(DefaultOperationMethod.class, "setTargetDimensions", "targetDimensions");
}
}
/**
* Invoked by JAXB for marshalling a citation to the formula. In principle at most one of
* {@code getFormulaCitation()} and {@link #getFormulaDescription()} methods can return a
* non-null value. However SIS accepts both coexist (but this is invalid GML).
*/
@XmlElement(name = "formulaCitation")
private Citation getFormulaCitation() {
final Formula formula = getFormula(); // Give to users a chance to override.
return (formula != null) ? formula.getCitation() : null;
}
/**
* Invoked by JAXB for marshalling the formula literally. In principle at most one of
* {@code getFormulaDescription()} and {@link #getFormulaCitation()} methods can return
* a non-null value. However SIS accepts both to coexist (but this is invalid GML).
*/
@XmlElement(name = "formula")
private String getFormulaDescription() {
final Formula formula = getFormula(); // Give to users a chance to override.
return (formula != null) ? StringAdapter.toString(formula.getFormula()) : null;
}
/**
* Invoked by JAXB for setting the citation to the formula.
*/
private void setFormulaCitation(final Citation citation) {
if (formula == null || formula.getCitation() == null) {
formula = (formula == null) ? new DefaultFormula(citation)
: new DefaultFormula(formula.getFormula(), citation);
} else {
MetadataUtilities.propertyAlreadySet(DefaultOperationMethod.class, "setFormulaCitation", "formulaCitation");
}
}
/**
* Invoked by JAXB for setting the formula description.
*/
private void setFormulaDescription(final String description) {
if (formula == null || formula.getFormula() == null) {
formula = (formula == null) ? new DefaultFormula(description)
: new DefaultFormula(new SimpleInternationalString(description), formula.getCitation());
} else {
MetadataUtilities.propertyAlreadySet(DefaultOperationMethod.class, "setFormulaDescription", "formula");
}
}
/**
* Invoked by JAXB for getting the parameters to marshal. This method usually marshals the sequence of
* descriptors without their {@link ParameterDescriptorGroup} wrapper, because GML is defined that way.
* The {@code ParameterDescriptorGroup} wrapper is a GeoAPI addition done for allowing usage of its
* methods as a convenience (e.g. {@link ParameterDescriptorGroup#descriptor(String)}).
*
* <p>However it could happen that the user really wanted to specify a {@code ParameterDescriptorGroup} as
* the sole {@code <gml:parameter>} element. We currently have no easy way to distinguish those cases.</p>
*
* <div class="note"><b>Tip:</b>
* One possible way to distinguish the two cases would be to check that the parameter group does not contain
* any property that this method does not have:
*
* {@preformat java
* if (IdentifiedObjects.getProperties(this).entrySet().containsAll(
* IdentifiedObjects.getProperties(parameters).entrySet())) ...
* }
*
* But we would need to make sure that {@link AbstractSingleOperation#getParameters()} is consistent
* with the decision taken by this method.</div>
*
* <p><b>Historical note:</b> older, deprecated, names for the parameters were:
* <ul>
* <li>{@code includesParameter}</li>
* <li>{@code generalOperationParameter} - note that this name was used by the EPSG registry</li>
* <li>{@code usesParameter}</li>
* </ul>
*
* @see #getParameters()
* @see AbstractSingleOperation#getParameters()
*/
@XmlElement(name = "parameter")
private GeneralParameterDescriptor[] getDescriptors() {
if (parameters != null) {
final List<GeneralParameterDescriptor> descriptors = parameters.descriptors();
if (descriptors != null) { // Paranoiac check (should not be allowed).
return CC_OperationMethod.filterImplicit(descriptors.toArray(
new GeneralParameterDescriptor[descriptors.size()]));
}
}
return null;
}
/**
* Invoked by JAXB for setting the unmarshalled parameters.
* This method wraps the given descriptors in a {@link DefaultParameterDescriptorGroup}.
*
* <p>The parameter descriptors created by this method are incomplete since we can not
* provide a non-null value for {@link ParameterDescriptor#getValueClass()}. The value
* class will be provided either by replacing this {@code OperationMethod} by one of the
* pre-defined methods, or by unmarshalling the enclosing {@link AbstractSingleOperation}.</p>
*
* <p><b>Maintenance note:</b> the {@code "setDescriptors"} method name is also hard-coded in
* {@link org.apache.sis.internal.jaxb.referencing.CC_GeneralOperationParameter} for logging purpose.</p>
*
* @see AbstractSingleOperation#setParameters
*/
private void setDescriptors(final GeneralParameterDescriptor[] descriptors) {
if (parameters == null) {
parameters = CC_OperationMethod.group(super.getName(), descriptors);
} else {
MetadataUtilities.propertyAlreadySet(DefaultOperationMethod.class, "setDescriptors", "parameter");
}
}
/**
* Invoked by {@link AbstractSingleOperation} for completing the parameter descriptor.
*/
final void updateDescriptors(final GeneralParameterDescriptor[] descriptors) {
final ParameterDescriptorGroup previous = parameters;
parameters = new DefaultParameterDescriptorGroup(IdentifiedObjects.getProperties(previous),
previous.getMinimumOccurs(), previous.getMaximumOccurs(), descriptors);
}
}