blob: 5c8fdb508f7844201d341083037656bcc6e017f9 [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.crs;
import java.util.Map;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.ValidationException;
import org.opengis.util.FactoryException;
import org.opengis.referencing.datum.Datum;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.GeneralDerivedCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.geometry.MismatchedDimensionException;
import org.apache.sis.referencing.operation.DefaultConversion;
import org.apache.sis.internal.jaxb.referencing.CC_Conversion;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.apache.sis.internal.metadata.MetadataUtilities;
import org.apache.sis.internal.metadata.Identifiers;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.system.Semaphores;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ComparisonMode;
import static org.apache.sis.util.Utilities.deepEquals;
/**
* A coordinate reference system that is defined by its coordinate
* {@linkplain org.apache.sis.referencing.operation.DefaultConversion conversion} from another CRS
* (not by a {@linkplain org.apache.sis.referencing.datum.AbstractDatum datum}).
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.2
*
* @param <C> the conversion type, either {@code Conversion} or {@code Projection}.
*
* @since 0.6
* @module
*/
@XmlType(name = "AbstractGeneralDerivedCRSType")
@XmlRootElement(name = "AbstractGeneralDerivedCRS")
@XmlSeeAlso({
DefaultDerivedCRS.class,
DefaultProjectedCRS.class
})
abstract class AbstractDerivedCRS<C extends Conversion> extends AbstractCRS implements GeneralDerivedCRS {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -175151161496419854L;
/**
* The conversion from the {@linkplain #getBaseCRS() base CRS} to this CRS.
* The base CRS of this {@code GeneralDerivedCRS} is {@link Conversion#getSourceCRS()}.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setConversionFromBase(Conversion)}</p>
*
* @see #getConversionFromBase()
*/
private C conversionFromBase;
/**
* Creates a derived CRS from a defining conversion.
* The properties given in argument follow the same rules than for the
* {@linkplain AbstractCRS#AbstractCRS(Map, CoordinateSystem) super-class constructor}.
*
* @param properties the properties to be given to the new derived CRS object.
* @param baseCRS coordinate reference system to base the derived CRS on.
* @param conversion the defining conversion from a normalized base to a normalized derived CRS.
* @param derivedCS the coordinate system for the derived CRS. The number of axes must match
* the target dimension of the {@code baseToDerived} transform.
* @throws MismatchedDimensionException if the source and target dimensions of {@code baseToDerived}
* do not match the dimensions of {@code base} and {@code derivedCS} respectively.
*/
AbstractDerivedCRS(final Map<String,?> properties,
final SingleCRS baseCRS,
final Conversion conversion,
final CoordinateSystem derivedCS)
throws MismatchedDimensionException
{
super(properties, derivedCS);
ArgumentChecks.ensureNonNull("baseCRS", baseCRS);
ArgumentChecks.ensureNonNull("conversion", conversion);
ArgumentChecks.ensureDimensionsMatch("baseToDerived",
baseCRS.getCoordinateSystem().getDimension(),
derivedCS.getDimension(),
conversion.getMathTransform());
conversionFromBase = createConversionFromBase(properties, baseCRS, conversion);
}
/**
* For {@link DefaultDerivedCRS#DefaultDerivedCRS(Map, SingleCRS, CoordinateReferenceSystem, OperationMethod,
* MathTransform, CoordinateSystem)} constructor only (<strong>not legal for {@code ProjectedCRS}</strong>).
*/
@SuppressWarnings("unchecked")
AbstractDerivedCRS(final Map<String,?> properties,
final SingleCRS baseCRS,
final CoordinateReferenceSystem interpolationCRS,
final OperationMethod method,
final MathTransform baseToDerived,
final CoordinateSystem derivedCS)
throws MismatchedDimensionException
{
super(properties, derivedCS);
ArgumentChecks.ensureNonNull("baseCRS", baseCRS);
ArgumentChecks.ensureNonNull("method", method);
ArgumentChecks.ensureNonNull("baseToDerived", baseToDerived);
conversionFromBase = (C) new DefaultConversion( // Cast to (C) is valid only for DefaultDerivedCRS.
ConversionKeys.unprefix(properties), baseCRS, this, interpolationCRS, method, baseToDerived);
}
/**
* Constructs a new coordinate reference system 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 crs the coordinate reference system to copy.
*/
AbstractDerivedCRS(final GeneralDerivedCRS crs) {
super(crs);
conversionFromBase = createConversionFromBase(null, crs.getBaseCRS(), crs.getConversionFromBase());
}
/**
* Creates the conversion instance to associate with this {@code AbstractDerivedCRS}.
*
* <p><b>WARNING:</b> this method is invoked at construction time and will invoke indirectly
* (through {@link DefaultConversion}) the {@link #getCoordinateSystem()} method on {@code this}.
* Consequently this method shall be invoked only after the construction of this {@code AbstractDerivedCRS}
* instance is advanced enough for allowing the {@code getCoordinateSystem()} method to execute.
* Subclasses may consider to make the {@code getCoordinateSystem()} method final for better guarantees.</p>
*/
private C createConversionFromBase(final Map<String,?> properties, final SingleCRS baseCRS, final Conversion conversion) {
MathTransformFactory factory = null;
if (properties != null) {
factory = (MathTransformFactory) properties.get(ReferencingFactoryContainer.MT_FACTORY);
}
if (factory == null) {
factory = DefaultFactories.forBuildin(MathTransformFactory.class);
}
try {
return DefaultConversion.castOrCopy(conversion).specialize(getConversionType(), baseCRS, this, factory);
} catch (FactoryException e) {
throw new IllegalArgumentException(Errors.getResources(properties).getString(
Errors.Keys.IllegalArgumentValue_2, "conversion", conversion.getName()), e);
}
}
/**
* Returns the type of conversion associated to this {@code AbstractDerivedCRS}.
*
* <p><b>WARNING:</b> this method is invoked (indirectly) at construction time.
* Consequently it shall return a constant value - this method is not allowed to
* depend on the object state.</p>
*/
abstract Class<C> getConversionType();
/**
* Returns the GeoAPI interface implemented by this class.
*/
@Override
public abstract Class<? extends GeneralDerivedCRS> getInterface();
/**
* Returns the datum of the {@linkplain #getBaseCRS() base CRS}.
*
* @return the datum of the base CRS.
*/
@Override
public abstract Datum getDatum();
/**
* Returns the conversion from the {@linkplain #getBaseCRS() base CRS} to this CRS.
*
* @return the conversion to this CRS.
*/
@Override
@XmlElement(name = "conversion", required = true)
public C getConversionFromBase() {
return conversionFromBase;
}
/**
* Compares this coordinate reference system with the specified object for equality.
*
* @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 coordinate transformations.
* @return {@code true} if both objects are equal.
*/
@Override
public boolean equals(final Object object, final ComparisonMode mode) {
if (object == this) {
return true;
}
if (super.equals(object, mode)) {
final boolean strict = (mode == ComparisonMode.STRICT);
/*
* Avoid never-ending recursivity: Conversion has a `targetCRS` field (inherited from
* the AbstractCoordinateOperation super-class) that is set to this AbstractDerivedCRS.
*
* Do NOT compare the baseCRS explicitly. This is done implicitely in the comparison of the Conversion
* objects, since (this.baseCRS == Conversion.sourceCRS) in Apache SIS. The reason why we delegate the
* comparison of that CRS to the Conversion object is because we want to ignore the baseCRS axes if the
* mode said to ignore metadata, but ignoring axis order and units has implication on the MathTransform
* instances to compare. The AbstractCoordinateOperation.equals(…) method implementation handles those
* cases.
*/
if (Semaphores.queryAndSet(Semaphores.CONVERSION_AND_CRS)) {
return true;
} else try {
return deepEquals(strict ? conversionFromBase : getConversionFromBase(),
strict ? ((AbstractDerivedCRS) object).conversionFromBase
: ((GeneralDerivedCRS) object).getConversionFromBase(), mode);
} finally {
Semaphores.clear(Semaphores.CONVERSION_AND_CRS);
}
}
return false;
}
/**
* {@inheritDoc}
*
* @return {@inheritDoc}
*/
@Override
protected long computeHashCode() {
/*
* Do not invoke `conversionFromBase.hashCode()` in order to avoid a never-ending loop.
* This is because Conversion inherits a `sourceCRS` field from the CoordinateOperation
* parent type, which is set to this DerivedCRS. Checking the OperationMethod does not
* work neither for the reason documented inside the AbstractSingleOperation.equals(…)
* method body. The MathTransform is our best discriminant.
*/
return super.computeHashCode()
+ 31 * conversionFromBase.getSourceCRS().hashCode()
+ 37 * conversionFromBase.getMathTransform().hashCode();
}
//////////////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// 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. ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Constructs 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.
*/
AbstractDerivedCRS() {
}
/**
* Invoked by JAXB at unmarshalling time for setting the <cite>defining conversion</cite>.
* At this state, the given conversion has null {@code sourceCRS} and {@code targetCRS}.
* Those CRS will be set later, in {@link #afterUnmarshal(Unmarshaller, Object)}.
*/
private void setConversionFromBase(final C conversion) {
if (conversionFromBase == null) {
conversionFromBase = conversion;
} else {
MetadataUtilities.propertyAlreadySet(AbstractDerivedCRS.class, "setConversionFromBase", "conversion");
}
}
/**
* Stores a temporary association to the given base CRS. This method shall be invoked only
* <strong>after</strong> the call to {@link #setConversionFromBase(Conversion)}, otherwise
* an exception will be thrown.
*
* <p>The given base CRS will not be stored in this {@code AbstractDerivedCRS} instance now,
* but in another temporary location. The reason is that we need the coordinate system (CS)
* before we can set the {@code baseCRS} in its final location, but the CS is not yet known
* when this method is invoked.</p>
*
* @param name the property name, used only in case of error message to format.
* @throws IllegalStateException if the base CRS can not be set.
*/
@SuppressWarnings("unchecked")
final void setBaseCRS(final String name, final SingleCRS baseCRS) {
if (conversionFromBase != null) {
final SingleCRS previous = CC_Conversion.setBaseCRS(conversionFromBase, baseCRS);
if (previous != null) {
CC_Conversion.setBaseCRS(conversionFromBase, previous); // Temporary location.
MetadataUtilities.propertyAlreadySet(AbstractDerivedCRS.class, "setBaseCRS", name);
}
} else {
throw new IllegalStateException(Errors.format(Errors.Keys.MissingComponentInElement_2, getInterface(), "conversion"));
}
}
/**
* Invoked by JAXB after all elements have been unmarshalled. At this point we should have the
* coordinate system (CS). The CS information is required by {@code createConversionFromBase(…)}
* in order to create a {@link MathTransform} with correct axis swapping and unit conversions.
*/
private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) throws ValidationException {
String property = "conversion";
if (conversionFromBase != null) {
final SingleCRS baseCRS = CC_Conversion.setBaseCRS(conversionFromBase, null); // Clear the temporary value now.
property = "coordinateSystem";
if (super.getCoordinateSystem() != null) {
property = "baseCRS";
if (baseCRS != null) {
conversionFromBase = createConversionFromBase(null, baseCRS, conversionFromBase);
return;
}
}
}
/*
* If we reach this point, we failed to update the conversion. The `baseCRS` information will be lost
* and call to `getConversionFromBase()` will throw a ClassCastException if this instance is actually
* a ProjectedCRS (because of the method overriding with return type covariance).
*/
throw new ValidationException(Identifiers.missingValueForProperty(getName(), property));
}
}