/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.
*/
package org.geotoolkit.referencing;
import java.util.List;
import java.awt.geom.Point2D;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Collections;
import org.opengis.geometry.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;
import org.opengis.metadata.extent.*;
import org.opengis.util.FactoryException;
import org.geotoolkit.lang.Static;
import org.apache.sis.util.Version;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.ComparisonMode;
import org.geotoolkit.factory.FactoryRegistryException;
import org.geotoolkit.internal.io.JNDI;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralEnvelope;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.resources.Errors;
import org.apache.sis.internal.metadata.NameMeaning;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.referencing.crs.DefaultCompoundCRS;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Utility class for making use of the {@linkplain CoordinateReferenceSystem Coordinate Reference
* System} and associated {@linkplain org.opengis.util.Factory} implementations. This utility class
* is made up of static functions working with arbitrary implementations of GeoAPI interfaces.
* <p>
* The methods defined in this class can be grouped in three categories:
* <p>
* <ul>
* <li>Methods working with factories, like {@link #decode(String)}.</li>
* <li>Methods providing informations, like {@link #isHorizontalCRS(CoordinateReferenceSystem)}.</li>
* <li>Methods performing coordinate transformations, like {@link #transform(CoordinateOperation,Envelope)}
* Note that many of those methods are also defined in the {@link Envelopes} class.</li>
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Jody Garnett (Refractions)
* @author Andrea Aime (TOPP)
* @version 3.19
*
* @see IdentifiedObjects
* @see Envelopes
*
* @since 2.1
* @module
*/
public final class CRS extends Static {
static {
JNDI.install();
}
/**
* Do not allow instantiation of this class.
*/
private CRS() {
}
//////////////////////////////////////////////////////////////
//// ////
//// FACTORIES, CRS CREATION AND INSPECTION ////
//// ////
//////////////////////////////////////////////////////////////
/**
* Returns the coordinate operation factory used by
* {@link #findMathTransform(CoordinateReferenceSystem, CoordinateReferenceSystem)
* findMathTransform} convenience methods.
*
* @param lenient ignored.
* @return The coordinate operation factory used for finding math transform in this class.
*
* @category factory
* @since 2.4
*/
@Deprecated
public static CoordinateOperationFactory getCoordinateOperationFactory(final boolean lenient) {
return DefaultFactories.forBuildin(CoordinateOperationFactory.class);
}
/**
* Returns the version number of the specified authority database, or {@code null} if
* not available.
*
* @param authority The authority name (typically {@code "EPSG"}).
* @return The version number of the authority database, or {@code null} if unknown.
* @throws FactoryRegistryException if no {@link CRSAuthorityFactory} implementation
* was found for the specified authority.
*
* @category factory
* @since 2.4
*/
public static Version getVersion(final String authority) throws FactoryRegistryException {
final String version;
try {
version = NameMeaning.getVersion(org.apache.sis.referencing.CRS.getAuthorityFactory(authority).getAuthority());
} catch (FactoryException e) {
throw new FactoryRegistryException(e.getLocalizedMessage(), e);
}
return (version != null) ? new Version(version) : null;
}
/**
* Returns the domain of validity for the specified coordinate reference system,
* or {@code null} if unknown. The returned envelope is expressed in terms of the
* specified CRS.
* <p>
* This method looks in two places:
* <p>
* <ul>
* <li>First, it checks the {@linkplain CoordinateReferenceSystem#getDomainOfValidity domain
* of validity} associated with the given CRS. Only {@linkplain GeographicExtent
* geographic extents} of kind {@linkplain BoundingPolygon bounding polygon} are
* taken in account.</li>
* <li>If the above step does not found found any bounding polygon, then the
* {@linkplain #getGeographicBoundingBox geographic bounding boxes} are
* used as a fallback.</li>
* </ul>
* <p>
* Note that this method is also accessible from the {@link Envelopes} class.
*
* @param crs The coordinate reference system, or {@code null}.
* @return The envelope in terms of the specified CRS, or {@code null} if none.
*
* @see #getGeographicBoundingBox(CoordinateReferenceSystem)
* @see Envelopes#getDomainOfValidity(CoordinateReferenceSystem)
* @see GeneralEnvelope#reduceToDomain(boolean)
*
* @category information
* @since 2.2
*
* @deprecated Moved to Apache SIS as {@link return org.apache.sis.referencing.CRS#getDomainOfValidity}.
*/
@Deprecated
public static Envelope getEnvelope(final CoordinateReferenceSystem crs) {
return org.apache.sis.referencing.CRS.getDomainOfValidity(crs);
}
/**
* Returns the first compound CRS which contains only the given components, in any order.
* First, this method gets the {@link SingleCRS} components of the given compound CRS. If
* all those components are {@linkplain #equalsIgnoreMetadata equal, ignoring metadata}
* and order, to the {@code SingleCRS} components given to this method, then the given
* {@code CompoundCRS} is returned. Otherwise if the given {@code CompoundCRS} contains
* nested {@code CompoundCRS}, then those nested CRS are inspected recursively by the same
* algorithm. Otherwise, this method returns {@code null}.
* <p>
* This method is useful for extracting metadata about the 3D spatial CRS part in a 4D
* spatio-temporal CRS. For example given the following CRS:
*
* {@preformat wkt
* COMPD_CS["Mercator + height + time",
* COMPD_CS["Mercator + height",
* PROJCS["Mercator", ...etc...]
* VERT_CS["Ellipsoidal height", ...etc...]]
* TemporalCRS["Modified Julian", ...etc...]]
* }
*
* Then the following code will returns the nested {@code COMPD_CS["Mercator + height"]}
* without prior knowledge of the CRS component order (the time CRS could be first, and
* the vertical CRS could be before the horizontal one):
*
* {@preformat java
* CompoundCRS crs = ...;
* SingleCRS horizontalCRS = getHorizontalCRS(crs);
* VerticalCRS verticalCRS = getVerticalCRS(crs);
* if (horizontalCRS != null && verticalCRS != null) {
* CompoundCRS spatialCRS = getCompoundCRS(crs, horizontalCRS, verticalCRS);
* if (spatialCRS != null) {
* // ...
* }
* }
* }
*
* @param crs The compound CRS to compare with the given component CRS, or {@code null}.
* @param components The CRS which must be components of the returned CRS.
* @return A CRS which contains the given components, or {@code null} if none.
*
* @see DefaultCompoundCRS#getSingleCRS()
*
* @since 3.16
*/
public static CompoundCRS getCompoundCRS(final CompoundCRS crs, final SingleCRS... components) {
final List<SingleCRS> actualComponents = org.apache.sis.referencing.CRS.getSingleComponents(crs);
if (actualComponents.size() == components.length) {
int firstValid = 0;
final SingleCRS[] toSearch = components.clone();
compare: for (final SingleCRS component : actualComponents) {
for (int i=firstValid; i<toSearch.length; i++) {
if (Utilities.equalsIgnoreMetadata(component, toSearch[i])) {
/*
* Found a match: remove it from the search list. Note that we copy the
* remaining components to the end of the array (which is unusual) rather
* than to the begining (as usual), in order to reduce the length of the
* part to copy on the assumption that the components given to this method
* are most likely in the same order than the elements in the CompoundCRS.
*/
System.arraycopy(toSearch, firstValid, toSearch, firstValid+1, i - firstValid);
toSearch[firstValid++] = null;
continue compare;
}
}
// No match found. We can stop the loop now.
firstValid = -1;
break;
}
/*
* If we found all the requested components and nothing more,
* returns the CRS.
*/
if (firstValid == toSearch.length) {
return crs;
}
}
/*
* Search recursively in the sub-components.
*/
for (final CoordinateReferenceSystem component : crs.getComponents()) {
if (component instanceof CompoundCRS) {
final CompoundCRS candidate = getCompoundCRS((CompoundCRS) component, components);
if (candidate != null) {
return candidate;
}
}
}
return null;
}
/**
* @todo Duplicate of {@link org.geotoolkit.referencing.factory.ReferencingFactoryContainer}?
*
* @see <a href="https://issues.apache.org/jira/browse/SIS-162">https://issues.apache.org/jira/browse/SIS-162</a>
*/
public static CoordinateReferenceSystem getOrCreateSubCRS(CoordinateReferenceSystem crs, int lower, int upper) {
if (crs == null) return crs;
int dimension = crs.getCoordinateSystem().getDimension();
if (lower < 0 || lower > upper || upper > dimension) {
throw new IndexOutOfBoundsException(Errors.format(
Errors.Keys.IndexOutOfBounds_1, lower < 0 ? lower : upper));
}
// Dimension exactly matches, no need to decompse the CRS
if (lower == 0 && dimension == upper) return crs;
// CRS can not be decomposed
if (!(crs instanceof CompoundCRS)) return null;
final List<CoordinateReferenceSystem> parts = new ArrayList<>(1);
final int res = decomposeCRS(crs, lower, upper, parts);
if (res == -1) {
// CRS could not be divided
return null;
}
final int size = parts.size();
if (size == 1) {
return parts.get(0);
} else {
// Aggregate crs parts name
final CoordinateReferenceSystem[] array = parts.toArray(new CoordinateReferenceSystem[size]);
final StringBuilder sb = new StringBuilder(array[0].getName().toString());
for (int i=1; i<size; i++) {
sb.append(" with ").append(array[i].getName().toString());
}
return new DefaultCompoundCRS(Collections.singletonMap(DefaultCompoundCRS.NAME_KEY, sb.toString()), array);
}
}
/**
* Internal use only.
* Fill a list of CoordinateReferenceSystem with CRS parts in the given lower/upper range.
*
* @param crs CoordinateReferenceSystem to decompose
* @param lower dimension start range
* @param upper dimension start range
* @param parts used to stack CoordinateReferenceSystem when decomposing CRS.
* @return number of dimensions used, -1 if the current crs could not be decomposed to match lower/upper bounds
*/
private static int decomposeCRS(CoordinateReferenceSystem crs, int lower, int upper, final List<CoordinateReferenceSystem> parts) {
final int dimension = crs.getCoordinateSystem().getDimension();
if (lower == 0 && dimension <= upper) {
// Dimension is smaller or exactly match, no need to decompse the crs
parts.add(crs);
return dimension;
} else if (lower >= dimension){
// Skip this CRS
return dimension;
}
// CRS can not be decomposed
if (!(crs instanceof CompoundCRS)) return -1;
int nbDimRead = 0;
final List<CoordinateReferenceSystem> components = ((CompoundCRS) crs).getComponents();
for (CoordinateReferenceSystem component : components) {
int res = decomposeCRS(component, lower, upper, parts);
if (res == -1) {
// Sub element could not be decomposed
return -1;
}
nbDimRead += res;
lower = Math.max(0, lower-res);
upper = Math.max(0, upper-res);
if (upper == 0) break;
}
return nbDimRead;
}
/**
* Returns the datum of the specified CRS, or {@code null} if none.
* This method processes as below:
* <p>
* <ul>
* <li>If the given CRS is an instance of {@link SingleCRS}, then this method returns
* <code>crs.{@linkplain SingleCRS#getDatum() getDatum()}</code>.</li>
* <li>Otherwise if the given CRS is an instance of {@link CompoundCRS}, then:
* <ul>
* <li>If all components have the same datum, then that datum is returned.</li>
* <li>Otherwise if the CRS contains only a geodetic datum with a vertical datum
* of type <em>ellipsoidal height</em> (no other type accepted), then the
* geodetic datum is returned.</li>
* </ul></li>
* <li>Otherwise this method returns {@code null}.</li>
* </ul>
*
* @param crs The coordinate reference system for which to get the datum. May be {@code null}.
* @return The datum in the given CRS, or {@code null} if none.
*
* @see #getEllipsoid(CoordinateReferenceSystem)
*
* @category information
* @since 3.16
*/
public static Datum getDatum(final CoordinateReferenceSystem crs) {
return CRSUtilities.getDatum(crs);
}
/**
* Returns the first ellipsoid found in a coordinate reference system,
* or {@code null} if there is none. More specifically:
* <p>
* <ul>
* <li>If the given CRS is an instance of {@link SingleCRS} and its datum is a
* {@link GeodeticDatum}, then this method returns the datum ellipsoid.</li>
* <li>Otherwise if the given CRS is an instance of {@link CompoundCRS}, then this method
* invokes itself recursively for each component until a geodetic datum is found.</li>
* <li>Otherwise this method returns {@code null}.</li>
* </ul>
* <p>
* Note that this method does not check if there is more than one ellipsoid
* (it should never be the case).
*
* @param crs The coordinate reference system, or {@code null}.
* @return The ellipsoid, or {@code null} if none.
*
* @see #getDatum(CoordinateReferenceSystem)
*
* @category information
* @since 2.4
*/
public static Ellipsoid getEllipsoid(final CoordinateReferenceSystem crs) {
if (crs instanceof SingleCRS) {
final Datum datum = ((SingleCRS) crs).getDatum();
if (datum instanceof GeodeticDatum) {
return ((GeodeticDatum) datum).getEllipsoid();
}
}
if (crs instanceof CompoundCRS) {
for (final CoordinateReferenceSystem c : ((CompoundCRS) crs).getComponents()) {
final Ellipsoid candidate = getEllipsoid(c);
if (candidate != null) {
return candidate;
}
}
}
return null;
}
/////////////////////////////////////////////////
//// ////
//// COORDINATE OPERATIONS ////
//// ////
/////////////////////////////////////////////////
/**
* Compares the specified objects for equality, ignoring metadata and slight differences
* in numerical values. If this method returns {@code true}, then:
*
* <ul>
* <li><p>If the two given objects are {@link MathTransform} instances, then transforming a
* set of coordinate values using one transform will produce <em>approximatively</em>
* the same results than transforming the same coordinates with the other transform.</p></li>
*
* <li><p>If the two given objects are {@link CoordinateReferenceSystem} instances,
* then a call to <code>{@linkplain #findMathTransform(CoordinateReferenceSystem,
* CoordinateReferenceSystem) findMathTransform}(crs1, crs2)</code> will return
* a transform close to the identity transform.</p></li>
* </ul>
*
* {@section Implementation note}
* This is a convenience method for the following method call:
*
* {@preformat java
* return Utilities.deepEquals(object1, object2, ComparisonMode.APPROXIMATIVE);
* }
*
* @param object1 The first object to compare (may be null).
* @param object2 The second object to compare (may be null).
* @return {@code true} if both objects are approximatively equal.
*
* @see Utilities#deepEquals(Object, Object, ComparisonMode)
* @see ComparisonMode#APPROXIMATIVE
*
* @category information
* @since 3.18
*
* @deprecated Moved to Apache SIS as {@link Utilities#equalsApproximatively(Object, Object)}.
*/
@Deprecated
public static boolean equalsApproximatively(final Object object1, final Object object2) {
return Utilities.equalsApproximatively(object1, object2);
}
/**
* Transforms the given relative distance using the given transform. A relative distance
* vector is transformed without applying the translation components. However it needs to
* be computed at a particular location, given by the {@code origin} parameter in units
* of the source CRS.
*
* @param transform The transformation to apply.
* @param origin The position where to compute the delta transform in the source CRS.
* @param vector The distance vector to be delta transformed.
* @return The result of the delta transformation.
* @throws TransformException if the transformation failed.
*
* @see AffineTransform#deltaTransform(Point2D, Point2D)
*
* @since 3.10 (derived from 2.3)
*/
public static double[] deltaTransform(final MathTransform transform,
final DirectPosition origin, final double... vector) throws TransformException
{
ensureNonNull("transform", transform);
final int sourceDim = transform.getSourceDimensions();
final int targetDim = transform.getTargetDimensions();
final double[] result = new double[targetDim];
if (vector.length != sourceDim) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedDimension_3,
"vector", vector.length, sourceDim));
}
if (transform instanceof AffineTransform) {
((AffineTransform) transform).deltaTransform(vector, 0, result, 0, 1);
} else {
/*
* If the optimized case in the previous "if" statement can't be used,
* use a more generic (but more costly) algorithm.
*/
final double[] coordinates = new double[2 * Math.max(sourceDim, targetDim)];
for (int i=0; i<sourceDim; i++) {
final double c = origin.getOrdinate(i);
final double d = vector[i] * 0.5;
coordinates[i] = c - d;
coordinates[i + sourceDim] = c + d;
}
transform.transform(coordinates, 0, coordinates, 0, 2);
for (int i=0; i<targetDim; i++) {
result[i] = coordinates[i + targetDim] - coordinates[i];
}
}
return result;
}
}