blob: feb0967c7aa8e15d433f96d7f2e1abf540238780 [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.internal.referencing;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import org.opengis.annotation.UML;
import org.opengis.annotation.Specification;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.datum.Datum;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.datum.PrimeMeridian;
import org.opengis.referencing.datum.GeodeticDatum;
import org.opengis.referencing.datum.VerticalDatum;
import org.opengis.referencing.datum.VerticalDatumType;
import org.apache.sis.util.Static;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.datum.DefaultPrimeMeridian;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory.Context;
import static java.util.Collections.singletonMap;
/**
* A set of static methods working on GeoAPI referencing objects.
* Some of those methods may be useful, but not really rigorous.
* This is why they do not appear in the public packages.
*
* <p><strong>Do not rely on this API!</strong> It may change in incompatible way in any future release.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
* @since 0.5
* @module
*/
public final class ReferencingUtilities extends Static {
/**
* Do not allow instantiation of this class.
*/
private ReferencingUtilities() {
}
/**
* Returns the longitude value relative to the Greenwich Meridian, expressed in the specified units.
* This method provides the same functionality than {@link DefaultPrimeMeridian#getGreenwichLongitude(Unit)},
* but on arbitrary implementation.
*
* @param primeMeridian the prime meridian from which to get the Greenwich longitude, or {@code null}.
* @param unit the unit for the prime meridian to return.
* @return the prime meridian in the given units, or {@code 0} if the given prime meridian was null.
*
* @see DefaultPrimeMeridian#getGreenwichLongitude(Unit)
* @see org.apache.sis.referencing.CRS#getGreenwichLongitude(GeodeticCRS)
*/
public static double getGreenwichLongitude(final PrimeMeridian primeMeridian, final Unit<Angle> unit) {
if (primeMeridian == null) {
return 0;
} else if (primeMeridian instanceof DefaultPrimeMeridian) { // Maybe the user overrode some methods.
return ((DefaultPrimeMeridian) primeMeridian).getGreenwichLongitude(unit);
} else {
return primeMeridian.getAngularUnit().getConverterTo(unit).convert(primeMeridian.getGreenwichLongitude());
}
}
/**
* Returns the unit used for all axes in the given coordinate system.
* If not all axes use the same unit, then this method returns {@code null}.
*
* <p>This method is used either when the coordinate system is expected to contain exactly one axis,
* or for operations that support only one units for all axes, for example Well Know Text version 1
* (WKT 1) formatting.</p>
*
* @param cs the coordinate system for which to get the unit, or {@code null}.
* @return the unit for all axis in the given coordinate system, or {@code null}.
*
* @see org.apache.sis.internal.referencing.AxisDirections#getAngularUnit(CoordinateSystem, Unit)
*/
public static Unit<?> getUnit(final CoordinateSystem cs) {
Unit<?> unit = null;
if (cs != null) {
for (int i=cs.getDimension(); --i>=0;) {
final CoordinateSystemAxis axis = cs.getAxis(i);
if (axis != null) { // Paranoiac check.
final Unit<?> candidate = axis.getUnit();
if (candidate != null) {
if (unit == null) {
unit = candidate;
} else if (!unit.equals(candidate)) {
return null;
}
}
}
}
}
return unit;
}
/**
* Returns the number of dimensions of the given CRS, or 0 if {@code null}.
*
* @param crs the CRS from which to get the number of dimensions, or {@code null}.
* @return the number of dimensions, or 0 if the given CRS or its coordinate system is null.
*/
public static int getDimension(final CoordinateReferenceSystem crs) {
if (crs != null) {
final CoordinateSystem cs = crs.getCoordinateSystem();
if (cs != null) { // Paranoiac check.
return cs.getDimension();
}
}
return 0;
}
/**
* Returns the GeoAPI interface implemented by the given object, or the implementation class
* if the interface is unknown.
*
* @param object the object for which to get the GeoAPI interface, or {@code null}.
* @return GeoAPI interface or implementation class of the given object, or {@code null} if the given object is null.
*/
public static Class<?> getInterface(final IdentifiedObject object) {
if (object == null) {
return null;
} else if (object instanceof AbstractIdentifiedObject) {
return ((AbstractIdentifiedObject) object).getInterface();
} else {
return object.getClass();
}
}
/**
* Copies all {@link SingleCRS} components from the given source to the given collection.
* For each {@link CompoundCRS} element found in the iteration, this method replaces the
* {@code CompoundCRS} by its {@linkplain CompoundCRS#getComponents() components}, which
* may themselves have other {@code CompoundCRS}. Those replacements are performed recursively
* until we obtain a flat view of CRS components.
*
* @param source the collection of single or compound CRS.
* @param addTo where to add the single CRS in order to obtain a flat view of {@code source}.
* @return {@code true} if this method found only single CRS in {@code source}, in which case {@code addTo}
* got the same content (assuming that {@code addTo} was empty prior this method call).
* @throws ClassCastException if a CRS is neither a {@link SingleCRS} or a {@link CompoundCRS}.
*
* @see org.apache.sis.referencing.CRS#getSingleComponents(CoordinateReferenceSystem)
*/
public static boolean getSingleComponents(final Iterable<? extends CoordinateReferenceSystem> source,
final Collection<? super SingleCRS> addTo) throws ClassCastException
{
boolean sameContent = true;
for (final CoordinateReferenceSystem candidate : source) {
if (candidate instanceof CompoundCRS) {
getSingleComponents(((CompoundCRS) candidate).getComponents(), addTo);
sameContent = false;
} else {
// Intentional CassCastException here if the candidate is not a SingleCRS.
addTo.add((SingleCRS) candidate);
}
}
return sameContent;
}
/**
* Returns {@code true} if the type of the given datum is ellipsoidal. A vertical datum is not allowed
* to be ellipsoidal according ISO 19111, but Apache SIS relaxes this restriction in some limited cases,
* for example when parsing a string in the legacy WKT 1 format. Apache SIS should not expose those
* vertical heights as much as possible, and instead try to combine them with three-dimensional
* geographic or projected CRS as soon as it can.
*
* @param datum the datum to test, or {@code null} if none.
* @return {@code true} if the given datum is non null and of ellipsoidal type.
*
* @see org.apache.sis.internal.referencing.VerticalDatumTypes#ELLIPSOIDAL
*/
public static boolean isEllipsoidalHeight(final VerticalDatum datum) {
if (datum != null) {
final VerticalDatumType type = datum.getVerticalDatumType();
if (type != null) {
return "ELLIPSOIDAL".equalsIgnoreCase(type.name());
}
}
return false;
}
/**
* Returns the ellipsoid used by the given coordinate reference system, or {@code null} if none.
* More specifically:
*
* <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>
*
* 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 for which to get the ellipsoid.
* @return the ellipsoid, or {@code null} if none.
*/
public static Ellipsoid getEllipsoid(final CoordinateReferenceSystem crs) {
if (crs != null) {
if (crs instanceof SingleCRS) {
final Datum datum = ((SingleCRS) crs).getDatum();
if (datum instanceof GeodeticDatum) {
final Ellipsoid e = ((GeodeticDatum) datum).getEllipsoid();
if (e != null) return e;
}
}
if (crs instanceof CompoundCRS) {
for (final CoordinateReferenceSystem c : ((CompoundCRS) crs).getComponents()) {
final Ellipsoid e = getEllipsoid(c);
if (e != null) return e;
}
}
}
return null;
}
/**
* Returns the ellipsoid used by the specified coordinate reference system, provided that the two first dimensions
* use an instance of {@link GeographicCRS}. Otherwise (i.e. if the two first dimensions are not geographic),
* returns {@code null}.
*
* <p>This method excludes geocentric CRS on intent. Some callers needs this exclusion as a way to identify
* which CRS in a Geographic/Geocentric conversion is the geographic one. An other point of view is to said
* that if this method returns a non-null value, then the coordinates are expected to be either two-dimensional
* or three-dimensional with an ellipsoidal height.</p>
*
* @param crs the coordinate reference system for which to get the ellipsoid.
* @return the ellipsoid in the given CRS, or {@code null} if none.
*/
public static Ellipsoid getEllipsoidOfGeographicCRS(CoordinateReferenceSystem crs) {
while (!(crs instanceof GeodeticCRS)) {
if (crs instanceof CompoundCRS) {
crs = ((CompoundCRS) crs).getComponents().get(0);
} else {
return null;
}
}
/*
* In order to determine if the CRS is geographic, checking the CoordinateSystem type is more reliable
* then checking if the CRS implements the GeographicCRS interface. This is because the GeographicCRS
* interface is GeoAPI-specific, so a CRS may be OGC-compliant without implementing that interface.
*/
if (crs.getCoordinateSystem() instanceof EllipsoidalCS) {
return ((GeodeticCRS) crs).getDatum().getEllipsoid();
} else {
return null; // Geocentric CRS.
}
}
/**
* Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>) axis in the specified order and in decimal degrees.
* If no such CRS can be obtained or created, returns {@code null}.
*
* <p>This method does not set the prime meridian to Greenwich.
* Meridian rotation, if needed, shall be performed by the caller.</p>
*
* @param crs a source CRS, or {@code null}.
* @param latlon {@code true} for (latitude, longitude) axis order, or {@code false} for (longitude, latitude).
* @param allow3D whether this method is allowed to return three-dimensional CRS (with ellipsoidal height).
* @return a two-dimensional geographic CRS with standard axes, or {@code null} if none.
*/
public static GeographicCRS toNormalizedGeographicCRS(CoordinateReferenceSystem crs, final boolean latlon, final boolean allow3D) {
/*
* ProjectedCRS instances always have a GeographicCRS as their base.
* More generally, derived CRS are always derived from a base, which
* is often (but not necessarily) geographic.
*/
while (crs instanceof GeneralDerivedCRS) {
crs = ((GeneralDerivedCRS) crs).getBaseCRS();
}
if (crs instanceof GeodeticCRS) {
/*
* At this point we usually have a GeographicCRS, but it could also be a GeocentricCRS.
* If we can let `forConvention` do its job, do that first since it may return a cached
* instance. If the CRS is a `GeographicCRS` but not a `DefaultGeographicCRS`, create a
* CRS in this code instead than invoking `DefaultGeographicCRS.castOrCopy(…)` in order
* to create only one CRS instead of two.
*/
final CoordinateSystem cs = crs.getCoordinateSystem();
if (!latlon && crs instanceof DefaultGeographicCRS && (allow3D || cs.getDimension() == 2)) {
return ((DefaultGeographicCRS) crs).forConvention(AxesConvention.NORMALIZED);
}
/*
* Get a normalized coordinate system with the number of dimensions authorized by the
* `allow3D` argument. We do not check if we can invoke `cs.forConvention(…)` because
* it is unlikely that `cs` will be an instance of `DefaultEllipsoidalCS` is `crs` is
* not a `DefaultGeographicCRS`. The code below has more chances to use cached instance.
*/
EllipsoidalCS normalizedCS;
if (allow3D && cs.getDimension() >= 3) {
normalizedCS = CommonCRS.WGS84.geographic3D().getCoordinateSystem();
if (!latlon) {
normalizedCS = DefaultEllipsoidalCS.castOrCopy(normalizedCS).forConvention(AxesConvention.NORMALIZED);
}
} else if (latlon) {
normalizedCS = CommonCRS.WGS84.geographic().getCoordinateSystem();
} else {
normalizedCS = CommonCRS.defaultGeographic().getCoordinateSystem();
}
if (crs instanceof GeographicCRS && Utilities.equalsIgnoreMetadata(normalizedCS, cs)) {
return (GeographicCRS) crs;
}
return new DefaultGeographicCRS(
singletonMap(DefaultGeographicCRS.NAME_KEY, NilReferencingObject.UNNAMED),
((GeodeticCRS) crs).getDatum(), normalizedCS);
}
if (crs instanceof CompoundCRS) {
for (final CoordinateReferenceSystem e : ((CompoundCRS) crs).getComponents()) {
final GeographicCRS candidate = toNormalizedGeographicCRS(e, latlon, allow3D);
if (candidate != null) {
return candidate;
}
}
}
return null;
}
/**
* Returns the properties of the given object but potentially with a modified name.
* Current implement truncates the name at the first non-white character which is not
* a valid Unicode identifier part, with the following exception:
*
* <ul>
* <li>If the character is {@code '('} and the content until the closing {@code ')'} is a valid
* Unicode identifier, then that part is included. The intent is to keep the prime meridian
* name in names like <cite>"NTF (Paris)"</cite>.</li>
* </ul>
*
* <div class="note"><b>Example:</b><ul>
* <li><cite>"NTF (Paris)"</cite> is left unchanged.</li>
* <li><cite>"WGS 84 (3D)"</cite> is truncated as <cite>"WGS 84"</cite>.</li>
* <li><cite>"Ellipsoidal 2D CS. Axes: latitude, longitude. Orientations: north, east. UoM: degree"</cite>
* is truncated as <cite>"Ellipsoidal 2D CS"</cite>.</li>
* </ul></div>
*
* @param object the identified object to view as a properties map.
* @return a view of the identified object properties.
*
* @see IdentifiedObjects#getProperties(IdentifiedObject, String...)
*/
public static Map<String,?> getPropertiesForModifiedCRS(final IdentifiedObject object) {
final Map<String,?> properties = IdentifiedObjects.getProperties(object, IdentifiedObject.IDENTIFIERS_KEY);
final Identifier id = (Identifier) properties.get(IdentifiedObject.NAME_KEY);
if (id != null) {
String name = id.getCode();
if (name != null) {
for (int i=0; i < name.length();) {
final int c = name.codePointAt(i);
if (!Character.isUnicodeIdentifierPart(c) && !Character.isSpaceChar(c)) {
if (c == '(') {
final int endAt = name.indexOf(')', i);
if (endAt >= 0) {
final String extra = name.substring(i+1, endAt);
if (CharSequences.isUnicodeIdentifier(extra)) {
i += extra.length() + 2;
}
}
}
name = CharSequences.trimWhitespaces(name, 0, i).toString();
if (!name.isEmpty()) {
final Map<String,Object> copy = new HashMap<>(properties);
copy.put(IdentifiedObject.NAME_KEY, name);
return copy;
}
}
i += Character.charCount(c);
}
}
}
return properties;
}
/**
* Returns the XML property name of the given interface.
*
* For {@link CoordinateSystem} base type, the returned value shall be one of
* {@code affineCS}, {@code cartesianCS}, {@code cylindricalCS}, {@code ellipsoidalCS}, {@code linearCS},
* {@code parametricCS}, {@code polarCS}, {@code sphericalCS}, {@code timeCS} or {@code verticalCS}.
*
* @param base the abstract base interface.
* @param type the interface or classes for which to get the XML property name.
* @return the XML property name for the given class or interface, or {@code null} if none.
*
* @see WKTUtilities#toType(Class, Class)
*/
public static StringBuilder toPropertyName(final Class<?> base, final Class<?> type) {
final UML uml = type.getAnnotation(UML.class);
if (uml != null) {
final Specification spec = uml.specification();
if (spec == Specification.ISO_19111) {
final String name = uml.identifier();
final int length = name.length();
final StringBuilder buffer = new StringBuilder(length).append(name, name.indexOf('_') + 1, length);
if (buffer.length() != 0) {
buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
return buffer;
}
}
}
for (final Class<?> c : type.getInterfaces()) {
if (base.isAssignableFrom(c)) {
final StringBuilder name = toPropertyName(base, c);
if (name != null) {
return name;
}
}
}
return null;
}
/**
* Sets the source and target ellipsoids and coordinate systems to values inferred from the given CRS.
* The ellipsoids will be non-null only if the given CRS is geographic (not geocentric).
*
* @param sourceCRS the CRS from which to get the source coordinate system and ellipsoid.
* @param targetCRS the CRS from which to get the target coordinate system and ellipsoid.
* @param context a pre-allocated context, or {@code null} for creating a new one.
* @return the given context if it was non-null, or a new context otherwise.
*/
public static Context createTransformContext(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS, Context context)
{
if (context == null) {
context = new Context();
}
final CoordinateSystem sourceCS = (sourceCRS != null) ? sourceCRS.getCoordinateSystem() : null;
final CoordinateSystem targetCS = (targetCRS != null) ? targetCRS.getCoordinateSystem() : null;
if (sourceCRS instanceof GeodeticCRS && sourceCS instanceof EllipsoidalCS) {
context.setSource((EllipsoidalCS) sourceCS, ((GeodeticCRS) sourceCRS).getDatum().getEllipsoid());
} else {
context.setSource(sourceCS);
}
if (targetCRS instanceof GeodeticCRS && targetCS instanceof EllipsoidalCS) {
context.setTarget((EllipsoidalCS) targetCS, ((GeodeticCRS) targetCRS).getDatum().getEllipsoid());
} else {
context.setTarget(targetCS);
}
return context;
}
/**
* Returns the mapping between parameter identifiers and parameter names as defined by the given authority.
* This method assumes that the identifiers of all parameters defined by that authority are numeric.
* Examples of authorities defining numeric parameters are EPSG and GeoTIFF.
*
* <p>The map returned by this method is modifiable. Callers are free to add or remove entries.</p>
*
* @param parameters the parameters for which to get a mapping from identifiers to names.
* @param authority the authority defining the parameters.
* @return mapping from parameter identifiers to parameter names defined by the given authority.
* @throws NumberFormatException if a parameter identifier of the given authority is not numeric.
* @throws IllegalArgumentException if the same identifier is used for two or more parameters.
*/
public static Map<Integer,String> identifierToName(final ParameterDescriptorGroup parameters, final Citation authority) {
final Map<Integer,String> mapping = new HashMap<>();
for (final GeneralParameterDescriptor descriptor : parameters.descriptors()) {
final Identifier id = IdentifiedObjects.getIdentifier(descriptor, authority);
if (id != null) {
String name = IdentifiedObjects.getName(descriptor, authority);
if (name == null) {
name = IdentifiedObjects.getName(descriptor, null);
}
if (mapping.put(Integer.valueOf(id.getCode()), name) != null) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedIdentifier_1, id));
}
}
}
return mapping;
}
/**
* Returns short names for all axes of the given CRS. This method uses short names like "Latitude" or "Height",
* even if the full ISO 19111 names are "Geodetic latitude" or "Ellipsoidal height". This is suitable as header
* for columns in a table. This method does not include abbreviation or units in the returned names.
*
* @param resources the resources from which to get "latitude" and "longitude" localized labels.
* @param crs the coordinate reference system from which to get axis names.
* @return axis names, localized if possible.
*/
public static String[] getShortAxisNames(final Vocabulary resources, final CoordinateReferenceSystem crs) {
final boolean isGeographic = (crs instanceof GeographicCRS);
final boolean isProjected = (crs instanceof ProjectedCRS);
final CoordinateSystem cs = crs.getCoordinateSystem();
final String[] names = new String[cs.getDimension()];
for (int i=0; i<names.length; i++) {
short key = 0;
final CoordinateSystemAxis axis = cs.getAxis(i);
final AxisDirection direction = axis.getDirection();
if (AxisDirections.isCardinal(direction)) {
final boolean isMeridional = AxisDirection.NORTH.equals(direction) || AxisDirection.SOUTH.equals(direction);
if (isGeographic) {
key = isMeridional ? Vocabulary.Keys.Latitude : Vocabulary.Keys.Longitude;
} else if (isProjected) {
// We could add "Easting" / "Northing" here for ProjectedCRS in a future version.
}
} else if (AxisDirection.UP.equals(direction)) {
if (isGeographic | isProjected) {
key = Vocabulary.Keys.Height;
}
}
names[i] = (key != 0) ? resources.getString(key) : axis.getName().getCode();
}
return names;
}
}