/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2001-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.internal.referencing; import java.util.List; import java.util.ArrayList; import javax.measure.Unit; import javax.measure.quantity.Angle; import org.opengis.referencing.*; import org.opengis.referencing.cs.*; import org.opengis.referencing.crs.*; import org.opengis.referencing.datum.*; import org.opengis.referencing.operation.*; import org.opengis.geometry.Envelope; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.referencing.ReferencingUtilities; import org.apache.sis.internal.metadata.AxisDirections; import org.geotoolkit.lang.Static; import org.geotoolkit.lang.Workaround; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.referencing.crs.DefaultCompoundCRS; import org.apache.sis.referencing.crs.DefaultGeographicCRS; import org.apache.sis.internal.metadata.VerticalDatumTypes; import org.geotoolkit.measure.Measure; import org.geotoolkit.resources.Errors; import org.apache.sis.measure.Units; import static java.util.Collections.singletonMap; /** * A set of static methods working on OpenGIS® * {@linkplain CoordinateReferenceSystem coordinate reference system} objects. * Some of those methods are useful, but not really rigorous. This is why they * do not appear in the "official" package, but instead in this private one. * <p> * <strong>Do not rely on this API!</strong> It may change in incompatible way * in any future release. * * @version 3.21 * * @since 2.0 * @module */ public final class CRSUtilities extends Static { /** * Do not allow creation of instances of this class. */ private CRSUtilities() { } /** * Returns the dimension of the first coordinate reference system of the given type. The * {@code type} argument must be a sub-interface of {@link CoordinateReferenceSystem}. * If no such dimension is found, then this method returns {@code -1}. * * @param crs The coordinate reference system (CRS) to examine. * @param type The CRS type to look for. * Must be a subclass of {@link CoordinateReferenceSystem}. * @return The dimension range of the specified CRS type, or {@code -1} if none. * @throws IllegalArgumentException if the {@code type} is not legal. */ public static int getDimensionOf(final CoordinateReferenceSystem crs, final Class<? extends CoordinateReferenceSystem> type) throws IllegalArgumentException { if (type.isAssignableFrom(crs.getClass())) { return 0; } if (crs instanceof CompoundCRS) { int offset = 0; for (final CoordinateReferenceSystem ci : ((CompoundCRS) crs).getComponents()) { final int index = getDimensionOf(ci, type); if (index >= 0) { return index + offset; } offset += ci.getCoordinateSystem().getDimension(); } } return -1; } /** * Returns a two-dimensional coordinate reference system representing the two first dimensions * of the specified coordinate reference system. If {@code crs} is already a two-dimensional * CRS, then it is returned unchanged. Otherwise, if it is a {@link CompoundCRS}, then the * head coordinate reference system is examined. * * @param crs The coordinate system, or {@code null}. * @return A two-dimensional coordinate reference system that represents the two first * dimensions of {@code crs}, or {@code null} if {@code crs} was {@code null}. * @throws TransformException if {@code crs} can't be reduced to a two-coordinate system. * We use this exception class since this method is usually invoked in the context * of a transformation process. */ public static CoordinateReferenceSystem getCRS2D(CoordinateReferenceSystem crs) throws TransformException { if (crs != null) { final CoordinateReferenceSystem original = crs; while (crs.getCoordinateSystem().getDimension() != 2) { if (crs instanceof CompoundCRS) { crs = ((CompoundCRS) crs).getComponents().get(0); // Continue the loop, examining only the first component. } else { crs = CRS.getHorizontalComponent(crs); if (crs == null) { throw new TransformException(Errors.format( Errors.Keys.CantReduceToTwoDimensions_1, original.getName())); } } } } return crs; } /** * Returns an envelope containing all dimensions of the given CRS, padding additional dimensions * with NaN values if necessary. This method is a workaround for the current Geotk behavior, * which is to thrown an exception when re-projecting an envelope to another CRS having more * dimensions. This method will hopefully be deleted in a future version. * * @param envelope The envelope to expand. * @param crs The target CRS. * @return An envelope having the dimensions of the given CRS. */ @Workaround(library="Geotk", version="3.21") public static Envelope appendMissingDimensions(final Envelope envelope, final CompoundCRS crs) { final List<CoordinateReferenceSystem> toAdd = new ArrayList<>(4); final CoordinateReferenceSystem currentCRS = envelope.getCoordinateReferenceSystem(); final CoordinateSystem currentCS = currentCRS.getCoordinateSystem(); for (final SingleCRS subCRS : CRS.getSingleComponents(crs)) { final CoordinateSystem subCS = subCRS.getCoordinateSystem(); if (subCS.getDimension() == 1 && AxisDirections.indexOfColinear(currentCS, subCS) < 0) { toAdd.add(subCRS); } } if (toAdd.isEmpty()) { return envelope; } toAdd.add(0, currentCRS); final GeneralEnvelope expanded = new GeneralEnvelope(new DefaultCompoundCRS( singletonMap(DefaultCompoundCRS.NAME_KEY, "Temporarily expanded"), toAdd.toArray(new CoordinateReferenceSystem[toAdd.size()]))); expanded.setToNaN(); expanded.subEnvelope(0, envelope.getDimension()).setEnvelope(envelope); return expanded; } /** * Returns the angular unit of the specified coordinate system. * The preference will be given to the longitude axis, if found. */ public static Unit<Angle> getAngularUnit(final CoordinateSystem coordinateSystem) { Unit<Angle> unit = Units.DEGREE; for (int i=coordinateSystem.getDimension(); --i>=0;) { final CoordinateSystemAxis axis = coordinateSystem.getAxis(i); final Unit<?> candidate = axis.getUnit(); if (Units.isAngular(candidate)) { unit = candidate.asType(Angle.class); if (AxisDirection.EAST.equals(AxisDirections.absolute(axis.getDirection()))) { break; // Found the longitude axis. } } } return unit; } /** * Implementation of {@link CRS#getDatum(CoordinateReferenceSystem)}, defined here in order * to avoid a dependency of {@link org.geotoolkit.referencing.crs.AbstractDerivedCRS} to the * {@link CRS} class. * * @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 CRS#getEllipsoid(CoordinateReferenceSystem) */ public static Datum getDatum(final CoordinateReferenceSystem crs) { Datum datum = null; if (crs instanceof SingleCRS) { datum = ((SingleCRS) crs).getDatum(); } else if (crs instanceof CompoundCRS) { for (final CoordinateReferenceSystem component : ((CompoundCRS) crs).getComponents()) { final Datum candidate = getDatum(component); if (datum != null && !datum.equals(candidate)) { if (isGeodetic3D(datum, candidate)) { continue; // Keep the current datum unchanged. } if (isGeodetic3D(candidate, datum)) { continue; } return null; // Can't build a 3D geodetic datum. } datum = candidate; } } return datum; } /** * Returns {@code true} if the given datum can form a three-dimensional geodetic datum. * * @param geodetic The presumed geodetic datum. * @param vertical The presumed vertical datum. * @return If the given datum can form a 3D geodetic datum. * * @deprecated This is not right: we can not said that we have a match if we do not known * on which geodetic datum the ellipsoidal height is. */ private static boolean isGeodetic3D(final Datum geodetic, final Datum vertical) { return (geodetic instanceof GeodeticDatum) && (vertical instanceof VerticalDatum) && VerticalDatumTypes.ELLIPSOIDAL.equals(((VerticalDatum) vertical).getVerticalDatumType()); } /** * Computes the resolution of the horizontal component, or {@code null} if it can not be * computed. * * @param crs The full (potentially multi-dimensional) CRS. * @param resolution The resolution along each CRS dimension, or {@code null} if none. * The array length shall be equals to the CRS dimension. * @return The horizontal resolution, or {@code null}. * * @since 3.18 */ public static Measure getHorizontalResolution(final CoordinateReferenceSystem crs, double... resolution) { if (resolution != null) { final SingleCRS horizontalCRS = CRS.getHorizontalComponent(crs); if (horizontalCRS != null) { final CoordinateSystem hcs = horizontalCRS.getCoordinateSystem(); final Unit<?> unit = ReferencingUtilities.getUnit(hcs); if (unit != null) { final int s = AxisDirections.indexOfColinear(crs.getCoordinateSystem(), hcs); if (s >= 0) { final int dim = hcs.getDimension(); double min = Double.POSITIVE_INFINITY; for (int i=s; i<dim; i++) { final double r = resolution[i]; if (r > 0 && r < min) min = r; } if (min != Double.POSITIVE_INFINITY) { return new Measure(min, unit); } } } } } return null; } /** * Delegates to the {@code shiftAxisRange} method of the appropriate class. This method is not * in public API because the call to {@code castOrCopy} may involve useless objects creation. * Developers are encouraged to invoke {@code castOrCopy} themselves, then replace their old * reference by the new one before to invoke {@code shiftAxisRange}, because they Geotk * implementations cache the result of {@code shiftAxisRange} calls. * * @param crs The CRS on which to apply the shift, or {@code null}. * @param type The desired range of ordinate values. * @return The shifted CRS, or {@code crs} if no range shift applied. * * @since 3.20 */ public static CoordinateReferenceSystem shiftAxisRange(final CoordinateReferenceSystem crs, final AxesConvention type) { if (crs instanceof GeographicCRS) { final DefaultGeographicCRS impl = DefaultGeographicCRS.castOrCopy((GeographicCRS) crs); final DefaultGeographicCRS shifted = impl.forConvention(type); if (shifted != impl) return shifted; } else if (crs instanceof CompoundCRS) { final DefaultCompoundCRS impl = DefaultCompoundCRS.castOrCopy((CompoundCRS) crs); final DefaultCompoundCRS shifted = impl.forConvention(type); if (shifted != impl) return shifted; } return crs; } /** * Retrieve index of the first axis of the geographic component in the input {@link CoordinateReferenceSystem}. * * @param crs {@link CoordinateReferenceSystem} which is analyzed. * @return Index of the first horizontal axis in this CRS * @throws IllegalArgumentException if input CRS has no horizontal component. * * @todo Inaccurate implementation: a Cartesian or spherical CS does not mean that the axes is horizontal. */ public static int firstHorizontalAxis(final CoordinateReferenceSystem crs) { int tempOrdinate = 0; for (CoordinateReferenceSystem component : CRS.getSingleComponents(crs)) { final CoordinateSystem cs = component.getCoordinateSystem(); if ((cs instanceof CartesianCS) || (cs instanceof SphericalCS) || (cs instanceof EllipsoidalCS)) // Inaccurate check: see TODO in javadoc. { return tempOrdinate; } tempOrdinate += cs.getDimension(); } throw new IllegalArgumentException("crs doesn't have any horizontal crs"); } /** * Returns the group of referencing objects to which the given type belong. * The {@code type} argument can be a GeoAPI interface or an implementation class. * The value returned by this method will be one of {@link CoordinateReferenceSystem}, * {@link CoordinateSystem}, {@link CoordinateSystemAxis}, {@link Datum}, {@link Ellipsoid}, * {@link PrimeMeridian} or {@link IdentifiedObject} classes. If the given type is assignable * to more than one group, then this method returns {@code IdentifiedObject} class. * * @param type The type for which to get the referencing group, or {@code null}. * @return The group to which the given type belong, or {@code null} if the given argument * was {@code null}. */ public static Class<? extends IdentifiedObject> getReferencingGroup(final Class<? extends IdentifiedObject> type) { Class<? extends IdentifiedObject> found = null; if (type != null) { search: for (int i=0; ; i++) { final Class<? extends IdentifiedObject> candidate; switch (i) { case 0: candidate = CoordinateReferenceSystem.class; break; case 1: candidate = CoordinateSystem.class; break; case 2: candidate = CoordinateSystemAxis.class; break; case 3: candidate = Datum.class; break; case 4: candidate = Ellipsoid.class; break; case 5: candidate = PrimeMeridian.class; break; default: break search; } if (candidate.isAssignableFrom(type)) { if (found != null) { return IdentifiedObject.class; } found = candidate; } } if (found == null) { return IdentifiedObject.class; } } return found; } }