/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.resources; import java.util.Map; import java.util.List; import java.util.Iterator; import java.util.Collections; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import javax.measure.unit.Unit; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.crs.CompoundCRS; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.GeneralDerivedCRS; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.datum.Datum; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import org.opengis.geometry.DirectPosition; import org.geotools.geometry.GeneralDirectPosition; import org.geotools.measure.AngleFormat; import org.geotools.measure.Latitude; import org.geotools.measure.Longitude; import org.geotools.referencing.CRS; import org.geotools.referencing.ReferencingFactoryFinder; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.referencing.cs.DefaultEllipsoidalCS; import org.geotools.referencing.datum.DefaultGeodeticDatum; import org.geotools.referencing.datum.DefaultPrimeMeridian; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; /** * 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. * <strong>Do not rely on this API!</strong> It may change in incompatible way * in any future release. * * @since 2.0 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public final class CRSUtilities { /** * Do not allow creation of instances of this class. */ private CRSUtilities() { } /** * Returns the dimension within the coordinate system of the first occurrence of an axis * colinear with the specified axis. If an axis with the same * {@linkplain CoordinateSystemAxis#getDirection direction} or an * {@linkplain AxisDirection#inverse opposite} direction than {@code axis} * ocurs in the coordinate system, then the dimension of the first such occurrence * is returned. That is, the a value <i>k</i> such that: * * <blockquote><pre> * cs.getAxis(<i>k</i>).getDirection().absolute() == axis.getDirection().absolute() * </pre></blockquote> * * is {@code true}. If no such axis occurs in this coordinate system, * then {@code -1} is returned. * <p> * For example, {@code dimensionColinearWith(CoordinateSystemAxis.TIME)} * returns the dimension number of time axis. * * @param cs The coordinate system to examine. * @param axis The axis to look for. * @return The dimension number of the specified axis, or {@code -1} if none. */ public static int dimensionColinearWith(final CoordinateSystem cs, final CoordinateSystemAxis axis) { int candidate = -1; final int dimension = cs.getDimension(); final AxisDirection direction = axis.getDirection().absolute(); for (int i=0; i<dimension; i++) { final CoordinateSystemAxis xi = cs.getAxis(i); if (direction.equals(xi.getDirection().absolute())) { candidate = i; if (axis.equals(xi)) { break; } } } return candidate; } /** * Returns the unit used for all axis in the specified coordinate system. * If not all axis uses the same unit, then this method returns {@code null}. * This convenience method is often used for Well Know Text (WKT) formatting. * * @param cs The coordinate system for which to get the unit. * @return The unit for all axis in the given coordinate system, or {@code null}. * * @since 2.2 */ public static Unit<?> getUnit(final CoordinateSystem cs) { Unit<?> unit = null; for (int i=cs.getDimension(); --i>=0;) { final Unit<?> candidate = cs.getAxis(i).getUnit(); if (candidate != null) { if (unit == null) { unit = candidate; } else if (!unit.equals(candidate)) { return null; } } } return unit; } /** * Returns the components of the specified CRS, or {@code null} if none. */ private static List<CoordinateReferenceSystem> getComponents(CoordinateReferenceSystem crs) { if (crs instanceof CompoundCRS) { final List<CoordinateReferenceSystem> components; components = ((CompoundCRS) crs).getCoordinateReferenceSystems(); if (!components.isEmpty()) { return components; } } return null; } /** * Returns the dimension of the first coordinate reference system of the given type. The * {@code type} argument must be a subinterface 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; } final List<CoordinateReferenceSystem> c = getComponents(crs); if (c != null) { int offset = 0; for (final CoordinateReferenceSystem ci : c) { final int index = getDimensionOf(ci, type); if (index >= 0) { return index + offset; } offset += ci.getCoordinateSystem().getDimension(); } } return -1; } /** * Returns a sub-coordinate reference system for the specified dimension range. * * @param crs The coordinate reference system to decompose. * @param lower The first dimension to keep, inclusive. * @param upper The last dimension to keep, exclusive. * @return The sub-coordinate system, or {@code null} if {@code crs} can't * be decomposed for dimensions in the range {@code [lower..upper]}. */ public static CoordinateReferenceSystem getSubCRS(CoordinateReferenceSystem crs, int lower, int upper) { int dimension = crs.getCoordinateSystem().getDimension(); if (lower<0 || lower>upper || upper>dimension) { throw new IndexOutOfBoundsException(Errors.format( ErrorKeys.INDEX_OUT_OF_BOUNDS_$1, lower<0 ? lower : upper)); } while (lower!=0 || upper!=dimension) { final List<CoordinateReferenceSystem> c = getComponents(crs); if (c == null) { return null; } for (final Iterator<CoordinateReferenceSystem> it=c.iterator(); it.hasNext();) { crs = it.next(); dimension = crs.getCoordinateSystem().getDimension(); if (lower < dimension) { break; } lower -= dimension; upper -= dimension; } } return crs; } /** * 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) { while (crs.getCoordinateSystem().getDimension() != 2) { final List<CoordinateReferenceSystem> c = getComponents(crs); if (c == null) { throw new TransformException(Errors.format( ErrorKeys.CANT_REDUCE_TO_TWO_DIMENSIONS_$1, crs.getName())); } crs = c.get(0); } } return crs; } /** * Changes the dimension declared in the name. For example if {@code name} is * "WGS 84 (geographic 3D)", {@code search} is "3D" and {@code replace} is "2D", * then this method returns "WGS 84 (geographic 2D)". If the string to search is * not found, then it is concatenated to the name. * * @param object The identified object having the original name. * @param search The dimension token to search in the {@code object} name. * @param replace The new token to substitute to the one we were looking for. * @return The name with the substitution performed. * * @since 2.6 */ public static Map<String,?> changeDimensionInName(final IdentifiedObject object, final String search, final String replace) { final StringBuilder name = new StringBuilder(object.getName().getCode()); final int last = name.length() - search.length(); boolean append = true; for (int i=name.lastIndexOf(search); i>=0; i=name.lastIndexOf(search, i-1)) { if (i != 0 && Character.isLetterOrDigit(name.charAt(i-1))) { continue; } if (i != last && Character.isLetterOrDigit(i + search.length())) { continue; } name.replace(i, i+search.length(), replace); i = name.indexOf(". ", i); if (i >= 0) { /* * Stops the sentence after the dimension, since it may contains details that * are not applicable anymore. For example the EPSG name for 3D EllipsoidalCS is: * * Ellipsoidal 3D CS. Axes: latitude, longitude, ellipsoidal height. * Orientations: north, east, up. UoM: DMSH, DMSH, m. */ name.setLength(i+1); } append = false; break; } if (append) { if (name.indexOf(" ") >= 0) { name.append(" (").append(replace).append(')'); } else { name.append('_').append(replace); } } return Collections.singletonMap(IdentifiedObject.NAME_KEY, name.toString()); } /** * Returns the datum of the specified CRS, or {@code null} if none. * * @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. */ public static Datum getDatum(final CoordinateReferenceSystem crs) { return (crs instanceof SingleCRS) ? ((SingleCRS) crs).getDatum() : null; } /** * Returns the ellipsoid used by the specified coordinate reference system, providing 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}. * * @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 getHeadGeoEllipsoid(CoordinateReferenceSystem crs) { while (!(crs instanceof GeographicCRS)) { final List<CoordinateReferenceSystem> c = getComponents(crs); if (c == null) { return null; } crs = c.get(0); } return ((GeographicCRS) crs).getDatum().getEllipsoid(); } /** * Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>) axis order in * decimal degrees, relative to Greenwich. If no such CRS can be obtained of created, returns * {@link DefaultGeographicCRS#WGS84}. * * @param crs A source CRS. * @return A two-dimensional geographic CRS with standard axis. Never {@code null}. */ public static GeographicCRS getStandardGeographicCRS2D(CoordinateReferenceSystem crs) { while (crs instanceof GeneralDerivedCRS) { crs = ((GeneralDerivedCRS) crs).getBaseCRS(); } if (!(crs instanceof SingleCRS)) { return DefaultGeographicCRS.WGS84; } final Datum datum = ((SingleCRS) crs).getDatum(); if (!(datum instanceof GeodeticDatum)) { return DefaultGeographicCRS.WGS84; } GeodeticDatum geoDatum = (GeodeticDatum) datum; if (geoDatum.getPrimeMeridian().getGreenwichLongitude() != 0) { geoDatum = new DefaultGeodeticDatum(geoDatum.getName().getCode(), geoDatum.getEllipsoid(), DefaultPrimeMeridian.GREENWICH); } else if (crs instanceof GeographicCRS) { if (CRS.equalsIgnoreMetadata(DefaultEllipsoidalCS.GEODETIC_2D, crs.getCoordinateSystem())) { return (GeographicCRS) crs; } } return new DefaultGeographicCRS(crs.getName().getCode(), geoDatum, DefaultEllipsoidalCS.GEODETIC_2D); } /** * Transforms the relative distance vector specified by {@code source} and stores * the result in {@code dest}. A relative distance vector is transformed without * applying the translation components. * * @param transform The transform to apply. * @param origin The position where to compute the delta transform in the source CS. * @param source The distance vector to be delta transformed * @return The result of the transformation. * @throws TransformException if the transformation failed. * * @since 2.3 */ public static DirectPosition deltaTransform(final MathTransform transform, final DirectPosition origin, final DirectPosition source) throws TransformException { final int sourceDim = transform.getSourceDimensions(); final int targetDim = transform.getTargetDimensions(); DirectPosition P1 = new GeneralDirectPosition(sourceDim); DirectPosition P2 = new GeneralDirectPosition(sourceDim); for (int i=0; i<sourceDim; i++) { final double c = origin.getOrdinate(i); final double d = source.getOrdinate(i) * 0.5; P1.setOrdinate(i, c-d); P2.setOrdinate(i, c+d); } P1 = transform.transform(P1, (sourceDim==targetDim) ? P1 : null); P2 = transform.transform(P2, (sourceDim==targetDim) ? P2 : null); for (int i=0; i<targetDim; i++) { P2.setOrdinate(i, P2.getOrdinate(i) - P1.getOrdinate(i)); } return P2; } /** * Transforms the relative distance vector specified by {@code source} and stores * the result in {@code dest}. A relative distance vector is transformed without * applying the translation components. * * @param transform The transform to apply. * @param origin The position where to compute the delta transform in the source CS. * @param source The distance vector to be delta transformed * @param dest The resulting transformed distance vector, or {@code null} * @return The result of the transformation. * @throws TransformException if the transformation failed. * * @see AffineTransform#deltaTransform(Point2D,Point2D) */ public static Point2D deltaTransform(final MathTransform2D transform, final Point2D origin, final Point2D source, Point2D dest) throws TransformException { if (transform instanceof AffineTransform) { return ((AffineTransform) transform).deltaTransform(source, dest); } final double ox = origin.getX(); final double oy = origin.getY(); final double dx = source.getX()*0.5; final double dy = source.getY()*0.5; Point2D P1 = new Point2D.Double(ox-dx, oy-dy); Point2D P2 = new Point2D.Double(ox+dx, oy+dy); P1 = transform.transform(P1, P1); P2 = transform.transform(P2, P2); if (dest == null) { dest = P2; } dest.setLocation(P2.getX()-P1.getX(), P2.getY()-P1.getY()); return dest; } /** * Returns a character string for the specified geographic area. The string will have the * form "45°00.00'N-50°00.00'N 30°00.00'E-40°00.00'E". If a map projection is required in * order to obtain this representation, it will be automatically applied. This string is * mostly used for debugging purpose. * * @param crs The coordinate reference system of the bounding box. * @param bounds The bounding box to format. * @return The bounding box formatted as a string. * * @todo Move this method as a static method in {@link org.geotools.referencing.CRS}. * Or yet better: move formatting code in {@code GeographicBoundingBox.toString()} * method, and move the transformation code into {@code GeographicBoundingBox} * constructor. * * @todo Do not requires specifically WGS 84, using {@link #getStandardGeographicCRS}. */ public static String toWGS84String(CoordinateReferenceSystem crs, Rectangle2D bounds) { Exception exception; final StringBuffer buffer = new StringBuffer(); final CoordinateReferenceSystem crs2D = CRS.getHorizontalCRS(crs); if (crs2D == null) { exception = new UnsupportedOperationException(Errors.format( ErrorKeys.CANT_SEPARATE_CRS_$1, crs.getName())); } else try { if (!CRS.equalsIgnoreMetadata(DefaultGeographicCRS.WGS84, crs2D)) { final CoordinateOperation op = ReferencingFactoryFinder.getCoordinateOperationFactory(null) .createOperation(crs2D, DefaultGeographicCRS.WGS84); bounds = CRS.transform(op, bounds, null); } final AngleFormat fmt = new AngleFormat("DD°MM.m'"); fmt.format(new Latitude(bounds.getMinY()), buffer, null).append('-'); fmt.format(new Latitude(bounds.getMaxY()), buffer, null).append(' '); fmt.format(new Longitude(bounds.getMinX()), buffer, null).append('-'); fmt.format(new Longitude(bounds.getMaxX()), buffer, null); return buffer.toString(); } catch (TransformException e) { exception = e; } catch (FactoryException e) { exception = e; } buffer.append(Classes.getShortClassName(exception)); final String message = exception.getLocalizedMessage(); if (message != null) { buffer.append(": ").append(message); } return buffer.toString(); } }