blob: 15f95b478cea8de46073da45c838667a0f70446e [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.cs;
import java.util.Map;
import java.util.EnumMap;
import java.util.Arrays;
import javax.measure.Unit;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
import org.opengis.util.GenericName;
import org.opengis.metadata.Identifier;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.cs.CSAuthorityFactory;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.geometry.MismatchedDimensionException;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.CRS;
import org.apache.sis.internal.referencing.WKTUtilities;
import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.io.wkt.ElementKind;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.*;
/**
* The set of {@linkplain DefaultCoordinateSystemAxis coordinate system axes} that spans a given coordinate space.
* The type of the coordinate system implies the set of mathematical rules for calculating geometric properties
* like angles, distances and surfaces.
*
* <p>This class is conceptually <cite>abstract</cite>, even if it is technically possible to instantiate it.
* Typical applications should create instances of the most specific subclass with {@code Default} prefix instead.
* An exception to this rule may occurs when it is not possible to identify the exact type. For example it is not
* possible to infer the exact coordinate system from <cite>Well Known Text</cite> (WKT) version 1 in some cases
* (e.g. in a {@code LOCAL_CS} element). In such exceptional situation, a plain {@code AbstractCS} object may be
* instantiated.</p>
*
* <h2>Immutability and thread safety</h2>
* This base class is immutable and thus thread-safe if the property <em>values</em> (not necessarily the map itself)
* and the {@link CoordinateSystemAxis} instances given to the constructor are also immutable. Most SIS subclasses and
* related classes are immutable under similar conditions. This means that unless otherwise noted in the javadoc,
* {@code CoordinateSystem} instances created using only SIS factories and static constants can be shared by many
* objects and passed between threads without synchronization.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
*
* @see DefaultCoordinateSystemAxis
* @see org.apache.sis.referencing.crs.AbstractCRS
*
* @since 0.4
* @module
*/
@XmlType(name = "AbstractCoordinateSystemType")
@XmlRootElement(name = "AbstractCoordinateSystem")
@XmlSeeAlso({
DefaultAffineCS.class,
DefaultCartesianCS.class, // Not an AffineCS subclass in GML schema.
DefaultSphericalCS.class,
DefaultEllipsoidalCS.class,
DefaultCylindricalCS.class,
DefaultPolarCS.class,
DefaultLinearCS.class,
DefaultVerticalCS.class,
DefaultTimeCS.class,
DefaultParametricCS.class,
DefaultUserDefinedCS.class
})
public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSystem {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = 6757665252533744744L;
/**
* Return value for {@link #validateAxis(AxisDirection, Unit)}
*/
static final int VALID = 0, INVALID_DIRECTION = 1, INVALID_UNIT = 2;
/**
* The sequence of axes for this coordinate system.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setAxis(CoordinateSystemAxis[])}</p>
*
* @see #getAxis(int)
*/
private CoordinateSystemAxis[] axes;
/**
* Other coordinate systems derived from this coordinate systems for other axes conventions.
* Created only when first needed.
*
* @see #forConvention(AxesConvention)
*/
private transient Map<AxesConvention,AbstractCS> derived;
/**
* Constructs a coordinate system from a set of properties and a sequence of axes.
* The properties map is given unchanged to the
* {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}.
* The following table is a reminder of main (not all) properties:
*
* <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.IdentifiedObject#NAME_KEY}</td>
* <td>{@link Identifier} or {@link String}</td>
* <td>{@link #getName()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
* <td>{@link 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 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>
*
* @param properties the properties to be given to the identified object.
* @param axes the sequence of axes.
*/
@SuppressWarnings("OverridableMethodCallDuringObjectConstruction")
public AbstractCS(final Map<String,?> properties, CoordinateSystemAxis... axes) {
super(properties);
ensureNonNull("axes", axes);
this.axes = axes = axes.clone();
for (int i=0; i<axes.length; i++) {
final CoordinateSystemAxis axis = axes[i];
ensureNonNullElement("axes", i, axis);
final Identifier name = axis.getName();
ensureNonNullElement("axes[#].name", i, name);
final AxisDirection direction = axis.getDirection();
ensureNonNullElement("axes[#].direction", i, direction);
final Unit<?> unit = axis.getUnit();
ensureNonNullElement("axes[#].unit", i, unit);
/*
* Ensures that axis direction and units are compatible with the
* coordinate system to be created. For example CartesianCS will
* accept only linear or dimensionless units.
*/
switch (validateAxis(direction, unit)) {
case INVALID_DIRECTION: {
throw new IllegalArgumentException(Resources.forProperties(properties).getString(
Resources.Keys.IllegalAxisDirection_2, getClass(), direction));
}
case INVALID_UNIT: {
throw new IllegalArgumentException(Resources.forProperties(properties).getString(
Resources.Keys.IllegalUnitFor_2, name, unit));
}
}
/*
* Ensures there is no axis along the same direction (e.g. two North axes, or an East and a West axis).
* An exception to this rule is the time axis, since ISO 19107 explicitly allows compound CRS to have
* more than one time axis. Such case happen in meteorological models.
*/
final AxisDirection dir = AxisDirections.absolute(direction);
if (!dir.equals(AxisDirection.OTHER)) {
for (int j=i; --j>=0;) {
final AxisDirection other = axes[j].getDirection();
final AxisDirection abs = AxisDirections.absolute(other);
if (dir.equals(abs) && !abs.equals(AxisDirection.FUTURE)) {
throw new IllegalArgumentException(Resources.forProperties(properties).getString(
Resources.Keys.ColinearAxisDirections_2, direction, other));
}
}
}
}
}
/**
* Creates a new coordinate 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 cs the coordinate system to copy.
*
* @see #castOrCopy(CoordinateSystem)
*/
protected AbstractCS(final CoordinateSystem cs) {
super(cs);
axes = (cs instanceof AbstractCS) ? ((AbstractCS) cs).axes : getAxes(cs);
}
/**
* Returns the axes of the given coordinate system.
*/
private static CoordinateSystemAxis[] getAxes(final CoordinateSystem cs) {
final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[cs.getDimension()];
for (int i=0; i<axes.length; i++) {
axes[i] = cs.getAxis(i);
}
return axes;
}
/**
* Returns a SIS coordinate system implementation with the values of the given arbitrary implementation.
* This method performs the first applicable action in the following choices:
*
* <ul>
* <li>If the given object is {@code null}, then this method returns {@code null}.</li>
* <li>Otherwise if the given object is an instance of
* {@link org.opengis.referencing.cs.AffineCS},
* {@link org.opengis.referencing.cs.CartesianCS},
* {@link org.opengis.referencing.cs.SphericalCS},
* {@link org.opengis.referencing.cs.EllipsoidalCS},
* {@link org.opengis.referencing.cs.CylindricalCS},
* {@link org.opengis.referencing.cs.PolarCS},
* {@link org.opengis.referencing.cs.LinearCS},
* {@link org.opengis.referencing.cs.VerticalCS},
* {@link org.opengis.referencing.cs.TimeCS} or
* {@link org.opengis.referencing.cs.UserDefinedCS},
* then this method delegates to the {@code castOrCopy(…)} method of the corresponding SIS subclass.
* Note that if the given object implements more than one of the above-cited interfaces,
* then the {@code castOrCopy(…)} method to be used is unspecified.</li>
* <li>Otherwise if the given object is already an instance of
* {@code AbstractCS}, then it is returned unchanged.</li>
* <li>Otherwise a new {@code AbstractCS} instance is created using the
* {@linkplain #AbstractCS(CoordinateSystem) copy constructor}
* and returned. Note that this is a <cite>shallow</cite> copy operation, since the other
* properties contained in the given object are not recursively copied.</li>
* </ul>
*
* @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 AbstractCS castOrCopy(final CoordinateSystem object) {
return SubTypes.castOrCopy(object);
}
/**
* Returns {@link #VALID} if the given argument values are allowed for an axis in this coordinate system,
* or an {@code INVALID_*} error code otherwise. This method is invoked at construction time for checking
* argument validity. The default implementation returns {@code VALID} in all cases. Subclasses override
* this method in order to put more restrictions on allowed axis directions and check for compatibility
* with {@linkplain org.apache.sis.measure.Units#METRE metre} or
* {@linkplain org.apache.sis.measure.Units#DEGREE degree} units.
*
* <p><b>Note for implementers:</b> since this method is invoked at construction time, it shall not depend
* on this object's state. This method is not in public API for that reason.</p>
*
* @param direction the direction to test for compatibility (never {@code null}).
* @param unit the unit to test for compatibility (never {@code null}).
* @return {@link #VALID} if the given direction and unit are compatible with this coordinate system,
* {@link #INVALID_DIRECTION} if the direction is invalid or {@link #INVALID_UNIT} if the unit
* is invalid.
*/
int validateAxis(final AxisDirection direction, final Unit<?> unit) {
return VALID;
}
/**
* Returns the GeoAPI interface implemented by this class.
* The default implementation returns {@code CoordinateSystem.class}.
* Subclasses implementing a more specific GeoAPI interface shall override this method.
*
* @return the coordinate system interface implemented by this class.
*/
@Override
public Class<? extends CoordinateSystem> getInterface() {
return CoordinateSystem.class;
}
/**
* Returns the number of dimensions of this coordinate system.
* This is the number of axes given at construction time.
*
* @return the number of dimensions of this coordinate system.
*/
@Override
public final int getDimension() {
return axes.length;
}
/**
* Returns the axis for this coordinate system at the specified dimension.
*
* @param dimension the zero based index of axis.
* @return the axis at the specified dimension.
* @throws IndexOutOfBoundsException if {@code dimension} is out of bounds.
*/
@Override
public final CoordinateSystemAxis getAxis(final int dimension) throws IndexOutOfBoundsException {
return axes[dimension];
}
/**
* Returns a coordinate system equivalent to this one but with axes rearranged according the given convention.
* If this coordinate system is already compatible with the given convention, then this method returns {@code this}.
*
* @param convention the axes convention for which a coordinate system is desired.
* @return a coordinate system compatible with the given convention (may be {@code this}).
*
* @see org.apache.sis.referencing.crs.AbstractCRS#forConvention(AxesConvention)
*/
public synchronized AbstractCS forConvention(final AxesConvention convention) {
ensureNonNull("convention", convention);
if (derived == null) {
derived = new EnumMap<>(AxesConvention.class);
}
AbstractCS cs = derived.get(convention);
if (cs == null) {
cs = Normalizer.forConvention(this, convention);
if (cs == null) {
cs = this; // This coordinate system is already normalized.
} else if (convention != AxesConvention.POSITIVE_RANGE) {
cs = cs.resolveEPSG(this);
}
/*
* It happen often that the CRS created by RIGHT_HANDED, DISPLAY_ORIENTED and
* NORMALIZED are the same. If this is the case, sharing the same instance
* not only save memory but can also make future comparisons faster.
*/
for (final AbstractCS existing : derived.values()) {
if (cs.equals(existing)) {
cs = existing;
break;
}
}
derived.put(convention, cs);
}
return cs;
}
/**
* Returns a coordinate system usually of the same type than this CS but with different axes.
* This method shall be overridden by all {@code AbstractCS} subclasses in this package.
*
* <p>This method returns a coordinate system of the same type if the number of axes is unchanged.
* But if the given {@code axes} array has less elements than this coordinate system dimension, then
* this method may return an other kind of coordinate system. See {@link AxisFilter} for an example.</p>
*
* @param axes the set of axes to give to the new coordinate system.
* @return a new coordinate system of the same type than {@code this}, but using the given axes.
* @throws IllegalArgumentException if {@code axes} contains an unexpected number of axes,
* or if an axis has an unexpected direction or unexpected unit of measurement.
*/
AbstractCS createForAxes(final Map<String,?> properties, final CoordinateSystemAxis[] axes) {
return new AbstractCS(properties, axes);
}
/**
* Verify if we can get a coordinate system from the EPSG database with the same axes.
* Such CS gives more information (better name and remarks). This is a "would be nice"
* feature; if we fail, we keep the CS built by {@link Normalizer}.
*
* @param original the coordinate system from which this CS is derived.
* @return the resolved CS, or {@code this} if none.
*/
private AbstractCS resolveEPSG(final AbstractCS original) {
if (IdentifiedObjects.getIdentifier(original, Citations.EPSG) != null) {
final Integer epsg = CoordinateSystems.getEpsgCode(getInterface(), axes);
if (epsg != null) try {
final AuthorityFactory factory = CRS.getAuthorityFactory(Constants.EPSG);
if (factory instanceof CSAuthorityFactory) {
final CoordinateSystem fromDB = ((CSAuthorityFactory) factory).createCoordinateSystem(epsg.toString());
if (fromDB instanceof AbstractCS) {
/*
* We should compare axes strictly using Arrays.equals(…). However axes in different order
* get different codes in EPSG database, which may them not strictly equal. We would need
* another comparison mode ignoring only the authority code. We don't add this complexity
* for now, and rather rely on the check for EPSG code done by the caller. If the original
* CS was an EPSG object, then we assume that we still want an EPSG object here.
*/
if (Utilities.equalsIgnoreMetadata(axes, ((AbstractCS) fromDB).axes)) {
return (AbstractCS) fromDB;
}
}
}
} catch (FactoryException e) {
/*
* NoSuchAuthorityCodeException may happen if factory is EPSGFactoryFallback.
* Other exceptions would probably be more serious errors, but it still non-fatal
* for this method since we can continue with what Normalizer created.
*/
Logging.recoverableException(Logging.getLogger(Modules.REFERENCING), getClass(), "forConvention", e);
}
}
return this;
}
/**
* Convenience method for implementations of {@link #createForAxes(Map, CoordinateSystemAxis[])}
* when the resulting coordinate system would have an unexpected number of dimensions.
*
* @param properties the properties which was supposed to be given to the constructor.
* @param axes the axes which was supposed to be given to the constructor.
* @param expected the minimal expected number of dimensions (may be less than {@link #getDimension()}).
*/
static IllegalArgumentException unexpectedDimension(final Map<String,?> properties,
final CoordinateSystemAxis[] axes, final int expected)
{
return new MismatchedDimensionException(Errors.getResources(properties).getString(
Errors.Keys.MismatchedDimension_3, "filter(cs)", expected, axes.length));
}
/**
* Compares the specified object with this coordinate system 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
@SuppressWarnings("fallthrough")
public boolean equals(final Object object, final ComparisonMode mode) {
if (object == this) {
return true; // Slight optimization.
}
if (!super.equals(object, mode)) {
return false;
}
switch (mode) {
case STRICT: {
// No need to check the class - this check has been done by super.equals(…).
return Arrays.equals(axes, ((AbstractCS) object).axes);
}
case DEBUG: {
final int d1, d2;
assert (d1 = axes.length) == (d2 = ((CoordinateSystem) object).getDimension())
: Errors.format(Errors.Keys.MismatchedDimension_2, d1, d2);
// Fall through
}
default: {
final CoordinateSystem that = (CoordinateSystem) object;
final int dimension = getDimension();
if (dimension != that.getDimension()) {
return false;
}
if (mode != ComparisonMode.ALLOW_VARIANT) {
for (int i=0; i<dimension; i++) {
if (!Utilities.deepEquals(getAxis(i), that.getAxis(i), mode)) {
return false;
}
}
}
return true;
}
}
}
/**
* 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() + Arrays.hashCode(axes);
}
/**
* Formats the inner part of the <cite>Well Known Text</cite> (WKT) representation of this coordinate system.
* This method does <strong>not</strong> format the axes, because they shall appear outside
* the {@code CS[…]} element for historical reasons. Axes shall be formatted by the enclosing
* element (usually an {@link org.apache.sis.referencing.crs.AbstractCRS}).
*
* <div class="note"><b>Example:</b> Well-Known Text of a two-dimensional {@code EllipsoidalCS}
* having (φ,λ) axes in a unit defined by the enclosing CRS (usually degrees).
*
* {@preformat wkt
* CS[ellipsoidal, 2],
* Axis["latitude", north],
* Axis["longitude", east]
* }
* </div>
*
* <div class="note"><b>Compatibility note:</b>
* {@code CS} is defined in the WKT 2 specification only.</div>
*
* @return {@code "CS"}.
*
* @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#36">WKT 2 specification §7.5</a>
*/
@Override
protected String formatTo(final Formatter formatter) {
final String type = WKTUtilities.toType(CoordinateSystem.class, getInterface());
if (type == null) {
formatter.setInvalidWKT(this, null);
}
formatter.append(type, ElementKind.CODE_LIST);
formatter.append(getDimension());
return WKTKeywords.CS;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// 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. ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////////////////
/**
* An empty array of axes, used only for JAXB.
*/
private static final CoordinateSystemAxis[] EMPTY = new CoordinateSystemAxis[0];
/**
* Constructs a new object in which every attributes are set to a null or empty 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.
*/
AbstractCS() {
super(org.apache.sis.internal.referencing.NilReferencingObject.INSTANCE);
axes = EMPTY;
/*
* Coordinate system axes are mandatory for SIS working. We do not verify their presence here
* (because the verification would have to be done in an 'afterMarshal(…)' method and throwing
* an exception in that method causes the whole unmarshalling to fail). But the CS_CoordinateSystem
* adapter does some verifications.
*/
}
/**
* Invoked by JAXB at marshalling time.
*/
@XmlElement(name = "axis")
private CoordinateSystemAxis[] getAxis() {
return getAxes(this); // Give a chance to users to override getAxis(int).
}
/**
* Invoked by JAXB at unmarshalling time.
*/
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
private void setAxis(final CoordinateSystemAxis[] values) {
axes = values;
}
}